diff --git a/.babelrc b/.babelrc index 9b3e764..e0467a6 100644 --- a/.babelrc +++ b/.babelrc @@ -11,11 +11,7 @@ ], "@babel/preset-react" ], - "env": { - "test": { - "plugins": ["@babel/plugin-transform-react-jsx-source", "istanbul"] - } - }, + "plugins": [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-import-meta", diff --git a/package-lock.json b/package-lock.json index c136cdb..b5f6b76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1020,6 +1020,16 @@ } } }, + "@jest/types": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.7.0.tgz", + "integrity": "sha512-ipJUa2rFWiKoBqMKP63Myb6h9+iT3FHRTF2M8OR6irxWzItisa8i4dcSg14IbvmXUnBlHBlUQPYUHWyX3UPpYA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/yargs": "^12.0.9" + } + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1029,6 +1039,12 @@ "any-observable": "^0.3.0" } }, + "@sheerun/mutationobserver-shim": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz", + "integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==", + "dev": true + }, "@textlint/ast-node-types": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-4.2.1.tgz", @@ -1073,6 +1089,12 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "@types/istanbul-lib-coverage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz", + "integrity": "sha512-eAtOAFZefEnfJiRFQBGw1eYqa5GTLCZ1y86N0XSI/D6EB+E8z6VPV/UL7Gi5UEclFqoQk+6NRqEDsfmDLXn8sg==", + "dev": true + }, "@types/node": { "version": "10.12.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz", @@ -1095,6 +1117,12 @@ "csstype": "^2.2.0" } }, + "@types/yargs": { + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", + "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==", + "dev": true + }, "abab": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", @@ -2770,6 +2798,53 @@ } } }, + "dom-testing-library": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.19.0.tgz", + "integrity": "sha512-gkGXP5GevcjC24Tk6Y6RwrZ7Nz0Ul4bchXV4yHLcnMidMp/EdBCvtHEgHTsZ2yZ4DhUpLowGbJv/1u1Z7bPvtw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.4", + "@sheerun/mutationobserver-shim": "^0.3.2", + "pretty-format": "^24.5.0", + "wait-for-expect": "^1.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz", + "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "pretty-format": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.7.0.tgz", + "integrity": "sha512-apen5cjf/U4dj7tHetpC7UEFCvtAgnNZnBDkfPv3fokzIqyOJckAG9OlAPC1BlFALnqT/lGB2tl9EJjlK6eCsA==", + "dev": true, + "requires": { + "@jest/types": "^24.7.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", + "dev": true + } + } + }, "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", @@ -8953,6 +9028,66 @@ "scheduler": "^0.13.1" } }, + "react-hooks-testing-library": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/react-hooks-testing-library/-/react-hooks-testing-library-0.4.0.tgz", + "integrity": "sha512-N/MBIlycPeeAOJwFloUnwpKLBDH2y0L17srRMw1bI/dCyYYC2zKwg9Xccxwy4HFVAGfO9uweVrAGFvOnc6YvUw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.2", + "react-testing-library": "^6.0.3" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz", + "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", + "dev": true + } + } + }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", + "dev": true + }, + "react-testing-library": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-6.1.2.tgz", + "integrity": "sha512-z69lhRDGe7u/NOjDCeFRoe1cB5ckJ4656n0tj/Fdcr6OoBUu7q9DBw0ftR7v5i3GRpdSWelnvl+feZFOyXyxwg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.2", + "dom-testing-library": "^3.19.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz", + "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", + "dev": true + } + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -10932,6 +11067,12 @@ "browser-process-hrtime": "^0.1.2" } }, + "wait-for-expect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.1.1.tgz", + "integrity": "sha512-vd9JOqqEcBbCDhARWhW85ecjaEcfBLuXgVBqatfS3iw6oU4kzAcs+sCNjF+TC9YHPImCW7ypsuQc+htscIAQCw==", + "dev": true + }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", diff --git a/package-scripts.js b/package-scripts.js index 986c14e..5f256f1 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -6,6 +6,10 @@ const rimraf = npsUtils.rimraf module.exports = { scripts: { + test: { + default: 'jest --coverage', + watch: 'jest --coverage --watch' + }, size: { description: 'check the size of the bundle', script: 'bundlesize' diff --git a/package.json b/package.json index 32de5cd..34c41f8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "scripts": { "start": "nps", + "test": "nps test", "doctoc": "doctoc README.md && doctoc docs/faq.md && prettier --write README.md && prettier --write docs/faq.md", "precommit": "lint-staged && npm start validate", "prepublish": "npm start validate", @@ -69,6 +70,7 @@ "prop-types": "^15.6.2", "react": "^16.8.1", "react-dom": "^16.8.1", + "react-hooks-testing-library": "^0.4.0", "rollup": "^1.1.2", "rollup-plugin-babel": "^4.0.1", "rollup-plugin-commonjs": "^9.2.0", diff --git a/src/useField.test.js b/src/useField.test.js new file mode 100644 index 0000000..11a4f96 --- /dev/null +++ b/src/useField.test.js @@ -0,0 +1,157 @@ +import { renderHook, cleanup } from 'react-hooks-testing-library' +import useField, { all } from './useField' + +describe('useField()', () => { + let form, name, subscription + + beforeEach(() => { + name = 'foo' + subscription = { value: true } + }) + afterEach(cleanup) + + describe("form hook parameter's registerField", () => { + beforeEach(() => { + form = { + registerField: jest.fn() + } + }) + + it('is called with correct params', () => { + renderHook(() => useField(name, form, subscription)) + + expect(form.registerField).toHaveBeenCalledWith( + name, + expect.any(Function), + subscription + ) + }) + + it('receives all subscriptions by default', () => { + renderHook(() => useField(name, form)) + + expect(form.registerField).toHaveBeenCalledWith( + name, + expect.any(Function), + all + ) + }) + }) + + describe('field input props return object', () => { + let value, blur, change, focus + + const setupHook = () => { + const { result } = renderHook(() => useField(name, form, subscription)) + const { input } = result.current + + return input + } + + beforeEach(() => { + value = 'bar' + blur = jest.fn() + change = jest.fn() + focus = jest.fn() + + form = { + registerField: jest.fn((name, callback, subscription) => + callback({ blur, change, focus, value }) + ) + } + }) + + it('has the correct name', () => { + const { name: returnName } = setupHook() + + expect(returnName).toBe(name) + }) + + it('has the correct value', () => { + const { value: returnValue } = setupHook() + + expect(returnValue).toBe(value) + }) + + describe('onBlur()', () => { + it('calls the correct event handler', () => { + const { onBlur } = setupHook() + + onBlur() + + expect(blur).toHaveBeenCalled() + }) + }) + + describe('onChange()', () => { + describe('when event is not an usual input event', () => { + const event = { foo: 'bar' } + + it('calls the provided handler with event object', () => { + const { onChange } = setupHook() + + onChange(event) + + expect(change).toHaveBeenCalledWith(event) + }) + }) + + describe('when event has a value prop', () => { + const event = { target: { value: 'foo' } } + + it('calls provided handler with value', () => { + const { onChange } = setupHook() + + onChange(event) + + expect(change).toHaveBeenLastCalledWith(event.target.value) + }) + }) + + describe('when event has a checked prop', () => { + const event = { target: { type: 'radio', checked: false } } + + it('calls provided handler with value', () => { + const { onChange } = setupHook() + + onChange(event) + + expect(change).toHaveBeenLastCalledWith(event.target.checked) + }) + }) + }) + + describe('onFocus()', () => { + it('calls the correct event handler', () => { + const { onFocus } = setupHook() + + onFocus() + + expect(focus).toHaveBeenCalled() + }) + }) + }) + + describe('field meta return object', () => { + let meta + + beforeEach(() => { + meta = { name: 'foo', bar: 'bar', biz: 'biz' } + + form = { + registerField: jest.fn((name, callback, subscription) => + callback({ ...meta }) + ) + } + }) + + it('has the correct values', () => { + const { result } = renderHook(() => useField(name, form, subscription)) + const { meta: returnMeta } = result.current + + delete meta.name + + expect(returnMeta).toEqual(meta) + }) + }) +}) diff --git a/src/useForm.test.js b/src/useForm.test.js new file mode 100644 index 0000000..f46508a --- /dev/null +++ b/src/useForm.test.js @@ -0,0 +1,113 @@ +import { renderHook, cleanup, act } from 'react-hooks-testing-library' +import useForm from './useForm' + +describe('useForm()', () => { + let defaultConfig + + beforeEach(() => { + defaultConfig = { + onSubmit: jest.fn() + } + }) + afterEach(cleanup) + + const setup = overrideConfig => { + const initialProps = { + ...defaultConfig, + ...overrideConfig + } + const { result, rerender } = renderHook(props => useForm(props), { + initialProps + }) + const { handleSubmit, form } = result.current + + return { handleSubmit, form, rerender, result } + } + + describe('handleSubmit()', () => { + it('causes form to submit', () => { + const { handleSubmit } = setup() + + act(() => handleSubmit()) + + expect(defaultConfig.onSubmit).toHaveBeenCalled() + }) + + describe('when event has preventDefault()', () => { + let event + + beforeEach(() => { + event = { preventDefault: jest.fn() } + }) + + it('calls prevent default', () => { + const { handleSubmit } = setup() + + act(() => handleSubmit(event)) + + expect(event.preventDefault).toHaveBeenCalled() + }) + }) + + describe('when event has stopPropagation()', () => { + let event + + beforeEach(() => { + event = { stopPropagation: jest.fn() } + }) + + it('calls prevent default', () => { + const { handleSubmit } = setup() + + act(() => handleSubmit(event)) + + expect(event.stopPropagation).toHaveBeenCalled() + }) + }) + }) + + describe('form config', () => { + describe('initialValues', () => { + const overrideConfig = { initialValues: { foo: 'foo' } } + + it('is set to initialValues provided in config object', () => { + const { form } = setup(overrideConfig) + const { initialValues } = form.getState() + + expect(initialValues).toEqual(overrideConfig.initialValues) + }) + + it('can be changed', () => { + const nextConfig = { initialValues: { bar: 'bar' } } + const { rerender, result } = setup(overrideConfig) + + act(() => rerender({ ...defaultConfig, ...nextConfig })) + const { form } = result.current + const { initialValues } = form.getState() + + expect(initialValues).toEqual(nextConfig.initialValues) + }) + }) + + describe('other configuration', () => { + const overrideConfig = { mutators: { foo: () => 'foo' } } + + it('is set to configuration provided in config object', () => { + const { form } = setup(overrideConfig) + + expect(form.mutators.hasOwnProperty('foo')).toBe(true) + }) + + it('can be changed', () => { + const nextConfig = { mutators: { bar: () => 'bar' } } + const { rerender, result } = setup(overrideConfig) + + act(() => rerender({ ...defaultConfig, ...nextConfig })) + const { form } = result.current + + expect(form.mutators.hasOwnProperty('foo')).toBe(false) + expect(form.mutators.hasOwnProperty('bar')).toBe(true) + }) + }) + }) +}) diff --git a/src/useFormState.test.js b/src/useFormState.test.js new file mode 100644 index 0000000..9d9b668 --- /dev/null +++ b/src/useFormState.test.js @@ -0,0 +1,56 @@ +import { renderHook, cleanup, act } from 'react-hooks-testing-library' +import useFormState, { all } from './useFormState' + +describe('useFormState()', () => { + let form, formState, setFormState, subscription + + beforeEach(() => { + subscription = { value: true } + formState = { foo: 'foo' } + form = { + getState: jest.fn(() => formState), + subscribe: jest.fn((setState, subscription) => (setFormState = setState)) + } + }) + afterEach(cleanup) + + describe('form state', () => { + it('comes from form parameter', () => { + const { result } = renderHook(() => useFormState(form, subscription)) + const returnState = result.current + + expect(returnState).toBe(formState) + }) + }) + + describe('subscription array', () => { + it('defaults to all subscriptions', () => { + renderHook(() => useFormState(form)) + + expect(form.subscribe).toHaveBeenCalledWith(expect.any(Function), all) + }) + }) + + describe("form's subscribe function", () => { + it('is called with the subscrition array', () => { + renderHook(() => useFormState(form, subscription)) + + expect(form.subscribe).toHaveBeenCalledWith( + expect.any(Function), + subscription + ) + }) + + it('receives a callback to update form state', () => { + const nextState = { bar: 'bar' } + const { result } = renderHook(() => useFormState(form, subscription)) + + act(() => { + setFormState(nextState) + }) + const returnState = result.current + + expect(returnState).toBe(nextState) + }) + }) +})