From b7aa23e931ef135827687a07e58341cb8c5019fb Mon Sep 17 00:00:00 2001 From: Nevo Date: Tue, 11 Dec 2018 20:20:02 +0200 Subject: [PATCH 01/10] Changing the "defaults" property & adding "assignDefaultOptions" method - the "defaults" property called now "defaultOptions", also the file that hold all the default options called "default-options" - adding "assignDefaultOptions" method to make it more easy to change the defaults --- src/core/Form.ts | 16 ++++++++--- src/{defaults.ts => default-options.ts} | 8 +++--- src/helpers/generateOptions.ts | 2 +- tests/core/Form.spec.ts | 35 ++++++++++++++++++++----- tests/core/Validator.spec.ts | 2 +- tests/helpers/generateOptions.spec.ts | 2 +- tsconfig.json | 3 ++- 7 files changed, 51 insertions(+), 17 deletions(-) rename src/{defaults.ts => default-options.ts} (87%) diff --git a/src/core/Form.ts b/src/core/Form.ts index 43dc6f0..2eaed60 100644 --- a/src/core/Form.ts +++ b/src/core/Form.ts @@ -5,13 +5,13 @@ import { Field, Options, SubmitCallback } from '../types' import { isObject } from '../utils' import generateDefaultLabel from '../helpers/generateDefaultLabel' import generateOptions from '../helpers/generateOptions' -import defaultsOptions from '../defaults' +import defaultsOptions from '../default-options' export class Form { /** * Defaults options for the Form instance */ - public static defaults: Options = defaultsOptions + public static defaultOptions: Options = defaultsOptions /** * determine if the form is on submitting mode @@ -57,7 +57,7 @@ export class Form { /** * Options of the Form */ - public $options: Options = Form.defaults + public $options: Options = Form.defaultOptions /** * constructor of the class @@ -71,6 +71,16 @@ export class Form { .resetValues() } + /** + * setting up default options for the Form class in more + * convenient way then "Form.defaultOptions.validation.something = something" + * + * @param options + */ + public static assignDefaultOptions(options: Options = {}): void { + Form.defaultOptions = generateOptions(Form.defaultOptions, options) + } + /** * Hook for successful submission * use Form.successfulSubmissionHook = () => {}; diff --git a/src/defaults.ts b/src/default-options.ts similarity index 87% rename from src/defaults.ts rename to src/default-options.ts index 4ee829c..dde5ea0 100644 --- a/src/defaults.ts +++ b/src/default-options.ts @@ -1,9 +1,9 @@ +import { Options } from './types' + /** * Default options that provide to Form instance */ -import { Options } from './types' - -const defaults: Options = { +const defaultOptions: Options = { successfulSubmission: { clearErrors: true, clearTouched: true, @@ -19,4 +19,4 @@ const defaults: Options = { }, } -export default defaults +export default defaultOptions diff --git a/src/helpers/generateOptions.ts b/src/helpers/generateOptions.ts index 937e4db..565f323 100644 --- a/src/helpers/generateOptions.ts +++ b/src/helpers/generateOptions.ts @@ -31,7 +31,7 @@ const assignNewOptions = ( } /** - * generate Options base on the defaults Options and new options + * generate Options base on the defaultOptions Options and new options * * @param defaultOptions * @param overwriteOptions diff --git a/tests/core/Form.spec.ts b/tests/core/Form.spec.ts index b173060..15c6ff8 100644 --- a/tests/core/Form.spec.ts +++ b/tests/core/Form.spec.ts @@ -1,9 +1,9 @@ import { Errors } from '../../src/core/Errors' import { Validator } from '../../src/core/Validator' import { Touched } from '../../src/core/Touched' -import { Form } from '../../src' +import { Form } from '../../src/core/Form' import generateOptions from '../../src/helpers/generateOptions' -import defaultOptionsSource from '../../src/defaults' +import defaultOptionsSource from '../../src/default-options' jest.mock('../../src/core/Errors') jest.mock('../../src/core/Validator') @@ -364,11 +364,34 @@ describe('Form.ts', () => { } }) - it('should change the defaults options of the Form', () => { - Form.defaults.validation.defaultMessage = ({ label, value }) => + it('should change the defaultOptions options of the Form', () => { + Form.defaultOptions.validation.defaultMessage = ({ label, value }) => `${label}: ${value}` - Form.defaults.successfulSubmission.clearErrors = false - Form.defaults.successfulSubmission.resetValues = false + Form.defaultOptions.successfulSubmission.clearErrors = false + Form.defaultOptions.successfulSubmission.resetValues = false + + let form = new Form(data) + + expect( + form.$options.validation.defaultMessage( + { label: 'a', value: 'b', key: 'c' }, + form + ) + ).toEqual('a: b') + expect(form.$options.successfulSubmission.clearErrors).toBe(false) + expect(form.$options.successfulSubmission.resetValues).toBe(false) + }) + + it('should assign defaultOptions to the form', () => { + Form.assignDefaultOptions({ + validation: { + defaultMessage: ({ label, value }) => `${label}: ${value}`, + }, + successfulSubmission: { + clearErrors: false, + resetValues: false, + }, + }) let form = new Form(data) diff --git a/tests/core/Validator.spec.ts b/tests/core/Validator.spec.ts index 5562ba3..3571241 100644 --- a/tests/core/Validator.spec.ts +++ b/tests/core/Validator.spec.ts @@ -1,6 +1,6 @@ import { Form } from '../../src' import { Validator } from '../../src/core/Validator' -import defaultOptions from '../../src/defaults' +import defaultOptions from '../../src/default-options' jest.mock('../../src/core/Form') diff --git a/tests/helpers/generateOptions.spec.ts b/tests/helpers/generateOptions.spec.ts index a72a6b9..0dd3fdd 100644 --- a/tests/helpers/generateOptions.spec.ts +++ b/tests/helpers/generateOptions.spec.ts @@ -1,5 +1,5 @@ import generateOptions from '../../src/helpers/generateOptions' -import defaultOptions from '../../src/defaults' +import defaultOptions from '../../src/default-options' describe('generateOptions.ts', () => { it('should generate new options object from default options and new options Object', () => { diff --git a/tsconfig.json b/tsconfig.json index e16a9c8..a3350b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "outDir": "./lib/", "module": "es2015", "target": "es2015", - "declaration": true + "declaration": true, + "esModuleInterop": true }, "include": [ "src/**/*" From d0a0338c77a1102b97930fc013ef79ce3b6311e4 Mon Sep 17 00:00:00 2001 From: Nevo Date: Thu, 13 Dec 2018 18:10:51 +0200 Subject: [PATCH 02/10] adding coverage --- jest.config.js | 2 ++ tests/core/Validator.spec.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 650c513..b14d4cd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ module.exports = { preset: 'ts-jest', + collectCoverage: true, + coverageDirectory: './coverage', } diff --git a/tests/core/Validator.spec.ts b/tests/core/Validator.spec.ts index 3571241..29dcda0 100644 --- a/tests/core/Validator.spec.ts +++ b/tests/core/Validator.spec.ts @@ -1,4 +1,4 @@ -import { Form } from '../../src' +import { Form } from '../../src/core/Form' import { Validator } from '../../src/core/Validator' import defaultOptions from '../../src/default-options' From 4e560e33e9bae94af3fc031ad92cf562c7d746ad Mon Sep 17 00:00:00 2001 From: Nevo Date: Fri, 14 Dec 2018 23:07:28 +0200 Subject: [PATCH 03/10] Interceptor Implementation refactor the whole `submit` method in Form to interceptors now all the "side effects" that happen before and after submission is on interceptors function --- src/core/Form.ts | 156 +++++++++++----------- src/core/InterceptorManager.ts | 60 +++++++++ src/interceptors/beforeSubmission.ts | 35 +++++ src/interceptors/index.ts | 7 + src/interceptors/submissionComplete.ts | 59 +++++++++ src/types.ts | 18 +++ tests/core/{ => Form}/Form.spec.ts | 172 +++---------------------- tests/core/Form/Form.submit.spec.ts | 167 ++++++++++++++++++++++++ 8 files changed, 437 insertions(+), 237 deletions(-) create mode 100644 src/core/InterceptorManager.ts create mode 100644 src/interceptors/beforeSubmission.ts create mode 100644 src/interceptors/index.ts create mode 100644 src/interceptors/submissionComplete.ts rename tests/core/{ => Form}/Form.spec.ts (67%) create mode 100644 tests/core/Form/Form.submit.spec.ts diff --git a/src/core/Form.ts b/src/core/Form.ts index 2eaed60..0450249 100644 --- a/src/core/Form.ts +++ b/src/core/Form.ts @@ -1,11 +1,19 @@ import { Errors } from './Errors' import { Validator } from './Validator' import { Touched } from './Touched' -import { Field, Options, SubmitCallback } from '../types' +import { InterceptorManager } from './InterceptorManager' import { isObject } from '../utils' import generateDefaultLabel from '../helpers/generateDefaultLabel' import generateOptions from '../helpers/generateOptions' import defaultsOptions from '../default-options' +import basicInterceptors from '../interceptors/index' +import { + Field, + InterceptorHandler, + InterceptorManagersObject, + Options, + SubmitCallback, +} from '../types' export class Form { /** @@ -13,6 +21,11 @@ export class Form { */ public static defaultOptions: Options = defaultsOptions + /** + * Interceptors that will run in every submission + */ + public static defaultInterceptors: InterceptorManagersObject + /** * determine if the form is on submitting mode */ @@ -59,6 +72,11 @@ export class Form { */ public $options: Options = Form.defaultOptions + /** + * holds the interceptor managers + */ + public $interceptors: InterceptorManagersObject + /** * constructor of the class * @@ -82,33 +100,23 @@ export class Form { } /** - * Hook for successful submission - * use Form.successfulSubmissionHook = () => {}; - * for extending the successful submission handling + * assign options to Options object * - * @param response - * @param form - */ - public static successfulSubmissionHook( - response: any, - form: Form - ): Promise { - return Promise.resolve(response) + * @param options + */ + public assignOptions(options: Options) { + this.$options = generateOptions(this.$options, options) + + return this } /** - * Hook for un successful submission - * use Form.unSuccessfulSubmissionHook = () => {}; - * for extending the un successful submission handling + * checks if field exits or not in the form class * - * @param error - * @param form - */ - public static unSuccessfulSubmissionHook( - error: any, - form: Form - ): Promise { - return Promise.reject(error) + * @param fieldKey + */ + public hasField(fieldKey: string): boolean { + return this.hasOwnProperty(fieldKey) } /** @@ -254,44 +262,6 @@ export class Form { return this[fieldKey] !== this.$initialValues[fieldKey] } - /** - * assign options to Options object - * - * @param options - */ - public assignOptions(options: Options) { - this.$options = generateOptions(this.$options, options) - - return this - } - - /** - * submit the form, this method received a callback that - * will submit the form and must return a Promise. - * - * @param callback - */ - public submit(callback: SubmitCallback): Promise { - if (this.$options.validation.onSubmission && !this.validate()) { - return Promise.reject({ message: 'Form is not valid' }) - } - - this.$submitting = true - - return callback(this) - .then(this.successfulSubmission.bind(this)) - .catch(this.unSuccessfulSubmission.bind(this)) - } - - /** - * checks if field exits or not in the form class - * - * @param fieldKey - */ - public hasField(fieldKey: string): boolean { - return this.hasOwnProperty(fieldKey) - } - /** * handle change/input on field * @@ -342,6 +312,36 @@ export class Form { return this } + /** + * submit the form, this method received a callback that + * will submit the form and must return a Promise. + * + * @param callback + */ + public submit(callback: SubmitCallback): Promise { + let chain: any[] = [this.wrapSubmitCallBack(callback), null] + + this.$interceptors.beforeSubmission + .merge(basicInterceptors.beforeSubmission) + .forEach((handler: InterceptorHandler) => + chain.unshift(handler.fulfilled, handler.rejected) + ) + + this.$interceptors.submissionComplete + .merge(basicInterceptors.submissionComplete) + .forEach((handler: InterceptorHandler) => + chain.push(handler.fulfilled, handler.rejected) + ) + + let promise: Promise = Promise.resolve(this) + + while (chain.length) { + promise = promise.then(chain.shift(), chain.shift()) + } + + return promise + } + /** * Init the form * fill all the values that should be filled (Validator, OriginalData etc..( @@ -377,6 +377,10 @@ export class Form { this.$validator = new Validator(rules, this.$options.validation) this.$errors = new Errors() this.$touched = new Touched() + this.$interceptors = { + beforeSubmission: new InterceptorManager(), + submissionComplete: new InterceptorManager(), + } return this } @@ -395,28 +399,16 @@ export class Form { } /** - * Successful submission method + * wrap the submit callback function + * to normalize the promise resolve or reject parameter * - * @param response - */ - private successfulSubmission(response: any): Promise { - this.$submitting = false - - this.$options.successfulSubmission.clearErrors && this.$errors.clear() - this.$options.successfulSubmission.clearTouched && this.$touched.clear() - this.$options.successfulSubmission.resetValues && this.resetValues() - - return Form.successfulSubmissionHook(response, this) - } - - /** - * UnSuccessful submission method - * - * @param error + * @param callback */ - private unSuccessfulSubmission(error: any): Promise { - this.$submitting = false - - return Form.unSuccessfulSubmissionHook(error, this) + private wrapSubmitCallBack(callback: SubmitCallback): Function { + return () => + callback(this).then( + response => Promise.resolve({ response, form: this }), + error => Promise.reject({ error, form: this }) + ) } } diff --git a/src/core/InterceptorManager.ts b/src/core/InterceptorManager.ts new file mode 100644 index 0000000..cf0638d --- /dev/null +++ b/src/core/InterceptorManager.ts @@ -0,0 +1,60 @@ +import { InterceptorHandler } from '../types' + +export class InterceptorManager { + /** + * holds all the function that should run on the chain + */ + $handlers: InterceptorHandler[] = [] + + /** + * adding function to the handlers chain + * and returns the position of the handler in the chain + * + * @param fulfilled + * @param rejected + */ + public use(fulfilled: Function | null, rejected: Function | null): number { + this.$handlers.push({ + fulfilled, + rejected, + }) + + return this.$handlers.length - 1 + } + + /** + * eject a handler from the chain, by his position. + * + * @param position + */ + public eject(position: number): void { + if (this.$handlers[position]) { + this.$handlers[position] = null + } + } + + /** + * letting you merge more interceptors to the handlers array + * NOTICE: this will put the interceptors at the BEGINNING of the chain + * + * @param interceptors + */ + public merge(interceptors: InterceptorHandler[]): InterceptorManager { + this.$handlers = [...interceptors, ...this.$handlers] + + return this + } + + /** + * run over the handlers + * + * @param func + */ + public forEach(func: Function): void { + this.$handlers.forEach((handler: InterceptorHandler) => { + if (handler !== null) { + func(handler) + } + }) + } +} diff --git a/src/interceptors/beforeSubmission.ts b/src/interceptors/beforeSubmission.ts new file mode 100644 index 0000000..546ffa1 --- /dev/null +++ b/src/interceptors/beforeSubmission.ts @@ -0,0 +1,35 @@ +import { Form } from '../core/Form' +import { InterceptorHandler } from '../types' + +/** + * validate the form before submission + * only if the option of validation on submission set as true. + */ +export const validateForm: InterceptorHandler = { + fulfilled: (form: Form): Promise => { + if (form.$options.validation.onSubmission && !form.validate()) { + return Promise.reject({ error: { message: 'Form is invalid.' }, form }) + } + + return Promise.resolve(form) + }, + rejected: null, +} + +/** + * Set the $submitting as true (this is must to be the LAST interceptor before submitting) + * but the FIRST here in the export array + */ +export const setSubmittingAsTrue: InterceptorHandler = { + fulfilled: (form: Form): Promise => { + form.$submitting = true + + return Promise.resolve(form) + }, + rejected: null, +} + +/** + * the order of the interceptors will be from the LAST to the first + */ +export default [setSubmittingAsTrue, validateForm] diff --git a/src/interceptors/index.ts b/src/interceptors/index.ts new file mode 100644 index 0000000..4aef5cd --- /dev/null +++ b/src/interceptors/index.ts @@ -0,0 +1,7 @@ +import beforeSubmission from './beforeSubmission' +import submissionComplete from './submissionComplete' + +export default { + beforeSubmission, + submissionComplete, +} diff --git a/src/interceptors/submissionComplete.ts b/src/interceptors/submissionComplete.ts new file mode 100644 index 0000000..6389403 --- /dev/null +++ b/src/interceptors/submissionComplete.ts @@ -0,0 +1,59 @@ +import { Form } from '../core/Form' +import { InterceptorHandler } from '../types' + +/** + * The interface of an object with successful response from the + * SubmitCallback function + */ +interface successfulResponse { + form: Form + response: any +} + +/** + * The interface of an object with unsuccessful response from the + * SubmitCallback function + */ +interface InvalidResponse { + form: Form + error: any +} + +/** + * set the $submitting property as false event if the submission + * was successful or not + */ +export const setSubmittingAsFalse: InterceptorHandler = { + fulfilled: (response: successfulResponse) => { + response.form.$submitting = false + + return Promise.resolve(response) + }, + rejected: (error: InvalidResponse) => { + error.form.$submitting = false + + return Promise.reject(error) + }, +} + +/** + * clear the form (errors, touched and values) base on the options + * that was set at the form. + */ +export const clearForm: InterceptorHandler = { + fulfilled: (response: successfulResponse) => { + const { form } = response + + form.$options.successfulSubmission.clearErrors && form.$errors.clear() + form.$options.successfulSubmission.clearTouched && form.$touched.clear() + form.$options.successfulSubmission.resetValues && form.resetValues() + + return Promise.resolve(response) + }, + rejected: null, +} + +/** + * the order of the interceptors will be from the FIRST to the last + */ +export default [setSubmittingAsFalse, clearForm] diff --git a/src/types.ts b/src/types.ts index 44c03d0..817ef50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import { Form } from './core/Form' +import { InterceptorManager } from './core/InterceptorManager' /** * Field object that passes the PassesFunction and MessageFunction, @@ -112,6 +113,23 @@ export interface Options { validation?: ValidationOptions } +/** + * an object that hold 2 function one for fulfill and one for reject + */ +export interface InterceptorHandler { + fulfilled: Function + rejected: Function +} + +/** + * an object that hold only InterceptorManagers as value + */ +export interface InterceptorManagersObject { + beforeSubmission: InterceptorManager + submissionComplete: InterceptorManager + [key: string]: InterceptorManager +} + /** * Submit callback interface, * the function the should pass to submit method in Form class diff --git a/tests/core/Form.spec.ts b/tests/core/Form/Form.spec.ts similarity index 67% rename from tests/core/Form.spec.ts rename to tests/core/Form/Form.spec.ts index 15c6ff8..8d3cd4b 100644 --- a/tests/core/Form.spec.ts +++ b/tests/core/Form/Form.spec.ts @@ -1,13 +1,14 @@ -import { Errors } from '../../src/core/Errors' -import { Validator } from '../../src/core/Validator' -import { Touched } from '../../src/core/Touched' -import { Form } from '../../src/core/Form' -import generateOptions from '../../src/helpers/generateOptions' -import defaultOptionsSource from '../../src/default-options' - -jest.mock('../../src/core/Errors') -jest.mock('../../src/core/Validator') -jest.mock('../../src/core/Touched') +import { Errors } from '../../../src/core/Errors' +import { Validator } from '../../../src/core/Validator' +import { Touched } from '../../../src/core/Touched' +import { Form } from '../../../src/core/Form' +import generateOptions from '../../../src/helpers/generateOptions' +import defaultOptionsSource from '../../../src/default-options' +import { InterceptorManager } from '../../../src/core/InterceptorManager' + +jest.mock('../../../src/core/Errors') +jest.mock('../../../src/core/Validator') +jest.mock('../../../src/core/Touched') describe('Form.ts', () => { interface FormData { @@ -72,6 +73,12 @@ describe('Form.ts', () => { ) expect(Errors).toHaveBeenCalled() expect(Touched).toHaveBeenCalled() + expect(form.$interceptors.beforeSubmission).toBeInstanceOf( + InterceptorManager + ) + expect(form.$interceptors.submissionComplete).toBeInstanceOf( + InterceptorManager + ) }) it('should access the form props', () => { @@ -141,126 +148,6 @@ describe('Form.ts', () => { ) }) - it('should successfully submitted if the callback returns Promise.resolve', async () => { - let form = new Form(data) as Form & FormData - - let responseParam = { - status: 200, - data: {}, - } - - Form.successfulSubmissionHook = jest.fn(() => - Promise.resolve(responseParam) - ) - Form.unSuccessfulSubmissionHook = jest.fn() - form.resetValues = jest.fn() - - let mockCallable = jest.fn(() => Promise.resolve(responseParam)) - - let response = await form.submit(mockCallable) - - expect(mockCallable.mock.calls.length).toBe(1) - expect(form.$errors.clear).toHaveBeenCalledTimes(1) - expect(form.$touched.clear).toHaveBeenCalledTimes(1) - expect(form.resetValues).toHaveBeenCalledTimes(1) - expect(Form.successfulSubmissionHook).toBeCalledWith(responseParam, form) - expect(Form.unSuccessfulSubmissionHook).not.toHaveBeenCalledTimes(1) - expect(response).toBe(responseParam) - }) - - it('should send reject promise if the callback was return reject promise', async () => { - let form = new Form(data) as Form & FormData - - let responseParam = { - status: 404, - } - - Form.successfulSubmissionHook = jest.fn() - Form.unSuccessfulSubmissionHook = jest.fn(() => - Promise.reject(responseParam) - ) - - let mockCallable = jest.fn(() => Promise.reject(responseParam)) - - expect.assertions(4) - - try { - await form.submit(mockCallable) - } catch (e) { - expect(mockCallable.mock.calls.length).toBe(1) - expect(Form.unSuccessfulSubmissionHook).toBeCalledWith( - responseParam, - form - ) - expect(Form.successfulSubmissionHook).not.toHaveBeenCalled() - expect(e).toBe(responseParam) - } - }) - - it('should set $submitting as true if submit method is called and false if validation failed and callback method not called', async () => { - let form = new Form(data) as Form & FormData - - let mockCallable = jest.fn(() => Promise.resolve()) - form.validate = jest.fn(() => false) - form.submit(mockCallable).catch(() => false) - - expect(form.$submitting).toBe(false) - - form.validate = jest.fn(() => true) - form.submit(mockCallable) - - expect(form.$submitting).toBe(true) - expect(mockCallable.mock.calls.length).toBe(1) - }) - - it('should not resetValues after success submission if resetValues option is false', async () => { - let form = new Form(data, { - successfulSubmission: { - resetValues: false, - }, - }) as Form & FormData - - form.resetValues = jest.fn() - - await form.submit(() => Promise.resolve()) - - expect(form.$errors.clear).toHaveBeenCalledTimes(1) - expect(form.$touched.clear).toHaveBeenCalledTimes(1) - expect(form.resetValues).not.toHaveBeenCalled() - }) - - it('should not clear errors after success submission if clearErrorsAfterSuccessfulSubmission option is false', async () => { - let form = new Form(data, { - successfulSubmission: { - clearErrors: false, - }, - }) as Form & FormData - - form.resetValues = jest.fn() - - await form.submit(() => Promise.resolve()) - - expect(form.$errors.clear).not.toHaveBeenCalled() - expect(form.$touched.clear).toHaveBeenCalledTimes(1) - expect(form.resetValues).toHaveBeenCalledTimes(1) - }) - - it('should not clear touched after success submission if successfulSubmission.clearTouched set to false', async () => { - let form = new Form(data, { - successfulSubmission: { - clearTouched: false, - }, - }) as Form & FormData - - form.resetValues = jest.fn() - - await form.submit(() => Promise.resolve()) - - expect(form.$errors.clear).toHaveBeenCalledTimes(1) - expect(form.$touched.clear).not.toHaveBeenCalled() - expect(form.resetValues).toHaveBeenCalledTimes(1) - }) - it('should call to validate specific field or all the fields', () => { let form = new Form(data) as Form & FormData @@ -339,31 +226,6 @@ describe('Form.ts', () => { expect(form.validateAll()).toBe(false) }) - it('should validate the form on submission if the option is set to validate the form', async () => { - let form = new Form( - { - name: 'Nevo', - rules: [() => true], - }, - { - validation: { - onSubmission: true, - }, - } - ) - - form.validate = jest.fn(() => false) - - expect.assertions(2) - - try { - await form.submit(() => Promise.resolve()) - } catch (e) { - expect(form.validate).toBeCalled() - expect(e.hasOwnProperty('message')).toBe(true) - } - }) - it('should change the defaultOptions options of the Form', () => { Form.defaultOptions.validation.defaultMessage = ({ label, value }) => `${label}: ${value}` diff --git a/tests/core/Form/Form.submit.spec.ts b/tests/core/Form/Form.submit.spec.ts new file mode 100644 index 0000000..48766d0 --- /dev/null +++ b/tests/core/Form/Form.submit.spec.ts @@ -0,0 +1,167 @@ +import { Form } from '../../../src/core/Form' + +jest.mock('../../../src/core/Errors') +jest.mock('../../../src/core/Validator') +jest.mock('../../../src/core/Touched') + +describe('Form.submit.ts', () => { + it('should successfully submitted if the callback returns Promise.resolve', async () => { + let form = new Form({}) as Form + + let responseParam = { + status: 200, + data: {}, + } + + form.resetValues = jest.fn() + + expect.assertions(6) + + let mockCallable = jest.fn(formParam => { + expect(formParam).toBe(form) + + return Promise.resolve(responseParam) + }) + + let response = await form.submit(mockCallable) + + expect(mockCallable.mock.calls.length).toBe(1) + expect(form.$errors.clear).toHaveBeenCalledTimes(1) + expect(form.$touched.clear).toHaveBeenCalledTimes(1) + expect(form.resetValues).toHaveBeenCalledTimes(1) + expect(response).toEqual({ form, response: responseParam }) + }) + + it('should send reject promise if the callback was return reject promise', async () => { + let form = new Form({}) as Form + + let responseParam = { + status: 404, + } + + let mockCallable = jest.fn(() => Promise.reject(responseParam)) + + expect.assertions(2) + + try { + await form.submit(mockCallable) + } catch (e) { + expect(mockCallable.mock.calls.length).toBe(1) + expect(e).toEqual({ error: responseParam, form }) + } + }) + + it('should validate the form on submission if the option is set to validate the form', async () => { + let form = new Form( + { + name: 'Nevo', + rules: [() => true], + }, + { + validation: { + onSubmission: true, + }, + } + ) + + // mock the validate method to return FALSE + form.validate = jest.fn(() => false) + + expect.assertions(4) + + try { + await form.submit(() => Promise.resolve()) + } catch (e) { + expect(form.validate).toBeCalled() + expect(e.hasOwnProperty('error')).toBe(true) + expect(e.error.hasOwnProperty('message')).toBe(true) + expect(e.form).toBe(form) + } + }) + + it('should set $submitting as true if submit method is called', () => { + let form = new Form({}) as Form + + form.validate = jest.fn(() => true) + + expect.assertions(2) + + return form + .submit(formParam => { + expect(formParam.$submitting).toBe(true) + + return new Promise(resolve => resolve('Yay!')) + }) + .then(() => { + expect(form.$submitting).toBe(false) + }) + }) + + it('should set $submitting false if validation failed and callback method not called', () => { + let form = new Form({}) as Form + + let mockCallable = jest.fn(() => Promise.resolve()) + form.validate = jest.fn(() => false) + form.submit(mockCallable).catch(() => false) + + expect(form.$submitting).toBe(false) + expect(mockCallable).toHaveBeenCalledTimes(0) + }) + + it('should not resetValues after success submission if resetValues option is false', async () => { + let form = new Form( + {}, + { + successfulSubmission: { + resetValues: false, + }, + } + ) as Form + + form.resetValues = jest.fn() + + await form.submit(() => Promise.resolve()) + + expect(form.$errors.clear).toHaveBeenCalledTimes(1) + expect(form.$touched.clear).toHaveBeenCalledTimes(1) + expect(form.resetValues).not.toHaveBeenCalled() + }) + + it('should not clear errors after success submission if clearErrorsAfterSuccessfulSubmission option is false', async () => { + let form = new Form( + {}, + { + successfulSubmission: { + clearErrors: false, + }, + } + ) as Form + + form.resetValues = jest.fn() + + await form.submit(() => Promise.resolve()) + + expect(form.$errors.clear).not.toHaveBeenCalled() + expect(form.$touched.clear).toHaveBeenCalledTimes(1) + expect(form.resetValues).toHaveBeenCalledTimes(1) + }) + + it('should not clear touched after success submission if successfulSubmission.clearTouched set to false', async () => { + let form = new Form( + {}, + { + successfulSubmission: { + clearTouched: false, + }, + } + ) as Form & FormData + + form.resetValues = jest.fn() + + await form.submit(() => Promise.resolve()) + + expect(form.$errors.clear).toHaveBeenCalledTimes(1) + expect(form.$touched.clear).not.toHaveBeenCalled() + expect(form.resetValues).toHaveBeenCalledTimes(1) + }) +}) From b700391b1a9c6bed5497f9bcc5bb1fd11495c370 Mon Sep 17 00:00:00 2001 From: Nevo Date: Sat, 15 Dec 2018 15:35:05 +0200 Subject: [PATCH 04/10] Testing Interceptors in more depth. + fixing some errors in the submit "chain" + updating dependencies for the project. --- jest.config.js | 1 + package-lock.json | 67 ++++++++++++--- package.json | 9 +- src/core/Form.ts | 10 +-- src/core/InterceptorManager.ts | 5 +- src/interceptors/beforeSubmission.ts | 2 +- tests/core/Form/Form.interceptors.spec.ts | 100 ++++++++++++++++++++++ tsconfig.json | 6 +- 8 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 tests/core/Form/Form.interceptors.spec.ts diff --git a/jest.config.js b/jest.config.js index b14d4cd..6f8abb4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,4 +2,5 @@ module.exports = { preset: 'ts-jest', collectCoverage: true, coverageDirectory: './coverage', + setupTestFrameworkScriptFile: 'jest-extended', } diff --git a/package-lock.json b/package-lock.json index 5dc7975..a1e9401 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "form-wrapper-js", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -39,9 +39,9 @@ "dev": true }, "@types/jest": { - "version": "23.3.9", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.9.tgz", - "integrity": "sha512-wNMwXSUcwyYajtbayfPp55tSayuDVU6PfY5gzvRSj80UvxdXEJOVPnUVajaOp7NgXLm+1e2ZDLULmpsU9vDvQw==", + "version": "23.3.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.10.tgz", + "integrity": "sha512-DC8xTuW/6TYgvEg3HEXS7cu9OijFqprVDXXiOcdOKZCU/5PJNLZU37VVvmZHdtMiGOa8wAA/We+JzbdxFzQTRQ==", "dev": true }, "@types/node": { @@ -2993,6 +2993,46 @@ "jest-util": "^23.4.0" } }, + "jest-extended": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-0.11.0.tgz", + "integrity": "sha512-7VJQDyObjKRqaiaRvzSbWchwTvk7mQYPaEzPcK2Nwrna6ZSPe/AB9aPDjgH2oT0QONtF6FvM3GIvDdJhttJeaA==", + "dev": true, + "requires": { + "expect": "^23.6.0", + "jest-get-type": "^22.4.3", + "jest-matcher-utils": "^22.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-matcher-utils": { + "version": "22.4.3", + "resolved": "http://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", + "integrity": "sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-get-type": "^22.4.3", + "pretty-format": "^22.4.3" + } + }, + "pretty-format": { + "version": "22.4.3", + "resolved": "http://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", + "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } + } + }, "jest-get-type": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", @@ -4392,9 +4432,9 @@ } }, "rollup-plugin-typescript2": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.18.0.tgz", - "integrity": "sha512-AL7LJl31bYO/x8zO1fuE7ACn/2nDs9DVYL3qjiWSYg5LC4EV/iKuCL4Fm6pjzEqCB4fFIMXoUuGUf5R+BLNKSg==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.18.1.tgz", + "integrity": "sha512-aR2m5NCCAUV/KpcKgCWX6Giy8rTko9z92b5t0NX9eZyjOftCvcdDFa1C9Ze/9yp590hnRymr5hG0O9SAXi1oUg==", "dev": true, "requires": { "fs-extra": "7.0.0", @@ -5252,9 +5292,9 @@ "dev": true }, "ts-jest": { - "version": "23.10.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.10.4.tgz", - "integrity": "sha512-oV/wBwGUS7olSk/9yWMiSIJWbz5xO4zhftnY3gwv6s4SMg6WHF1m8XZNBvQOKQRiTAexZ9754Z13dxBq3Zgssw==", + "version": "23.10.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.10.5.tgz", + "integrity": "sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A==", "dev": true, "requires": { "bs-logger": "0.x", @@ -5263,6 +5303,7 @@ "json5": "2.x", "make-error": "1.x", "mkdirp": "0.x", + "resolve": "1.x", "semver": "^5.5", "yargs-parser": "10.x" }, @@ -5324,9 +5365,9 @@ } }, "typescript": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz", - "integrity": "sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", + "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index a4e4047..d68e523 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,13 @@ "author": "Nevo Golan ", "license": "MIT", "devDependencies": { - "@types/jest": "^23.3.9", + "@types/jest": "^23.3.10", "jest": "^23.6.0", + "jest-extended": "^0.11.0", "prettier": "1.15.3", "rollup": "^0.67.4", - "rollup-plugin-typescript2": "^0.18.0", - "ts-jest": "^23.10.4", - "typescript": "^3.1.6" + "rollup-plugin-typescript2": "^0.18.1", + "ts-jest": "^23.10.5", + "typescript": "^3.2.2" } } diff --git a/src/core/Form.ts b/src/core/Form.ts index 0450249..d2810de 100644 --- a/src/core/Form.ts +++ b/src/core/Form.ts @@ -21,11 +21,6 @@ export class Form { */ public static defaultOptions: Options = defaultsOptions - /** - * Interceptors that will run in every submission - */ - public static defaultInterceptors: InterceptorManagersObject - /** * determine if the form is on submitting mode */ @@ -319,7 +314,10 @@ export class Form { * @param callback */ public submit(callback: SubmitCallback): Promise { - let chain: any[] = [this.wrapSubmitCallBack(callback), null] + let chain: any[] = [ + this.wrapSubmitCallBack(callback), + error => Promise.reject({ error, form: this }), + ] this.$interceptors.beforeSubmission .merge(basicInterceptors.beforeSubmission) diff --git a/src/core/InterceptorManager.ts b/src/core/InterceptorManager.ts index cf0638d..f1ae357 100644 --- a/src/core/InterceptorManager.ts +++ b/src/core/InterceptorManager.ts @@ -13,7 +13,10 @@ export class InterceptorManager { * @param fulfilled * @param rejected */ - public use(fulfilled: Function | null, rejected: Function | null): number { + public use( + fulfilled: Function | null, + rejected: Function | null = null + ): number { this.$handlers.push({ fulfilled, rejected, diff --git a/src/interceptors/beforeSubmission.ts b/src/interceptors/beforeSubmission.ts index 546ffa1..991a9e1 100644 --- a/src/interceptors/beforeSubmission.ts +++ b/src/interceptors/beforeSubmission.ts @@ -8,7 +8,7 @@ import { InterceptorHandler } from '../types' export const validateForm: InterceptorHandler = { fulfilled: (form: Form): Promise => { if (form.$options.validation.onSubmission && !form.validate()) { - return Promise.reject({ error: { message: 'Form is invalid.' }, form }) + return Promise.reject({ message: 'Form is invalid.' }) } return Promise.resolve(form) diff --git a/tests/core/Form/Form.interceptors.spec.ts b/tests/core/Form/Form.interceptors.spec.ts new file mode 100644 index 0000000..c8d21ba --- /dev/null +++ b/tests/core/Form/Form.interceptors.spec.ts @@ -0,0 +1,100 @@ +import { Form } from '../../../src/core/Form' + +jest.mock('../../../src/core/Errors') +jest.mock('../../../src/core/Validator') +jest.mock('../../../src/core/Touched') + +describe('Form.interceptors.ts', () => { + it('should add new interceptor to the end of the chain when using submissionComplete interceptorManager', async () => { + let form = new Form({}) + + const fulfilledFunc = jest.fn() + const rejectedFunc = jest.fn() + + form.$interceptors.submissionComplete.use(fulfilledFunc, rejectedFunc) + + let callback = jest.fn(() => Promise.resolve('yay!')) + + await form.submit(callback) + + expect(fulfilledFunc).toHaveBeenCalledTimes(1) + expect(fulfilledFunc).toHaveBeenLastCalledWith({ response: 'yay!', form }) + expect(fulfilledFunc).toHaveBeenCalledAfter(callback) + expect(rejectedFunc).toHaveBeenCalledTimes(0) + + fulfilledFunc.mockClear() + rejectedFunc.mockClear() + callback.mockClear() + + callback = jest.fn(() => Promise.reject('Oh...')) + + await form.submit(callback) + + expect(rejectedFunc).toHaveBeenCalledTimes(1) + expect(rejectedFunc).toHaveBeenLastCalledWith({ error: 'Oh...', form }) + expect(rejectedFunc).toHaveBeenCalledAfter(callback) + expect(fulfilledFunc).toHaveBeenCalledTimes(0) + }) + + it('should add new interceptor to the begging of the chain when using beforeSubmission interceptorManager', async () => { + let form = new Form({}) + + expect.assertions(8) + + const fulfilledFunc = jest.fn(form => form) + const rejectedFunc = jest.fn(error => Promise.reject(error)) + + form.$interceptors.beforeSubmission.use(fulfilledFunc, rejectedFunc) + + let callback = jest.fn(() => Promise.resolve('yay!')) + + await form.submit(callback) + + expect(fulfilledFunc).toHaveBeenCalledTimes(1) + expect(fulfilledFunc).toHaveBeenLastCalledWith(form) + expect(fulfilledFunc).toHaveBeenCalledBefore(callback) + expect(rejectedFunc).toHaveBeenCalledTimes(0) + + fulfilledFunc.mockClear() + rejectedFunc.mockClear() + callback.mockClear() + + form.$interceptors.beforeSubmission.use(() => Promise.reject('Error!')) + + try { + await form.submit(callback) + } catch (e) { + expect(rejectedFunc).toHaveBeenCalledTimes(1) + expect(rejectedFunc).toHaveBeenLastCalledWith('Error!') + expect(callback).toHaveBeenCalledTimes(0) + expect(fulfilledFunc).toHaveBeenCalledTimes(0) + } + }) + + it('should not call the interceptor if the user eject it', async () => { + let form = new Form({}) + + expect.assertions(4) + + const fulfilledFunc = jest.fn() + const rejectedFunc = jest.fn() + + const interceptorIndex = form.$interceptors.submissionComplete.use( + fulfilledFunc, + rejectedFunc + ) + form.$interceptors.submissionComplete.eject(interceptorIndex) + + await form.submit(() => Promise.resolve()) + + expect(fulfilledFunc).toHaveBeenCalledTimes(0) + expect(rejectedFunc).toHaveBeenCalledTimes(0) + + try { + await form.submit(() => Promise.reject()) + } catch (e) { + expect(fulfilledFunc).toHaveBeenCalledTimes(0) + expect(rejectedFunc).toHaveBeenCalledTimes(0) + } + }) +}) diff --git a/tsconfig.json b/tsconfig.json index a3350b5..ed9a5b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,11 @@ "module": "es2015", "target": "es2015", "declaration": true, - "esModuleInterop": true + "esModuleInterop": true, + "types": [ + "jest", + "jest-extended" + ] }, "include": [ "src/**/*" From ee35dfb7b396f6fe85b03eec109b996fbf3dba39 Mon Sep 17 00:00:00 2001 From: Nevo Date: Sat, 15 Dec 2018 15:47:22 +0200 Subject: [PATCH 05/10] changing the defaultOptions to default.options and also adding the default for interceptors without usage for now --- src/core/Form.ts | 20 +++++++++++++------- src/types.ts | 9 +++++++++ tests/core/Form/Form.spec.ts | 6 +++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/core/Form.ts b/src/core/Form.ts index d2810de..6d70ca0 100644 --- a/src/core/Form.ts +++ b/src/core/Form.ts @@ -5,10 +5,11 @@ import { InterceptorManager } from './InterceptorManager' import { isObject } from '../utils' import generateDefaultLabel from '../helpers/generateDefaultLabel' import generateOptions from '../helpers/generateOptions' -import defaultsOptions from '../default-options' +import defaultOptions from '../default-options' import basicInterceptors from '../interceptors/index' import { Field, + FormDefaults, InterceptorHandler, InterceptorManagersObject, Options, @@ -17,10 +18,15 @@ import { export class Form { /** - * Defaults options for the Form instance + * holds all the defaults for the forms */ - public static defaultOptions: Options = defaultsOptions - + public static defaults: FormDefaults = { + options: defaultOptions, + interceptors: { + beforeSubmission: new InterceptorManager(), + submissionComplete: new InterceptorManager(), + }, + } /** * determine if the form is on submitting mode */ @@ -65,7 +71,7 @@ export class Form { /** * Options of the Form */ - public $options: Options = Form.defaultOptions + public $options: Options = Form.defaults.options /** * holds the interceptor managers @@ -86,12 +92,12 @@ export class Form { /** * setting up default options for the Form class in more - * convenient way then "Form.defaultOptions.validation.something = something" + * convenient way then "Form.defaults.options.validation.something = something" * * @param options */ public static assignDefaultOptions(options: Options = {}): void { - Form.defaultOptions = generateOptions(Form.defaultOptions, options) + Form.defaults.options = generateOptions(Form.defaults.options, options) } /** diff --git a/src/types.ts b/src/types.ts index 817ef50..e6faae2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,6 +130,15 @@ export interface InterceptorManagersObject { [key: string]: InterceptorManager } +/** + * The defaults of the form, + * that can be changeable and then will affect on all the new Form instances + */ +export interface FormDefaults { + options: Options + interceptors: InterceptorManagersObject +} + /** * Submit callback interface, * the function the should pass to submit method in Form class diff --git a/tests/core/Form/Form.spec.ts b/tests/core/Form/Form.spec.ts index 8d3cd4b..ccbc053 100644 --- a/tests/core/Form/Form.spec.ts +++ b/tests/core/Form/Form.spec.ts @@ -227,10 +227,10 @@ describe('Form.ts', () => { }) it('should change the defaultOptions options of the Form', () => { - Form.defaultOptions.validation.defaultMessage = ({ label, value }) => + Form.defaults.options.validation.defaultMessage = ({ label, value }) => `${label}: ${value}` - Form.defaultOptions.successfulSubmission.clearErrors = false - Form.defaultOptions.successfulSubmission.resetValues = false + Form.defaults.options.successfulSubmission.clearErrors = false + Form.defaults.options.successfulSubmission.resetValues = false let form = new Form(data) From b97ecea5c79b1b1259e3d2352d6a5262ebf8eca8 Mon Sep 17 00:00:00 2001 From: Nevo Date: Sat, 15 Dec 2018 17:50:02 +0200 Subject: [PATCH 06/10] Adding the default interceptors option. now user can set default interceptors that will intercept submission in all the new instance of Form --- src/core/Form.ts | 8 ++++-- src/core/InterceptorManager.ts | 16 ++++++++++++ tests/core/Form/Form.interceptors.spec.ts | 31 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/core/Form.ts b/src/core/Form.ts index 6d70ca0..bf5939b 100644 --- a/src/core/Form.ts +++ b/src/core/Form.ts @@ -382,8 +382,12 @@ export class Form { this.$errors = new Errors() this.$touched = new Touched() this.$interceptors = { - beforeSubmission: new InterceptorManager(), - submissionComplete: new InterceptorManager(), + beforeSubmission: new InterceptorManager( + Form.defaults.interceptors.beforeSubmission.all() + ), + submissionComplete: new InterceptorManager( + Form.defaults.interceptors.submissionComplete.all() + ), } return this diff --git a/src/core/InterceptorManager.ts b/src/core/InterceptorManager.ts index f1ae357..b41b5dc 100644 --- a/src/core/InterceptorManager.ts +++ b/src/core/InterceptorManager.ts @@ -6,6 +6,15 @@ export class InterceptorManager { */ $handlers: InterceptorHandler[] = [] + /** + * constructor + * + * @param handlers + */ + constructor(handlers: InterceptorHandler[] = []) { + this.merge(handlers) + } + /** * adding function to the handlers chain * and returns the position of the handler in the chain @@ -48,6 +57,13 @@ export class InterceptorManager { return this } + /** + * return all the handlers + */ + public all(): InterceptorHandler[] { + return this.$handlers + } + /** * run over the handlers * diff --git a/tests/core/Form/Form.interceptors.spec.ts b/tests/core/Form/Form.interceptors.spec.ts index c8d21ba..5a66a71 100644 --- a/tests/core/Form/Form.interceptors.spec.ts +++ b/tests/core/Form/Form.interceptors.spec.ts @@ -97,4 +97,35 @@ describe('Form.interceptors.ts', () => { expect(rejectedFunc).toHaveBeenCalledTimes(0) } }) + + it('should merge the defaults interceptors into the interceptors array', () => { + const beforeSubmissionFulfilledFunc = jest.fn() + const beforeSubmissionRejectedFunc = jest.fn() + const submissionCompleteFulfilledFunc = jest.fn() + const submissionCompleteRejectedFunc = jest.fn() + + Form.defaults.interceptors.beforeSubmission.use( + beforeSubmissionFulfilledFunc, + beforeSubmissionRejectedFunc + ) + Form.defaults.interceptors.submissionComplete.use( + submissionCompleteFulfilledFunc, + submissionCompleteRejectedFunc + ) + + let form = new Form({}) + + expect(form.$interceptors.beforeSubmission.$handlers[0].fulfilled).toBe( + beforeSubmissionFulfilledFunc + ) + expect(form.$interceptors.beforeSubmission.$handlers[0].rejected).toBe( + beforeSubmissionRejectedFunc + ) + expect(form.$interceptors.submissionComplete.$handlers[0].fulfilled).toBe( + submissionCompleteFulfilledFunc + ) + expect(form.$interceptors.submissionComplete.$handlers[0].rejected).toBe( + submissionCompleteRejectedFunc + ) + }) }) From 69c8aa56733ef5a75840374ef938ab16678095db Mon Sep 17 00:00:00 2001 From: Nevo Date: Sat, 15 Dec 2018 18:13:02 +0200 Subject: [PATCH 07/10] adding codecov to the repo --- .circleci/config.yml | 6 +++++- README.md | 1 + package-lock.json | 36 +++++++++++++++++++++++++++++++++++- package.json | 2 ++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f087966..a37d869 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,6 +27,10 @@ jobs: name: Run tests command: npm test + - run: + name: Uplodad code coverage + command: npm run codecov + - persist_to_workspace: root: ~/repo paths: . @@ -60,4 +64,4 @@ workflows: tags: only: /^v.*/ branches: - ignore: /.*/ \ No newline at end of file + ignore: /.*/ diff --git a/README.md b/README.md index 8a76a15..86ed872 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Form Wrapper JS [![npm](https://img.shields.io/npm/v/form-wrapper-js.svg?style=shield)](https://www.npmjs.com/package/form-wrapper-js) +[![codecov](https://codecov.io/gh/Nevoss/form-wrapper-js/branch/master/graph/badge.svg)](https://codecov.io/gh/Nevoss/form-wrapper-js) [![CircleCI](https://circleci.com/gh/Nevoss/form-wrapper-js.svg?style=shield)](https://circleci.com/gh/Nevoss/form-wrapper-js) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=shield)](https://github.com/prettier/prettier) diff --git a/package-lock.json b/package-lock.json index 5dc7975..357e624 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "form-wrapper-js", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -423,6 +423,12 @@ "sprintf-js": "~1.0.2" } }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, "arr-diff": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", @@ -1033,6 +1039,19 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "codecov": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.1.0.tgz", + "integrity": "sha512-aWQc/rtHbcWEQLka6WmBAOpV58J2TwyXqlpAQGhQaSiEUoigTTUk6lLd2vB3kXkhnDyzyH74RXfmV4dq2txmdA==", + "dev": true, + "requires": { + "argv": "^0.0.2", + "ignore-walk": "^3.0.1", + "js-yaml": "^3.12.0", + "request": "^2.87.0", + "urlgrey": "^0.4.4" + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -2418,6 +2437,15 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "import-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", @@ -5442,6 +5470,12 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index a4e4047..05497e0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test": "jest", "build": "rollup -c", "watch": "rollup -cw", + "codecov": "codecov", "prepublishOnly": "npm run build" }, "repository": { @@ -34,6 +35,7 @@ "license": "MIT", "devDependencies": { "@types/jest": "^23.3.9", + "codecov": "^3.1.0", "jest": "^23.6.0", "prettier": "1.15.3", "rollup": "^0.67.4", From e458f88279e271b94ee12ecf4957625cd23948a0 Mon Sep 17 00:00:00 2001 From: Nevo Golan Date: Mon, 17 Dec 2018 23:52:24 +0200 Subject: [PATCH 08/10] Updating README.md (#23) Rewrite the usage section and set up some icons. --- README.md | 364 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 300 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 86ed872..b21ba4b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ -# Form Wrapper JS +# :pencil: Form wrapper JS +> A lightweight library that creates forms systems in a convenient and easy way, without dependencies and magic code. + [![npm](https://img.shields.io/npm/v/form-wrapper-js.svg?style=shield)](https://www.npmjs.com/package/form-wrapper-js) +![MIT](https://img.shields.io/github/license/Nevoss/form-wrapper-js.svg) [![codecov](https://codecov.io/gh/Nevoss/form-wrapper-js/branch/master/graph/badge.svg)](https://codecov.io/gh/Nevoss/form-wrapper-js) [![CircleCI](https://circleci.com/gh/Nevoss/form-wrapper-js.svg?style=shield)](https://circleci.com/gh/Nevoss/form-wrapper-js) -[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=shield)](https://github.com/prettier/prettier) - -A very light tool to create forms systems in a convenient and easy way, without dependencies and magic code. ---- -**this library is letting you as a developer, 😎 to create a powerful form systems by choosing how to build it (GUI, validation, submission etc.)** - ---- +## :art: Playgorund +- Vue - https://codesandbox.io/s/5x96q83yvp?module=%2Fsrc%2FApp.vue -## Installation +## :cd: Installation ``` npm install --save form-wrapper-js ``` @@ -21,76 +19,314 @@ npm install --save form-wrapper-js yarn add form-wrapper-js ``` -## Documentation -coming soon.. - -## Basic Usage -Basic examples in vue for now (react is coming soon...) - -### VueJS -you can play around with basic example [Here](https://codesandbox.io/s/5x96q83yvp?module=%2Fsrc%2FApp.vue) - -or just take a look at this code: -```html - - - +``` + +### Submitting the form + +The library wrapping up some form logic in `form.submit()` method, so just pass a callback that returns a `Promise` + +By default before any submission, it validates the form (as describe bellow) and set `form.$submitting` to true. after submission complete, it set `form.$submitting` to false and clear the form values, `$errors` and `$touched` array. + +all those default behaviors can be changed by the `form.$options` object. + +```vue + + + +``` - export default { - data() { +### Validating the form + +every value can get some validation rules that by default will be validated before submission, of course, you can customize it and validate on input or on blur events (again, more information below). + +```vue + + } + +``` +the `rules` file will look like this +```js +import validationLibrary from 'example-validation-library' + +// Example for validation rule as a function (will take default error message) + +export const required = ({ value }) => value !== null || value !== '' + + +// Example for validation rule as an object, alongside with message property ("message" can be also a string). + +export const email = { + passes: ({ value }) => validationLibrary.isEmail(value), // use any library you want to validate, or just use regex + message: ({ label, value }) => `${label} must be an email, ${value} is not an email.`, +} ``` -## Contribute -Evey body is welcome, you can contribute some Code, Docs, bug reports, ideas and even design. +this behavior letting you create your validation file (or files) that can be reusable and also very lightweight, without a lot of validation rules you don`t need. + +### Handling with form errors + +after validating form or a specific field, there is a case that some fields not pass some rules. you can use `form.$errors` API to retrieve those field errors. + +```vue + +``` + + ### Options + +the library tries to be as flexible has it can, so there are some options that letting you customize the behavior of your forms, you can set those options in 3 main ways + ```js +import { Form } from 'form-wrapper-js' + +// assign some options after the form instantiate +let form = new Form({ name: null }) +form.assignOptions({ + validation: { + onSubmission: false, + } +}) +// or +form.$options.successfulSubmission.resetValues = true + +// in the form construction +let form = new Form({ name: null }, { + validation: { + onFieldBlurred: false, + onFieldChanged: true, + } +}) + +// setting defaults that will apply on new Form instances. +Form.defaults.options.successfulSubmission.clearTouched = false +// or +Form.assignDefaultOptions({ + validation: { + unsetFieldErrorsOnFieldChange: true, + stopAfterFirstRuleFailed: true, + } +}) +``` + +Those are the default options +```js +{ + successfulSubmission: { + clearErrors: true, // clear errors after successful submission + clearTouched: true, // clear $touched array after successful submission + resetValues: true, // after successful submission, it set the values as the initial values that were provided at the construction after successful submission + }, + validation: { + onFieldBlurred: false, // validate on field blurred + onFieldChanged: false, // validate on change or input event + onSubmission: true, // validate on submission + unsetFieldErrorsOnFieldChange: false, // on change/input event, the errors of the targeted field will be removed + stopAfterFirstRuleFailed: true, // if the first validation of a specific field will fail it will stop to validate this specific field + defaultMessage: ({ label }) => `${label} is invalid.`, // default message, if an error message was not provided. + }, +} +``` + +### More complex form handling + +Sometimes you need more from your form, which input is on focus? which is dirty? or which is touched? you want to handle the labels inside the form and to set some extra data to a field that can be managed within the form instance. (e.g. select options and more...) + +to be able to use those features you need to bind some events to the input DOM element. +```vue + +``` + +There is not necessarily need to bind all those events to the input, try them out to know which of them you need and which not. + +some options will not work if there is no event binding e.g. +- `form.$options.validation.onFieldBlurred` - must have `fieldBlurred` event +- `form.$options.validation.onFieldChange` - must have `fieldChanged` event (on input/change DOM events) +- `form.$onFocus` - will not work if `fieldFocused` event will not bond to the input DOM element. +- etc.., + +if you are not sure which of them you need, just bind all of them. (nothing bad will happened) + +as you build your own form system you will see that some pattern repeat them self, be creative as you can and encapsulate those pattern inside components. + + +### Interceptors + +this concept is taking over from the axios API. +```js +import Form from 'form-wrapper-js' + +let form = new Form({name: null}) +form.$interceptors.submissionComplete.use( + ({ form, response }) => { + // this function will run every time submission SUCCESSFULLY completed. + return { form, response } + }, + ({ form, error }) => { + // this function will run every REJECTED submission + return Promise.reject({ form, error }) + } +) + +form.$interceptors.beforeSubmission.use((form) => { + // set some stuff before submission + return form +}) +``` + +you can even set some default interceptors, that all the new Form instances will automatically use those interceptors +```js +import Form from 'form-wrapper-js' + +Form.default.interceptors.submissionComplete.use(null, ({ error, form }) => { + if (error.response && error.response.status === 422) { + // backend endpoint that return errors the form can "record" those error + form.$errors.record(error.response.errors) + } + + return Promise.reject({ error, form }) +}) +``` + +### Extra + +so there are some things that were no covered throw this "basic-usage" guide, soon I will write a good documentation that will cover all features. + +here are some basic methods and props that can be useful: +```js +import { Form } from 'form-wrapper-js' + +let form = new Form({name: null}) + +form.isDirty('name') // returns if the field is dirty (the value of 'name' different from the initial value) +form.isDirty() // if only one of the fields is dirty will return `true` + +form.$onFocus // which field is on focus at the current time + +form.$touched.has('name') // if field is touched returns true. +form.$touched.any() // returns true if some field is touched. + +form.fill({ name: 'some name' }) // will fill the values based on the object that provided +form.values() // returns an object with all the fields values + +form.validate('name') // will validate only the name field +form.validate() // will validate the whole form + +form.$labels.name // will return the label of the field (eg: "Name") + +form.$errors.has('name') // checks if `name` field has errors +form.$errors.get('name') // gets errors array of `name` field +form.$errors.getFirst('name') // gets the first error from errors array of `name` field + +``` +--- +**And please if something is not clear enough, try to dig inside the code to understand it better, I was trying to make a very clear code and clear comments, and if something not clear enough please let me know** + +--- + +## :beers: Contribute +Everybody is welcome, you can contribute some Code, Docs, bug reports and even ideas. it is very easy to install the project just take a look at CONTRIBUTING.md and follow the instructions. -**The project is still on develop so ideas for features is more than welcome** ⭐ +**The project is still on development, so ideas for features are then welcome** ⭐ -## License +## :lock: License The MIT License (MIT). Please see License File for more information. From 588c093f4da4336e3bce75fb66a5dc63600e0f1a Mon Sep 17 00:00:00 2001 From: Nevo Golan Date: Tue, 18 Dec 2018 22:38:16 +0200 Subject: [PATCH 09/10] adding warn function (#25) warn in some methods about invalid fields. also, add some tests. --- jest.config.js | 1 + src/core/Form.ts | 19 ++- src/core/Touched.ts | 2 +- src/core/Validator.ts | 52 +++---- src/utils.ts | 9 ++ tests/core/Form/Form.events.spec.ts | 119 ++++++++++++++++ tests/core/Form/Form.spec.ts | 174 ++---------------------- tests/core/Form/Form.validation.spec.ts | 96 +++++++++++++ tests/core/Touched.spec.ts | 4 + tests/core/Validator.spec.ts | 11 +- tests/jest.setup.ts | 2 + tests/utils.spec.ts | 11 +- 12 files changed, 303 insertions(+), 197 deletions(-) create mode 100644 tests/core/Form/Form.events.spec.ts create mode 100644 tests/core/Form/Form.validation.spec.ts create mode 100644 tests/jest.setup.ts diff --git a/jest.config.js b/jest.config.js index 6f8abb4..8a231b6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,5 @@ module.exports = { collectCoverage: true, coverageDirectory: './coverage', setupTestFrameworkScriptFile: 'jest-extended', + setupFiles: ['/tests/jest.setup.ts'], } diff --git a/src/core/Form.ts b/src/core/Form.ts index bf5939b..7f02611 100644 --- a/src/core/Form.ts +++ b/src/core/Form.ts @@ -2,7 +2,7 @@ import { Errors } from './Errors' import { Validator } from './Validator' import { Touched } from './Touched' import { InterceptorManager } from './InterceptorManager' -import { isObject } from '../utils' +import { isObject, warn } from '../utils' import generateDefaultLabel from '../helpers/generateDefaultLabel' import generateOptions from '../helpers/generateOptions' import defaultOptions from '../default-options' @@ -167,10 +167,7 @@ export class Form { */ public fill(newData: Object): Form { for (let fieldName in newData) { - if ( - newData.hasOwnProperty(fieldName) && - this.$initialValues.hasOwnProperty(fieldName) - ) { + if (newData.hasOwnProperty(fieldName) && this.hasField(fieldName)) { this[fieldName] = newData[fieldName] } } @@ -194,6 +191,8 @@ export class Form { */ public validateField(fieldKey: string): boolean { if (!this.hasField(fieldKey)) { + warn(`\`${fieldKey}\` is not a valid field`) + return true } @@ -257,6 +256,8 @@ export class Form { */ public isFieldDirty(fieldKey: string): boolean { if (!this.hasField(fieldKey)) { + warn(`\`${fieldKey}\` is not a valid field`) + return false } @@ -270,6 +271,8 @@ export class Form { */ public fieldChanged(fieldKey: string): Form { if (!this.hasField(fieldKey)) { + warn(`\`${fieldKey}\` is not a valid field`) + return this } @@ -287,11 +290,15 @@ export class Form { */ public fieldFocused(fieldKey: string): Form { if (!this.hasField(fieldKey)) { + warn(`\`${fieldKey}\` is not a valid field`) + return this } this.$touched.push(fieldKey) this.$onFocus = fieldKey + + return this } /** @@ -301,6 +308,8 @@ export class Form { */ public fieldBlurred(fieldKey: string): Form { if (!this.hasField(fieldKey)) { + warn(`\`${fieldKey}\` is not a valid field`) + return this } diff --git a/src/core/Touched.ts b/src/core/Touched.ts index aa11148..9a02645 100644 --- a/src/core/Touched.ts +++ b/src/core/Touched.ts @@ -16,7 +16,7 @@ export class Touched { * * @param fieldsKeys */ - public record(fieldsKeys: string[] = []): Touched { + public record(fieldsKeys: string[]): Touched { this.$touched = [...fieldsKeys] return this diff --git a/src/core/Validator.ts b/src/core/Validator.ts index dd987ee..df6acc9 100644 --- a/src/core/Validator.ts +++ b/src/core/Validator.ts @@ -28,32 +28,6 @@ export class Validator { this.buildRules(rules) } - /** - * building rules object - * - * @param rules - */ - private buildRules(rules: Object): Validator { - Object.keys(rules).forEach(key => { - this.$rules[key] = rules[key].map(rule => { - let passes = rule - let message = this.$options.defaultMessage - - if (isObject(rule)) { - passes = rule.passes - message = rule.message - } - - return { - passes, - message: typeof message === 'function' ? message : () => message, - } - }) - }) - - return this - } - /** * check if field has rules * @@ -101,4 +75,30 @@ export class Validator { return messages } + + /** + * building rules object + * + * @param rules + */ + private buildRules(rules: Object): Validator { + Object.keys(rules).forEach(key => { + this.$rules[key] = rules[key].map(rule => { + let passes = rule + let message = this.$options.defaultMessage + + if (isObject(rule)) { + passes = rule.passes + message = rule.message + } + + return { + passes, + message: typeof message === 'function' ? message : () => message, + } + }) + }) + + return this + } } diff --git a/src/utils.ts b/src/utils.ts index 9b92b8a..356cc49 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,3 +6,12 @@ export const isObject = (value: any): boolean => { return value !== null && typeof value === 'object' && !Array.isArray(value) } + +/** + * sending a warning message + * + * @param message + */ +export const warn = (message): void => { + console.error(`[Form-wrapper-js warn]: ${message}`) +} diff --git a/tests/core/Form/Form.events.spec.ts b/tests/core/Form/Form.events.spec.ts new file mode 100644 index 0000000..d43e652 --- /dev/null +++ b/tests/core/Form/Form.events.spec.ts @@ -0,0 +1,119 @@ +import { Form } from '../../../src/core/Form' +import * as utils from '../../../src/utils' + +jest.mock('../../../src/core/Errors') +jest.mock('../../../src/core/Validator') +jest.mock('../../../src/core/Touched') + +describe('Form.events.ts', () => { + let data = { + first_name: null, + last_name: null, + is_developer: false, + } + + it('should validate field that was change if the "validation.onFieldChanged" set as true', () => { + let form = new Form(data, { + validation: { + onFieldChanged: true, + }, + }) + + form.validateField = jest.fn() + + form.fieldChanged('first_name') + + expect(form.validateField).toHaveBeenCalledTimes(1) + expect(form.validateField).toHaveBeenCalledWith('first_name') + + form.assignOptions({ + validation: { + onFieldChanged: false, + }, + }) + + form.fieldChanged('first_name') + + expect(form.validateField).toHaveBeenCalledTimes(1) + }) + + it('should clear field errors after field changed', () => { + let form = new Form(data, { + validation: { + unsetFieldErrorsOnFieldChange: false, + }, + }) + + form.fieldChanged('first_name') + + expect(form.$errors.unset).toHaveBeenCalledTimes(0) + + form.assignOptions({ + validation: { + unsetFieldErrorsOnFieldChange: true, + }, + }) + + form.fieldChanged('first_name') + + expect(form.$errors.unset).toHaveBeenCalledTimes(1) + expect(form.$errors.unset).toHaveBeenCalledWith('first_name') + }) + + it('should push to touched and set $onFocus when field is on focus', () => { + let form = new Form(data) + + form.fieldFocused('first_name') + + expect(form.$onFocus).toBe('first_name') + expect(form.$touched.push).toHaveBeenCalledTimes(1) + expect(form.$touched.push).toHaveBeenCalledWith('first_name') + }) + + it('should reset $onFocus if the field is on focus, and validate the field if "validation.onFieldBlurred" is set', () => { + let form = new Form(data, { + validation: { + onFieldBlurred: false, + }, + }) + + form.validateField = jest.fn() + form.$onFocus = 'first_name' + + form.fieldBlurred('first_name') + + expect(form.$onFocus).toBe(null) + expect(form.validateField).toHaveBeenCalledTimes(0) + + form.assignOptions({ + validation: { + onFieldBlurred: true, + }, + }) + form.$onFocus = 'last_name' + form.fieldBlurred('first_name') + + expect(form.$onFocus).toBe('last_name') + expect(form.validateField).toHaveBeenCalledTimes(1) + expect(form.validateField).toHaveBeenCalledWith('first_name') + }) + + it('should warn if field not exists in fieldBlurred, fieldChanged and fieldFocused methods', () => { + let warnMock = jest.spyOn(utils, 'warn') + + let form = new Form(data) + + form.fieldChanged('some_field_1') + expect(warnMock).toHaveBeenCalledTimes(1) + + warnMock.mockClear() + + form.fieldBlurred('some_field_2') + expect(warnMock).toHaveBeenCalledTimes(1) + + warnMock.mockClear() + + form.fieldFocused('some_field_3') + expect(warnMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/tests/core/Form/Form.spec.ts b/tests/core/Form/Form.spec.ts index ccbc053..8528e42 100644 --- a/tests/core/Form/Form.spec.ts +++ b/tests/core/Form/Form.spec.ts @@ -5,6 +5,7 @@ import { Form } from '../../../src/core/Form' import generateOptions from '../../../src/helpers/generateOptions' import defaultOptionsSource from '../../../src/default-options' import { InterceptorManager } from '../../../src/core/InterceptorManager' +import * as utils from '../../../src/utils' jest.mock('../../../src/core/Errors') jest.mock('../../../src/core/Validator') @@ -148,84 +149,6 @@ describe('Form.ts', () => { ) }) - it('should call to validate specific field or all the fields', () => { - let form = new Form(data) as Form & FormData - - form.validateAll = jest.fn() - form.validateField = jest.fn() - - form.validate() - - expect(form.validateAll).toHaveBeenCalledTimes(1) - expect(form.validateField).not.toHaveBeenCalled() - - form.validate('first_name') - - expect(form.validateAll).toHaveBeenCalledTimes(1) - expect(form.validateField).toHaveBeenCalledTimes(1) - }) - - it('should validate specific field', () => { - let form = new Form({ - name: { - value: 'a', - label: 'The Name', - rules: [() => true], - }, - }) as Form & { name: string } - - form.$validator.validateField = jest.fn(() => ['error']) - - let isValid = form.validateField('name') - - expect(isValid).toBe(false) - expect(form.$errors.unset).toHaveBeenCalledTimes(1) - expect(form.$errors.unset).toBeCalledWith('name') - expect(form.$errors.push).toHaveBeenCalledTimes(1) - expect(form.$errors.push).toBeCalledWith({ - name: ['error'], - }) - expect(form.$validator.validateField).toBeCalledWith( - { label: 'The Name', value: 'a', key: 'name' }, - form - ) - - form.$validator.validateField = jest.fn(() => []) - - isValid = form.validateField('name') - expect(isValid).toBe(true) - expect(form.$errors.push).toHaveBeenCalledTimes(1) - expect(form.$errors.unset).toHaveBeenCalledTimes(2) - }) - - it('should validate all the fields of the form', () => { - let form = new Form({ - name: { - value: null, - rules: [() => true], - }, - last_name: { - value: null, - rules: [() => false], - }, - }) - - form.validateField = jest - .fn() - .mockReturnValueOnce(true) - .mockReturnValueOnce(true) - - expect(form.validateAll()).toBe(true) - expect(form.validateField).toHaveBeenNthCalledWith(1, 'name') - expect(form.validateField).toHaveBeenNthCalledWith(2, 'last_name') - - form.validateField = jest - .fn() - .mockReturnValueOnce(true) - .mockReturnValueOnce(false) - expect(form.validateAll()).toBe(false) - }) - it('should change the defaultOptions options of the Form', () => { Form.defaults.options.validation.defaultMessage = ({ label, value }) => `${label}: ${value}` @@ -276,6 +199,16 @@ describe('Form.ts', () => { expect(form.isFieldDirty('last_name')).toBe(false) }) + it('should warn if field key that passed to isDirtyField is not exists', () => { + let warnMock = jest.spyOn(utils, 'warn') + + let form = new Form({ name: null }) + + form.isFieldDirty('some_key') + + expect(warnMock).toHaveBeenCalledTimes(1) + }) + it('should run isFieldDirty (argument passes to "isDirty")', () => { let form = new Form(data) as Form & FormData @@ -296,91 +229,6 @@ describe('Form.ts', () => { expect(form.isDirty()).toBe(true) }) - it('should validate field that was change if the "validation.onFieldChanged" set as true', () => { - let form = new Form(data, { - validation: { - onFieldChanged: true, - }, - }) - - form.validateField = jest.fn() - - form.fieldChanged('first_name') - - expect(form.validateField).toHaveBeenCalledTimes(1) - expect(form.validateField).toHaveBeenCalledWith('first_name') - - form.assignOptions({ - validation: { - onFieldChanged: false, - }, - }) - - form.fieldChanged('first_name') - - expect(form.validateField).toHaveBeenCalledTimes(1) - }) - - it('should clear field errors after field changed', () => { - let form = new Form(data, { - validation: { - unsetFieldErrorsOnFieldChange: false, - }, - }) - - form.fieldChanged('first_name') - - expect(form.$errors.unset).toHaveBeenCalledTimes(0) - - form.assignOptions({ - validation: { - unsetFieldErrorsOnFieldChange: true, - }, - }) - - form.fieldChanged('first_name') - - expect(form.$errors.unset).toHaveBeenCalledTimes(1) - expect(form.$errors.unset).toHaveBeenCalledWith('first_name') - }) - - it('should push to touched and set $onFocus when field is on focus', () => { - let form = new Form(data) - - form.fieldFocused('first_name') - - expect(form.$onFocus).toBe('first_name') - expect(form.$touched.push).toHaveBeenCalledTimes(1) - expect(form.$touched.push).toHaveBeenCalledWith('first_name') - }) - - it('should resetValues $onFocus if the field is on focus and validate the field if "validation.onFieldBlurred" is set', () => { - let form = new Form(data, { - validation: { - onFieldBlurred: false, - }, - }) - - form.validateField = jest.fn() - form.$onFocus = 'first_name' - form.fieldBlurred('first_name') - - expect(form.$onFocus).toBe(null) - expect(form.validateField).toHaveBeenCalledTimes(0) - - form.assignOptions({ - validation: { - onFieldBlurred: true, - }, - }) - form.$onFocus = 'last_name' - form.fieldBlurred('first_name') - - expect(form.$onFocus).toBe('last_name') - expect(form.validateField).toHaveBeenCalledTimes(1) - expect(form.validateField).toHaveBeenCalledWith('first_name') - }) - it('should reset all the form state', () => { let form = new Form(data) diff --git a/tests/core/Form/Form.validation.spec.ts b/tests/core/Form/Form.validation.spec.ts new file mode 100644 index 0000000..44372b0 --- /dev/null +++ b/tests/core/Form/Form.validation.spec.ts @@ -0,0 +1,96 @@ +import { Form } from '../../../src/core/Form' +import * as utils from '../../../src/utils' + +jest.mock('../../../src/core/Errors') +jest.mock('../../../src/core/Validator') +jest.mock('../../../src/core/Touched') + +describe('Form.validation.ts', () => { + it('should validate specific field', () => { + let form = new Form({ + name: { + value: 'a', + label: 'The Name', + rules: [() => true], + }, + }) + + form.$validator.validateField = jest.fn(() => ['error']) + + let isValid = form.validateField('name') + + expect(isValid).toBe(false) + expect(form.$errors.unset).toHaveBeenCalledTimes(1) + expect(form.$errors.unset).toBeCalledWith('name') + expect(form.$errors.push).toHaveBeenCalledTimes(1) + expect(form.$errors.push).toBeCalledWith({ + name: ['error'], + }) + expect(form.$validator.validateField).toBeCalledWith( + { label: 'The Name', value: 'a', key: 'name' }, + form + ) + + form.$validator.validateField = jest.fn(() => []) + + isValid = form.validateField('name') + expect(isValid).toBe(true) + expect(form.$errors.push).toHaveBeenCalledTimes(1) + expect(form.$errors.unset).toHaveBeenCalledTimes(2) + }) + + it('should warn if trying to validate field and the field is not exists', () => { + let warnMock = jest.spyOn(utils, 'warn') + + let form = new Form({ name: null }) + + form.validateField('first_name') + + expect(warnMock).toHaveBeenCalledTimes(1) + }) + + it('should validate all the fields of the form', () => { + let form = new Form({ + name: { + value: null, + rules: [() => true], + }, + last_name: { + value: null, + rules: [() => false], + }, + }) + + form.validateField = jest + .fn() + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + + expect(form.validateAll()).toBe(true) + expect(form.validateField).toHaveBeenNthCalledWith(1, 'name') + expect(form.validateField).toHaveBeenNthCalledWith(2, 'last_name') + + form.validateField = jest + .fn() + .mockReturnValueOnce(true) + .mockReturnValueOnce(false) + expect(form.validateAll()).toBe(false) + }) + + it('should call to validate specific field or all the fields', () => { + let form = new Form({ first_name: null }) + + form.validateAll = jest.fn() + form.validateField = jest.fn() + + form.validate() + + expect(form.validateAll).toHaveBeenCalledTimes(1) + expect(form.validateField).not.toHaveBeenCalled() + + form.validate('first_name') + + expect(form.validateAll).toHaveBeenCalledTimes(1) + expect(form.validateField).toHaveBeenCalledTimes(1) + }) +}) diff --git a/tests/core/Touched.spec.ts b/tests/core/Touched.spec.ts index 649ed2f..866ef8a 100644 --- a/tests/core/Touched.spec.ts +++ b/tests/core/Touched.spec.ts @@ -60,5 +60,9 @@ describe('Touched.ts', () => { touched.unset('a') expect(touched.all()).toEqual(['b']) + + touched.unset('c') + + expect(touched.all()).toEqual(['b']) }) }) diff --git a/tests/core/Validator.spec.ts b/tests/core/Validator.spec.ts index 29dcda0..4e44457 100644 --- a/tests/core/Validator.spec.ts +++ b/tests/core/Validator.spec.ts @@ -84,7 +84,7 @@ describe('Validator.js', () => { ).toEqual('Developer is invalid. the true is incorrect') }) - it('should deterime if has rule', () => { + it('should determine if has rule', () => { let validator = new Validator(rules, defaultOptions.validation) expect(validator.has('first_name')).toBe(true) @@ -132,6 +132,15 @@ describe('Validator.js', () => { expect(errors[0]).toBe('Is Developer is invalid. the false is incorrect') }) + it('should return empty errors array if field is not exists', () => { + let validator = new Validator(rules, defaultOptions.validation) + + let mockFormField = { key: 'some_other_field_1', value: null, label: 'A' } + let mockForm = new Form({}) + + expect(validator.validateField(mockFormField, mockForm)).toHaveLength(0) + }) + it('should call passes and message callback functions with the right params when validate', () => { let validator = new Validator( { diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts new file mode 100644 index 0000000..4348134 --- /dev/null +++ b/tests/jest.setup.ts @@ -0,0 +1,2 @@ +// Mocking the console error - the `warn` function will not log error out +console.error = jest.fn() diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index e80b853..d7d4670 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -1,4 +1,4 @@ -import { isObject } from '../src/utils' +import { isObject, warn } from '../src/utils' describe('utils.js', () => { it('should determine if value is object', () => { @@ -8,4 +8,13 @@ describe('utils.js', () => { expect(isObject(1)).toBe(false) expect(isObject('aa')).toBe(false) }) + + it('should console.error an error message', () => { + warn('random error message') + + expect(console.error).toHaveBeenCalledTimes(1) + expect(console.error).toHaveBeenCalledWith( + '[Form-wrapper-js warn]: random error message' + ) + }) }) From 8a43e6f5ba2ca6b2b160c782e2ba802ec02cc258 Mon Sep 17 00:00:00 2001 From: Nevo Date: Wed, 19 Dec 2018 20:18:27 +0200 Subject: [PATCH 10/10] Version 0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ebd04c..48b2db5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "form-wrapper-js", - "version": "0.5.0", + "version": "0.6.0", "description": "JS abstraction for forms", "main": "dist/index.js", "module": "dist/index.es.js",