From 83e73e115d92f1a38f5e416aab2e18adc83e0fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 16 Sep 2018 00:57:13 +0200 Subject: [PATCH 01/27] provide custom isEqual function --- src/FieldArray.js | 3 ++- src/FieldArray.test.js | 47 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/FieldArray.js b/src/FieldArray.js index 153b52b..f8c35f6 100644 --- a/src/FieldArray.js +++ b/src/FieldArray.js @@ -75,7 +75,8 @@ class FieldArray extends React.PureComponent { listener, subscription ? { ...subscription, length: true } : all, { - getValidator: () => this.validate + getValidator: () => this.validate, + isEqual: () => true } ) } diff --git a/src/FieldArray.test.js b/src/FieldArray.test.js index 1c10ba5..342d4ee 100644 --- a/src/FieldArray.test.js +++ b/src/FieldArray.test.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Fragment } from 'react' import TestUtils from 'react-dom/test-utils' import { Form, Field } from 'react-final-form' import arrayMutators from 'final-form-arrays' @@ -505,4 +505,49 @@ describe('FieldArray', () => { await sleep(2) expect(spy).not.toHaveBeenCalled() }) + + it('should provide custom isEqual method to calculate pristine correctly', () => { + const formRender = jest.fn(() => ( + ( + + {fields.map((name, index) => ( + + + + + ))} + + )} + /> + )) + const dom = TestUtils.renderIntoDocument( +
+ ) + const input = TestUtils.findRenderedDOMComponentWithTag(dom, 'input') + const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') + + // initially pristine true + expect(formRender.mock.calls[0][0]).toMatchObject({ + pristine: true + }) + + // changing value, pristine false + TestUtils.Simulate.change(input, { target: { value: 'foo' } }) + expect(formRender.mock.calls[1][0]).toMatchObject({ pristine: false }) + + // changing value back to default, pristine true + TestUtils.Simulate.change(input, { target: { value: 'example' } }) + expect(formRender.mock.calls[2][0]).toMatchObject({ pristine: true }) + + // removing field, pristine false + TestUtils.Simulate.click(button) + expect(formRender.mock.calls[3][0]).toMatchObject({ pristine: false }) + }) }) From 67b478ccaf7b27475a628a734b3a52e73473f899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Wed, 10 Oct 2018 23:42:20 +0200 Subject: [PATCH 02/27] Add isEqual prop to FieldArray component --- README.md | 87 +++++++++++++++++++++++------------------- src/FieldArray.js | 15 ++++++-- src/FieldArray.test.js | 27 +++++++------ 3 files changed, 72 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index f1fbbc1..410f74d 100644 --- a/README.md +++ b/README.md @@ -81,45 +81,50 @@ const MyForm = () => ( -* [Examples](#examples) - * [Simple Example](#simple-example) -* [Rendering](#rendering) -* [API](#api) - * [`FieldArray : React.ComponentType`](#fieldarray--reactcomponenttypefieldarrayprops) - * [`version: string`](#version-string) -* [Types](#types) - * [`FieldArrayProps`](#fieldarrayprops) - * [`children?: ((props: FieldArrayRenderProps) => React.Node) | React.Node`](#children-props-fieldarrayrenderprops--reactnode--reactnode) - * [`component?: React.ComponentType`](#component-reactcomponenttypefieldarrayrenderprops) - * [`name: string`](#name-string) - * [`render?: (props: FieldArrayRenderProps) => React.Node`](#render-props-fieldarrayrenderprops--reactnode) - * [`subscription?: FieldSubscription`](#subscription-fieldsubscription) - * [`validate?: (value: ?any[], allValues: Object) => ?any`](#validate-value-any-allvalues-object--any) - * [`FieldArrayRenderProps`](#fieldarrayrenderprops) - * [`fields.forEach: (iterator: (name: string, index: number) => void) => void`](#fieldsforeach-iterator-name-string-index-number--void--void) - * [`fields.insert: (index: number, value: any) => void`](#fieldsinsert-index-number-value-any--void) - * [`fields.map: (iterator: (name: string, index: number) => any) => any[]`](#fieldsmap-iterator-name-string-index-number--any--any) - * [`fields.move: (from: number, to: number) => void`](#fieldsmove-from-number-to-number--void) - * [`fields.name: string`](#fieldsname-string) - * [`fields.pop: () => any`](#fieldspop---any) - * [`fields.push: (value: any) => void`](#fieldspush-value-any--void) - * [`fields.remove: (index: number) => any`](#fieldsremove-index-number--any) - * [`fields.shift: () => any`](#fieldsshift---any) - * [`fields.swap: (indexA: number, indexB: number) => void`](#fieldsswap-indexa-number-indexb-number--void) - * [`fields.unshift: (value: any) => void`](#fieldsunshift-value-any--void) - * [`meta.active?: boolean`](#metaactive-boolean) - * [`meta.data: Object`](#metadata-object) - * [`meta.dirty?: boolean`](#metadirty-boolean) - * [`meta.error?: any`](#metaerror-any) - * [`meta.initial?: any`](#metainitial-any) - * [`meta.invalid?: boolean`](#metainvalid-boolean) - * [`meta.pristine?: boolean`](#metapristine-boolean) - * [`meta.submitError?: any`](#metasubmiterror-any) - * [`meta.submitFailed?: boolean`](#metasubmitfailed-boolean) - * [`meta.submitSucceeded?: boolean`](#metasubmitsucceeded-boolean) - * [`meta.touched?: boolean`](#metatouched-boolean) - * [`meta.valid?: boolean`](#metavalid-boolean) - * [`meta.visited?: boolean`](#metavisited-boolean) +- [🏁 React Final Form Arrays](#-react-final-form-arrays) + - [Installation](#installation) + - [Usage](#usage) + - [Table of Contents](#table-of-contents) + - [Examples](#examples) + - [Simple Example](#simple-example) + - [Rendering](#rendering) + - [API](#api) + - [`FieldArray : React.ComponentType`](#fieldarray--reactcomponenttypefieldarrayprops) + - [`version: string`](#version-string) + - [Types](#types) + - [`FieldArrayProps`](#fieldarrayprops) + - [`children?: ((props: FieldArrayRenderProps) => React.Node) | React.Node`](#children-props-fieldarrayrenderprops--reactnode--reactnode) + - [`component?: React.ComponentType`](#component-reactcomponenttypefieldarrayrenderprops) + - [`name: string`](#name-string) + - [`render?: (props: FieldArrayRenderProps) => React.Node`](#render-props-fieldarrayrenderprops--reactnode) + - [`isEqual?: (allPreviousValues: Array, allNewValues: Array) => boolean`](#isequal-allpreviousvalues-arrayany-allnewvalues-arrayany--boolean) + - [`subscription?: FieldSubscription`](#subscription-fieldsubscription) + - [`validate?: (value: ?any[], allValues: Object) => ?any`](#validate-value-any-allvalues-object--any) + - [`FieldArrayRenderProps`](#fieldarrayrenderprops) + - [`fields.forEach: (iterator: (name: string, index: number) => void) => void`](#fieldsforeach-iterator-name-string-index-number--void--void) + - [`fields.insert: (index: number, value: any) => void`](#fieldsinsert-index-number-value-any--void) + - [`fields.map: (iterator: (name: string, index: number) => any) => any[]`](#fieldsmap-iterator-name-string-index-number--any--any) + - [`fields.move: (from: number, to: number) => void`](#fieldsmove-from-number-to-number--void) + - [`fields.name: string`](#fieldsname-string) + - [`fields.pop: () => any`](#fieldspop---any) + - [`fields.push: (value: any) => void`](#fieldspush-value-any--void) + - [`fields.remove: (index: number) => any`](#fieldsremove-index-number--any) + - [`fields.shift: () => any`](#fieldsshift---any) + - [`fields.swap: (indexA: number, indexB: number) => void`](#fieldsswap-indexa-number-indexb-number--void) + - [`fields.unshift: (value: any) => void`](#fieldsunshift-value-any--void) + - [`meta.active?: boolean`](#metaactive-boolean) + - [`meta.data: Object`](#metadata-object) + - [`meta.dirty?: boolean`](#metadirty-boolean) + - [`meta.error?: any`](#metaerror-any) + - [`meta.initial?: any`](#metainitial-any) + - [`meta.invalid?: boolean`](#metainvalid-boolean) + - [`meta.pristine?: boolean`](#metapristine-boolean) + - [`meta.submitError?: any`](#metasubmiterror-any) + - [`meta.submitFailed?: boolean`](#metasubmitfailed-boolean) + - [`meta.submitSucceeded?: boolean`](#metasubmitsucceeded-boolean) + - [`meta.touched?: boolean`](#metatouched-boolean) + - [`meta.valid?: boolean`](#metavalid-boolean) + - [`meta.visited?: boolean`](#metavisited-boolean) @@ -184,6 +189,10 @@ A render function that is given [`FieldArrayRenderProps`](#fieldarrayrenderprops), as well as any non-API props passed into the `` component. +#### `isEqual?: (allPreviousValues: Array, allNewValues: Array) => boolean` + +A function that can be used to compare two arrays of values (before and after every change) and calculate pristine/dirty checks. + #### `subscription?: FieldSubscription` A diff --git a/src/FieldArray.js b/src/FieldArray.js index f8c35f6..eaa84c9 100644 --- a/src/FieldArray.js +++ b/src/FieldArray.js @@ -66,6 +66,14 @@ class FieldArray extends React.PureComponent { this.mounted = false } + isEqual = (a: Array, b: Array) => { + if (typeof this.props.isEqual === 'function') { + return this.props.isEqual(a, b) + } + + return true + } + subscribe = ( { name, subscription }: Props, listener: (state: FieldState) => void @@ -76,7 +84,7 @@ class FieldArray extends React.PureComponent { subscription ? { ...subscription, length: true } : all, { getValidator: () => this.validate, - isEqual: () => true + isEqual: this.isEqual } ) } @@ -84,7 +92,7 @@ class FieldArray extends React.PureComponent { validate: FieldValidator = (...args) => { const { validate } = this.props if (!validate) return undefined - const error = validate(...args) + const error = validate(args[0], args[1]) if (!error || Array.isArray(error)) { return error } else { @@ -192,8 +200,7 @@ class FieldArray extends React.PureComponent { valid, visited, ...fieldStateFunctions - } = - this.state.state || {} + } = this.state.state || {} const meta = { active, dirty, diff --git a/src/FieldArray.test.js b/src/FieldArray.test.js index 342d4ee..d42c71b 100644 --- a/src/FieldArray.test.js +++ b/src/FieldArray.test.js @@ -506,21 +506,20 @@ describe('FieldArray', () => { expect(spy).not.toHaveBeenCalled() }) - it('should provide custom isEqual method to calculate pristine correctly', () => { - const formRender = jest.fn(() => ( - ( - - {fields.map((name, index) => ( - - - - - ))} + it('should provide default isEqual method to calculate pristine correctly', () => { + const arrayFieldRender = jest.fn(({ fields }) => ( + + {fields.map((name, index) => ( + + + - )} - /> + ))} + + )) + + const formRender = jest.fn(() => ( + )) const dom = TestUtils.renderIntoDocument( Date: Sat, 1 Dec 2018 21:09:30 +0100 Subject: [PATCH 03/27] Reproduce a bug with properties, no generators --- package.json | 10 +-- src/FieldArray.property.test.js | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/FieldArray.property.test.js diff --git a/package.json b/package.json index bf47f72..c57c4a0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/plugin-transform-flow-strip-types": "^7.0.0", "@babel/preset-env": "^7.0.0", + "@babel/preset-flow": "^7.0.0", "@babel/preset-react": "^7.0.0", "@types/react": "^16.4.16", "babel-core": "^7.0.0-bridge.0", @@ -51,6 +52,7 @@ "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-react": "^7.11.1", + "fast-check": "^1.8.0", "final-form": "^4.10.0", "final-form-arrays": "^1.1.0", "flow-bin": "^0.83.0", @@ -68,6 +70,7 @@ "react": "^16.5.2", "react-dom": "^16.5.2", "react-final-form": "^4.0.0", + "react-testing-library": "^5.3.1", "rollup": "^0.66.6", "rollup-plugin-babel": "^4.0.1", "rollup-plugin-commonjs": "^9.2.0", @@ -75,15 +78,14 @@ "rollup-plugin-node-resolve": "^3.4.0", "rollup-plugin-replace": "^2.1.0", "rollup-plugin-uglify": "^6.0.0", - "typescript": "^3.1.3", - "@babel/preset-flow": "^7.0.0" + "typescript": "^3.1.3" }, "peerDependencies": { "final-form": ">=4.0.0", "final-form-arrays": ">=1.0.4", - "react-final-form": "^4.0.0", "prop-types": "^15.6.0", - "react": "^16.3.0" + "react": "^16.3.0", + "react-final-form": "^4.0.0" }, "jest": { "watchPlugins": [ diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js new file mode 100644 index 0000000..61c32cd --- /dev/null +++ b/src/FieldArray.property.test.js @@ -0,0 +1,107 @@ +import React, { Fragment } from 'react' +import TestUtils from 'react-dom/test-utils' +import { render, fireEvent } from 'react-testing-library' +import { Form, Field } from 'react-final-form' +import { FieldArray } from 'react-final-form-arrays' +import arrayMutators from 'final-form-arrays' + +const nope = () => {} +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) +const waitForFormToRerender = () => sleep(0) +const setup = () => + render( + + {({ + form: { + mutators: { push, move } + } + }) => { + return ( + + + {({ fields }) => { + return fields.map((name, index) => ( + + + + + )) + }} + + + + + ) + }} + + ) + +describe('FieldArray', () => { + it('should work', async () => { + const DOM = setup() + const Model = [] + + const commands = { + addField: async () => { + // abstract + Model.push('') + // real + const buttonEl = DOM.getByText('Add fruit') + fireEvent.click(buttonEl) + await waitForFormToRerender() + }, + changeValue: (index, newValue) => { + // abstract + Model[index] = newValue + // real + const label = `Fruit ${index + 1} name` + const inputEl = DOM.getByLabelText(label) + fireEvent.change(inputEl, { target: { value: newValue } }) + }, + move: async (from, to) => { + // abstract + const cache = Model[from] + Model.splice(from, 1) + Model.splice(to, 0, cache) + // real + const buttonEl = DOM.getByText('Move fruit') + TestUtils.Simulate.keyPress(buttonEl, { which: from, location: to }) + await waitForFormToRerender() + } + } + + await commands.addField() + commands.changeValue(0, 'apple') + await commands.addField() + commands.changeValue(1, 'banana') + await commands.move(0, 1) + commands.changeValue(0, 'orange') + + const inputElements = DOM.container.querySelectorAll('input') + + // number of elements should be the same + expect(inputElements.length).toBe(Model.length) + + // values should be the same + const realValues = [...inputElements.values()].map(element => element.value) + console.log(realValues) + realValues.forEach((realValue, index) => { + const modelValue = Model[index] + expect(realValue).toBe(modelValue) + }) + + DOM.unmount() + }) +}) From 4ddb3291af666684f294e11992fd805bcd5cf4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sat, 1 Dec 2018 21:47:17 +0100 Subject: [PATCH 04/27] Add pre and postconditions --- src/FieldArray.property.test.js | 126 +++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 61c32cd..5ba97a4 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -53,54 +53,98 @@ describe('FieldArray', () => { const DOM = setup() const Model = [] + const selectAllInputs = () => DOM.container.querySelectorAll('input') + + const correctNumberOfInputs = () => { + const inputElements = selectAllInputs() + expect(inputElements.length).toBe(Model.length) + } + + const correctValues = () => { + const inputElements = selectAllInputs() + const realValues = [...inputElements.values()].map( + element => element.value + ) + realValues.forEach((realValue, index) => { + const modelValue = Model[index] + expect(realValue).toBe(modelValue) + }) + } + const commands = { - addField: async () => { - // abstract - Model.push('') - // real - const buttonEl = DOM.getByText('Add fruit') - fireEvent.click(buttonEl) - await waitForFormToRerender() + addField: { + preconditions: () => true, + run: async () => { + // abstract + Model.push('') + // real + const buttonEl = DOM.getByText('Add fruit') + fireEvent.click(buttonEl) + await waitForFormToRerender() + }, + postconditions: () => { + correctNumberOfInputs() + correctValues() + } }, - changeValue: (index, newValue) => { - // abstract - Model[index] = newValue - // real - const label = `Fruit ${index + 1} name` - const inputEl = DOM.getByLabelText(label) - fireEvent.change(inputEl, { target: { value: newValue } }) + changeValue: { + preconditions: (index, newValue) => { + if (index >= Model.length) return false + return true + }, + run: (index, newValue) => { + // abstract + Model[index] = newValue + // real + const label = `Fruit ${index + 1} name` + const inputEl = DOM.getByLabelText(label) + fireEvent.change(inputEl, { target: { value: newValue } }) + }, + postconditions: () => { + correctNumberOfInputs() + correctValues() + } }, - move: async (from, to) => { - // abstract - const cache = Model[from] - Model.splice(from, 1) - Model.splice(to, 0, cache) - // real - const buttonEl = DOM.getByText('Move fruit') - TestUtils.Simulate.keyPress(buttonEl, { which: from, location: to }) - await waitForFormToRerender() + move: { + preconditions: (from, to) => { + if (from >= Model.length || to >= Model.length) return false + return true + }, + run: async (from, to) => { + // abstract + const cache = Model[from] + Model.splice(from, 1) + Model.splice(to, 0, cache) + // real + const buttonEl = DOM.getByText('Move fruit') + TestUtils.Simulate.keyPress(buttonEl, { which: from, location: to }) + await waitForFormToRerender() + }, + postconditions: () => { + correctNumberOfInputs() + correctValues() + } } } - await commands.addField() - commands.changeValue(0, 'apple') - await commands.addField() - commands.changeValue(1, 'banana') - await commands.move(0, 1) - commands.changeValue(0, 'orange') - - const inputElements = DOM.container.querySelectorAll('input') - - // number of elements should be the same - expect(inputElements.length).toBe(Model.length) + function execute(command) { + return { + with: async (...args) => { + if (!command.preconditions(...args)) { + throw Error('command cannot be executed'); + } + await command.run(...args) + command.postconditions(...args) + } + } + } - // values should be the same - const realValues = [...inputElements.values()].map(element => element.value) - console.log(realValues) - realValues.forEach((realValue, index) => { - const modelValue = Model[index] - expect(realValue).toBe(modelValue) - }) + await execute(commands.addField).with() + await execute(commands.changeValue).with(0, 'apple') + await execute(commands.addField).with() + await execute(commands.changeValue).with(1, 'banana') + await execute(commands.move).with(0, 1) + await execute(commands.changeValue).with(0, 'orange') DOM.unmount() }) From d153ffb6c88e40fa9f6c643afd40dac04f81f01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 08:20:30 +0100 Subject: [PATCH 05/27] Implement full-blown model-based tests --- src/FieldArray.property.test.js | 192 ++++++++++++++++++++------------ 1 file changed, 118 insertions(+), 74 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 5ba97a4..e9f87ac 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -1,15 +1,17 @@ import React, { Fragment } from 'react' import TestUtils from 'react-dom/test-utils' -import { render, fireEvent } from 'react-testing-library' +import { render, fireEvent, cleanup } from 'react-testing-library' import { Form, Field } from 'react-final-form' import { FieldArray } from 'react-final-form-arrays' import arrayMutators from 'final-form-arrays' +import fc from 'fast-check' const nope = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForFormToRerender = () => sleep(0) -const setup = () => - render( +const INITIAL_NUMBER_OF_FIELDS = 2 +const setup = async () => { + const DOM = render(
{({ form: { @@ -48,20 +50,28 @@ const setup = () =>
) + const Model = [] + + const buttonEl = DOM.getByText('Add fruit') + ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { + fireEvent.click(buttonEl) + Model.push('') + }) + await waitForFormToRerender() + return { DOM, Model } +} + describe('FieldArray', () => { it('should work', async () => { - const DOM = setup() - const Model = [] - - const selectAllInputs = () => DOM.container.querySelectorAll('input') + const selectAllInputs = DOM => DOM.container.querySelectorAll('input') - const correctNumberOfInputs = () => { - const inputElements = selectAllInputs() + const correctNumberOfInputs = (Model, DOM) => { + const inputElements = selectAllInputs(DOM) expect(inputElements.length).toBe(Model.length) } - const correctValues = () => { - const inputElements = selectAllInputs() + const correctValues = (Model, DOM) => { + const inputElements = selectAllInputs(DOM) const realValues = [...inputElements.values()].map( element => element.value ) @@ -72,80 +82,114 @@ describe('FieldArray', () => { } const commands = { - addField: { - preconditions: () => true, - run: async () => { - // abstract - Model.push('') - // real - const buttonEl = DOM.getByText('Add fruit') - fireEvent.click(buttonEl) - await waitForFormToRerender() - }, - postconditions: () => { - correctNumberOfInputs() - correctValues() + AddField: function addField() { + return { + toString: () => 'add field', + check: () => true, + run: async (Model, DOM) => { + // abstract + Model.push('') + // real + const buttonEl = DOM.getByText('Add fruit') + fireEvent.click(buttonEl) + await waitForFormToRerender() + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } } }, - changeValue: { - preconditions: (index, newValue) => { - if (index >= Model.length) return false - return true - }, - run: (index, newValue) => { - // abstract - Model[index] = newValue - // real - const label = `Fruit ${index + 1} name` - const inputEl = DOM.getByLabelText(label) - fireEvent.change(inputEl, { target: { value: newValue } }) - }, - postconditions: () => { - correctNumberOfInputs() - correctValues() + ChangeValue: function ChangeValue(index, newValue) { + return { + toString: () => `change value at ${index} to ${newValue}`, + check: Model => { + if (index >= Model.length) return false + return true + }, + run: (Model, DOM) => { + // abstract + Model[index] = newValue + // real + const label = `Fruit ${index + 1} name` + const inputEl = DOM.getByLabelText(label) + fireEvent.change(inputEl, { target: { value: newValue } }) + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } } }, - move: { - preconditions: (from, to) => { - if (from >= Model.length || to >= Model.length) return false - return true - }, - run: async (from, to) => { - // abstract - const cache = Model[from] - Model.splice(from, 1) - Model.splice(to, 0, cache) - // real - const buttonEl = DOM.getByText('Move fruit') - TestUtils.Simulate.keyPress(buttonEl, { which: from, location: to }) - await waitForFormToRerender() - }, - postconditions: () => { - correctNumberOfInputs() - correctValues() + Move: function Move(from, to) { + return { + toString: () => `move ${from} to ${to}`, + check: Model => { + if (from >= Model.length || to >= Model.length) return false + return true + }, + run: async (Model, DOM) => { + // abstract + const cache = Model[from] + Model.splice(from, 1) + Model.splice(to, 0, cache) + // real + const buttonEl = DOM.getByText('Move fruit') + TestUtils.Simulate.keyPress(buttonEl, { which: from, location: to }) + await waitForFormToRerender() + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } } } } - function execute(command) { - return { - with: async (...args) => { - if (!command.preconditions(...args)) { - throw Error('command cannot be executed'); - } - await command.run(...args) - command.postconditions(...args) - } + const genericModelRun = async (setup, commands, initialValue, then) => { + const { model, real } = await setup() + let state = initialValue + for (const c of commands) { + state = then(state, () => { + if (c.check(model)) return c.run(model, real) + }) } + return state } - await execute(commands.addField).with() - await execute(commands.changeValue).with(0, 'apple') - await execute(commands.addField).with() - await execute(commands.changeValue).with(1, 'banana') - await execute(commands.move).with(0, 1) - await execute(commands.changeValue).with(0, 'orange') + const asyncModelRun = (setup, commands) => { + const then = (p, c) => p.then(c) + return genericModelRun(setup, commands, Promise.resolve(), then) + } - DOM.unmount() + await fc.assert( + fc + .asyncProperty( + fc.commands([ + fc.constant(new commands.AddField()), + fc + .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS), fc.string()) + .map(args => new commands.ChangeValue(...args)), + fc + .tuple( + fc.nat(INITIAL_NUMBER_OF_FIELDS), + fc.nat(INITIAL_NUMBER_OF_FIELDS) + ) + .map(args => new commands.Move(...args)) + ]), + async commands => { + const getInitialState = async () => { + const { Model, DOM } = await setup() + return { + model: Model, + real: DOM + } + } + await asyncModelRun(getInitialState, commands) + } + ) + .afterEach(cleanup), + { + numRuns: 100, + verbose: true + } + ) }) }) From 2cf971ff69854b2dc29ef83248f6b17abd7fe136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 08:56:44 +0100 Subject: [PATCH 06/27] Better checks for add field, example for an issue --- src/FieldArray.property.test.js | 20 ++-- src/FieldArray.test.js | 173 +++++++++++++++++++++++++++++++- 2 files changed, 183 insertions(+), 10 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index e9f87ac..098d749 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -75,10 +75,7 @@ describe('FieldArray', () => { const realValues = [...inputElements.values()].map( element => element.value ) - realValues.forEach((realValue, index) => { - const modelValue = Model[index] - expect(realValue).toBe(modelValue) - }) + expect(realValues).toEqual(Model) } const commands = { @@ -165,16 +162,17 @@ describe('FieldArray', () => { fc.commands([ fc.constant(new commands.AddField()), fc - .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS), fc.string()) + .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) .map(args => new commands.ChangeValue(...args)), fc .tuple( - fc.nat(INITIAL_NUMBER_OF_FIELDS), - fc.nat(INITIAL_NUMBER_OF_FIELDS) + fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), + fc.nat(INITIAL_NUMBER_OF_FIELDS * 2) ) .map(args => new commands.Move(...args)) ]), async commands => { + console.log(commands) const getInitialState = async () => { const { Model, DOM } = await setup() return { @@ -188,7 +186,13 @@ describe('FieldArray', () => { .afterEach(cleanup), { numRuns: 100, - verbose: true + verbose: true, + // seed: 1842023377, + // seed: 1842107356, + examples: [ + // https://github.com/final-form/final-form-arrays/issues/15#issuecomment-442126496 + [[new commands.Move(1, 0), new commands.ChangeValue(0, 'apple')]] + ] } ) }) diff --git a/src/FieldArray.test.js b/src/FieldArray.test.js index 11662ea..334c6c3 100644 --- a/src/FieldArray.test.js +++ b/src/FieldArray.test.js @@ -1,5 +1,12 @@ import React, { Fragment } from 'react' import TestUtils from 'react-dom/test-utils' +import { + render, + fireEvent, + cleanup, + waitForElement +} from 'react-testing-library' +import fc from 'fast-check' import { Form, Field } from 'react-final-form' import arrayMutators from 'final-form-arrays' import FieldArray from './FieldArray' @@ -215,8 +222,8 @@ describe('FieldArray', () => { it('should allow field-level validation', async () => { const renderArray = jest.fn(() =>
) - const validate = jest.fn( - value => (value.length > 2 ? 'Too long' : undefined) + const validate = jest.fn(value => + value.length > 2 ? 'Too long' : undefined ) const render = jest.fn(() => (
@@ -603,3 +610,165 @@ describe('FieldArray', () => { expect(formRender.mock.calls[3][0]).toMatchObject({ pristine: false }) }) }) + +describe('FieldArray properties', () => { + it.only('should work', async () => { + function ChangeCommand(index, newValue) { + return { + check: model => { + console.log('I run check', model, index) + // return true; + return model.values.length >= index + }, + run: (model, real) => { + // abstract phase + model.values[index] = newValue + + // real phase + const inputEl = real.getByTestId(`input${index}`) + fireEvent.focus(inputEl) + fireEvent.change(inputEl, { target: { value: newValue } }) + fireEvent.blur(inputEl) + }, + toString: () => `change input value on index ${index} to ${newValue}` + } + } + + function AddFieldCommand() { + return { + check: () => true, + run: async (model, real) => { + model.push('') + + const addButton = real.getByText('add a new field') + fireEvent.click(addButton) + + const numberOfElementsInModel = model.length + await waitForElement(() => + real.getByTestId(`input${numberOfElementsInModel - 1}`) + ) + + const allInputElements = real.queryAllByTestId('input', { + exact: false + }) + const realValues = allInputElements.map(element => element.value) + // console.log(realValues); + + real.debug() + expect(allInputElements.length).toBe(numberOfElementsInModel) + }, + toString: () => 'Add a new field' + } + } + + const allCommands = [ + fc.constant(new AddFieldCommand()), + fc.nat(3).map(nat => { + // console.log(nat) + return new ChangeCommand(nat, 'abc') + }) + ] + + const generateRealSystem = () => + render( + ( + + + {({ fields }) => ( +
+ {fields.map((name, i) => ( + + ))} +
+ )} +
+ +
+ )} + /> + ) + + const realSystem = generateRealSystem() + + const generateState = () => ({ + model: [''], + real: generateRealSystem() + }) + + // const fieldCommand = new AddFieldCommand() + // await fieldCommand.run([], realSystem) + + // const result = fc.sample(allCommands[1]) + // console.log(result) + + await fc.assert( + fc.asyncProperty(fc.commands(allCommands, 6), async commands => { + await fc.asyncModelRun(generateState, commands) + console.log('I am cleaning up') + cleanup() + }), + { numRuns: 2 } + ) + }) +}) + +const generateInitialState = () => { + // generate random values for random number of inputs +} + +const initialState = generateInitialState() + +let model = { + state: initialState, + operations: { + handleChange: (state, index, newValue) => { + const newState = { ...state } + newState[index] = newValue + return newState + }, + handleMove: (state, from, to) => { + if (from === to) return { ...state } + const valueToMove = state[from] + const stateWithoutElement = [ + ...state.slice(0, from), + ...state.slice(from + 1) + ] + const newState = [ + ...stateWithoutElement.slice(0, to), + valueToMove, + ...stateWithoutElement.slice(to) + ] + return newState + } + } +} + +let avaliableCommands = { + change: { + preconditions: state => {}, + valuesGenerator: () => {}, + postconditions: state => {} + }, + move: { + preconditions: state => {}, + valuesGenerator: () => {}, + postconditions: state => {} + } +} + +// sources +// https://propertesting.com/book_stateful_properties.html +// https://hypothesis.readthedocs.io/en/latest/stateful.html From 865452135821791b1fbf6dad1565601228802a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 09:36:45 +0100 Subject: [PATCH 07/27] Add insert command --- src/FieldArray.property.test.js | 282 +++++++++++++++++++------------- 1 file changed, 166 insertions(+), 116 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 098d749..0a2c129 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -2,9 +2,9 @@ import React, { Fragment } from 'react' import TestUtils from 'react-dom/test-utils' import { render, fireEvent, cleanup } from 'react-testing-library' import { Form, Field } from 'react-final-form' -import { FieldArray } from 'react-final-form-arrays' import arrayMutators from 'final-form-arrays' import fc from 'fast-check' +import FieldArray from './FieldArray' const nope = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) @@ -15,7 +15,7 @@ const setup = async () => { {({ form: { - mutators: { push, move } + mutators: { push, move, insert } } }) => { return ( @@ -44,6 +44,13 @@ const setup = async () => { > Move fruit + ) }} @@ -60,129 +67,171 @@ const setup = async () => { await waitForFormToRerender() return { DOM, Model } } +const selectAllInputs = DOM => DOM.container.querySelectorAll('input') -describe('FieldArray', () => { - it('should work', async () => { - const selectAllInputs = DOM => DOM.container.querySelectorAll('input') +const correctNumberOfInputs = (Model, DOM) => { + const inputElements = selectAllInputs(DOM) + expect(inputElements.length).toBe(Model.length) +} - const correctNumberOfInputs = (Model, DOM) => { - const inputElements = selectAllInputs(DOM) - expect(inputElements.length).toBe(Model.length) - } +const correctValues = (Model, DOM) => { + const inputElements = selectAllInputs(DOM) + const realValues = [...inputElements.values()].map(element => element.value) + expect(realValues).toEqual(Model) +} - const correctValues = (Model, DOM) => { - const inputElements = selectAllInputs(DOM) - const realValues = [...inputElements.values()].map( - element => element.value +class AddField { + static generate = () => fc.constant(new commands.AddField()) + toString = () => 'add field' + check = () => true + run = async (Model, DOM) => { + // abstract + Model.push('') + // real + const buttonEl = DOM.getByText('Add fruit') + fireEvent.click(buttonEl) + await waitForFormToRerender() + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } +} + +class ChangeValue { + constructor(index, newValue) { + this.index = index + this.newValue = newValue + } + static generate = () => + fc + .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) + .map(args => new commands.ChangeValue(...args)) + toString = () => `change value at ${this.index} to ${this.newValue}` + check = Model => { + if (this.index >= Model.length) return false + return true + } + run = (Model, DOM) => { + // abstract + Model[this.index] = this.newValue + // real + const label = `Fruit ${this.index + 1} name` + const inputEl = DOM.getByLabelText(label) + fireEvent.change(inputEl, { target: { value: this.newValue } }) + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } +} + +class Move { + constructor(from, to) { + this.from = from + this.to = to + } + static generate = () => + fc + .tuple( + fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), + fc.nat(INITIAL_NUMBER_OF_FIELDS * 2) ) - expect(realValues).toEqual(Model) - } + .map(args => new commands.Move(...args)) + toString = () => `move ${this.from} to ${this.to}` + check = Model => { + if (this.from >= Model.length || this.to >= Model.length) return false + return true + } + run = async (Model, DOM) => { + // abstract + const cache = Model[this.from] + Model.splice(this.from, 1) + Model.splice(this.to, 0, cache) + // real + const buttonEl = DOM.getByText('Move fruit') + TestUtils.Simulate.keyPress(buttonEl, { + which: this.from, + location: this.to + }) + await waitForFormToRerender() + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } +} - const commands = { - AddField: function addField() { - return { - toString: () => 'add field', - check: () => true, - run: async (Model, DOM) => { - // abstract - Model.push('') - // real - const buttonEl = DOM.getByText('Add fruit') - fireEvent.click(buttonEl) - await waitForFormToRerender() - // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) - } - } - }, - ChangeValue: function ChangeValue(index, newValue) { - return { - toString: () => `change value at ${index} to ${newValue}`, - check: Model => { - if (index >= Model.length) return false - return true - }, - run: (Model, DOM) => { - // abstract - Model[index] = newValue - // real - const label = `Fruit ${index + 1} name` - const inputEl = DOM.getByLabelText(label) - fireEvent.change(inputEl, { target: { value: newValue } }) - // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) - } - } - }, - Move: function Move(from, to) { - return { - toString: () => `move ${from} to ${to}`, - check: Model => { - if (from >= Model.length || to >= Model.length) return false - return true - }, - run: async (Model, DOM) => { - // abstract - const cache = Model[from] - Model.splice(from, 1) - Model.splice(to, 0, cache) - // real - const buttonEl = DOM.getByText('Move fruit') - TestUtils.Simulate.keyPress(buttonEl, { which: from, location: to }) - await waitForFormToRerender() - // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) - } - } - } - } +class Insert { + constructor(index, value) { + this.index = index + this.value = value + } + static generate = () => + fc + .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) + .map(args => new commands.Insert(...args)) + toString = () => `insert ${this.value} at ${this.index}` + check = () => true + run = async (Model, DOM) => { + // abstract + const indexOfTheNewElement = Math.min(Model.length, this.index) + Model.splice(indexOfTheNewElement, 0, this.value) + // real + const buttonEl = DOM.getByText('Insert fruit') + TestUtils.Simulate.keyPress(buttonEl, { + which: this.index, + key: this.value + }) + await waitForFormToRerender() + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } +} - const genericModelRun = async (setup, commands, initialValue, then) => { - const { model, real } = await setup() - let state = initialValue - for (const c of commands) { - state = then(state, () => { - if (c.check(model)) return c.run(model, real) - }) - } - return state - } +const commands = { + AddField, + ChangeValue, + Move, + Insert +} - const asyncModelRun = (setup, commands) => { - const then = (p, c) => p.then(c) - return genericModelRun(setup, commands, Promise.resolve(), then) - } +const genericModelRun = async (setup, commands, initialValue, then) => { + const { model, real } = await setup() + let state = initialValue + for (const c of commands) { + state = then(state, () => { + if (c.check(model)) return c.run(model, real) + }) + } + return state +} +const asyncModelRun = (setup, commands) => { + const then = (p, c) => p.then(c) + return genericModelRun(setup, commands, Promise.resolve(), then) +} + +const generateCommands = [ + commands.AddField.generate(), + commands.ChangeValue.generate(), + commands.Move.generate(), + commands.Insert.generate() +] + +const getInitialState = async () => { + const { Model, DOM } = await setup() + return { + model: Model, + real: DOM + } +} + +describe('FieldArray', () => { + it('should work', async () => { await fc.assert( fc - .asyncProperty( - fc.commands([ - fc.constant(new commands.AddField()), - fc - .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) - .map(args => new commands.ChangeValue(...args)), - fc - .tuple( - fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), - fc.nat(INITIAL_NUMBER_OF_FIELDS * 2) - ) - .map(args => new commands.Move(...args)) - ]), - async commands => { - console.log(commands) - const getInitialState = async () => { - const { Model, DOM } = await setup() - return { - model: Model, - real: DOM - } - } - await asyncModelRun(getInitialState, commands) - } - ) + .asyncProperty(fc.commands(generateCommands), async commands => { + await asyncModelRun(getInitialState, commands) + }) .afterEach(cleanup), { numRuns: 100, @@ -191,7 +240,8 @@ describe('FieldArray', () => { // seed: 1842107356, examples: [ // https://github.com/final-form/final-form-arrays/issues/15#issuecomment-442126496 - [[new commands.Move(1, 0), new commands.ChangeValue(0, 'apple')]] + // [[new commands.Move(1, 0), new commands.ChangeValue(0, 'apple')]] + // form is not pristine after inserting ] } ) From 20d0b58519d71132e9ba01ca915a3eddd200a4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 09:45:33 +0100 Subject: [PATCH 08/27] Add Pop command --- src/FieldArray.property.test.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 0a2c129..920faad 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -15,7 +15,7 @@ const setup = async () => { {({ form: { - mutators: { push, move, insert } + mutators: { push, move, insert, pop } } }) => { return ( @@ -51,6 +51,7 @@ const setup = async () => { > Insert fruit + ) }} @@ -187,11 +188,31 @@ class Insert { } } +class Pop { + static generate = () => fc.constant(new commands.Pop()) + toString = () => 'removing the last element' + check = () => true + run = async (Model, DOM) => { + // abstract + Model.pop() + + // real + const buttonEl = DOM.getByText('Remove the last fruit') + fireEvent.click(buttonEl) + await waitForFormToRerender() + + // postconditions + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + } +} + const commands = { AddField, ChangeValue, Move, - Insert + Insert, + Pop } const genericModelRun = async (setup, commands, initialValue, then) => { @@ -214,7 +235,8 @@ const generateCommands = [ commands.AddField.generate(), commands.ChangeValue.generate(), commands.Move.generate(), - commands.Insert.generate() + commands.Insert.generate(), + commands.Pop.generate() ] const getInitialState = async () => { From f4ea6fbc9b420edc149f9c1a88357ad1ab172f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 18:11:21 +0100 Subject: [PATCH 09/27] prepare model structure to hold field state --- src/FieldArray.property.test.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 920faad..6911c91 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -63,7 +63,7 @@ const setup = async () => { const buttonEl = DOM.getByText('Add fruit') ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { fireEvent.click(buttonEl) - Model.push('') + Model.push({ value: '' }) }) await waitForFormToRerender() return { DOM, Model } @@ -78,7 +78,8 @@ const correctNumberOfInputs = (Model, DOM) => { const correctValues = (Model, DOM) => { const inputElements = selectAllInputs(DOM) const realValues = [...inputElements.values()].map(element => element.value) - expect(realValues).toEqual(Model) + const modelValues = Model.map(fieldState => fieldState.value) + expect(realValues).toEqual(modelValues) } class AddField { @@ -87,7 +88,7 @@ class AddField { check = () => true run = async (Model, DOM) => { // abstract - Model.push('') + Model.push({ value: '' }) // real const buttonEl = DOM.getByText('Add fruit') fireEvent.click(buttonEl) @@ -114,7 +115,7 @@ class ChangeValue { } run = (Model, DOM) => { // abstract - Model[this.index] = this.newValue + Model[this.index] = { value: this.newValue } // real const label = `Fruit ${this.index + 1} name` const inputEl = DOM.getByLabelText(label) @@ -174,7 +175,7 @@ class Insert { run = async (Model, DOM) => { // abstract const indexOfTheNewElement = Math.min(Model.length, this.index) - Model.splice(indexOfTheNewElement, 0, this.value) + Model.splice(indexOfTheNewElement, 0, { value: this.value }) // real const buttonEl = DOM.getByText('Insert fruit') TestUtils.Simulate.keyPress(buttonEl, { From 4fde29f76ff47a2f4289dbfc0966b9ee6362d30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 18:33:25 +0100 Subject: [PATCH 10/27] Storie field meta as data attributes --- src/FieldArray.property.test.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 6911c91..633001d 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -10,7 +10,20 @@ const nope = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForFormToRerender = () => sleep(0) const INITIAL_NUMBER_OF_FIELDS = 2 +const getDefaultFieldState = () => ({ + value: '', + dirty: false, + touched: false +}) const setup = async () => { + const Input = ({ input, meta, ...restProps }) => { + const dataAttrs = { + 'data-dirty': meta.dirty, + 'data-touched': meta.touched + } + return + } + const DOM = render( {({ @@ -25,7 +38,7 @@ const setup = async () => { return fields.map((name, index) => ( - + )) }} @@ -63,7 +76,7 @@ const setup = async () => { const buttonEl = DOM.getByText('Add fruit') ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { fireEvent.click(buttonEl) - Model.push({ value: '' }) + Model.push(getDefaultFieldState()) }) await waitForFormToRerender() return { DOM, Model } @@ -88,7 +101,7 @@ class AddField { check = () => true run = async (Model, DOM) => { // abstract - Model.push({ value: '' }) + Model.push(getDefaultFieldState()) // real const buttonEl = DOM.getByText('Add fruit') fireEvent.click(buttonEl) @@ -115,7 +128,7 @@ class ChangeValue { } run = (Model, DOM) => { // abstract - Model[this.index] = { value: this.newValue } + Model[this.index].value = this.newValue // real const label = `Fruit ${this.index + 1} name` const inputEl = DOM.getByLabelText(label) @@ -175,7 +188,10 @@ class Insert { run = async (Model, DOM) => { // abstract const indexOfTheNewElement = Math.min(Model.length, this.index) - Model.splice(indexOfTheNewElement, 0, { value: this.value }) + Model.splice(indexOfTheNewElement, 0, { + ...getDefaultFieldState(), + value: this.value + }) // real const buttonEl = DOM.getByText('Insert fruit') TestUtils.Simulate.keyPress(buttonEl, { @@ -235,7 +251,7 @@ const asyncModelRun = (setup, commands) => { const generateCommands = [ commands.AddField.generate(), commands.ChangeValue.generate(), - commands.Move.generate(), + // commands.Move.generate(), commands.Insert.generate(), commands.Pop.generate() ] From fd2bf28666a7f0603ca0d4e544daec2afd7ecc11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 18:44:04 +0100 Subject: [PATCH 11/27] Update fast-check version --- package.json | 2 +- src/FieldArray.property.test.js | 18 +--- src/FieldArray.test.js | 165 -------------------------------- 3 files changed, 2 insertions(+), 183 deletions(-) diff --git a/package.json b/package.json index c57c4a0..bf77e77 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-react": "^7.11.1", - "fast-check": "^1.8.0", + "fast-check": "^1.8.1", "final-form": "^4.10.0", "final-form-arrays": "^1.1.0", "flow-bin": "^0.83.0", diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 633001d..26bec60 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -232,22 +232,6 @@ const commands = { Pop } -const genericModelRun = async (setup, commands, initialValue, then) => { - const { model, real } = await setup() - let state = initialValue - for (const c of commands) { - state = then(state, () => { - if (c.check(model)) return c.run(model, real) - }) - } - return state -} - -const asyncModelRun = (setup, commands) => { - const then = (p, c) => p.then(c) - return genericModelRun(setup, commands, Promise.resolve(), then) -} - const generateCommands = [ commands.AddField.generate(), commands.ChangeValue.generate(), @@ -269,7 +253,7 @@ describe('FieldArray', () => { await fc.assert( fc .asyncProperty(fc.commands(generateCommands), async commands => { - await asyncModelRun(getInitialState, commands) + await fc.asyncModelRun(getInitialState, commands) }) .afterEach(cleanup), { diff --git a/src/FieldArray.test.js b/src/FieldArray.test.js index 334c6c3..90e3274 100644 --- a/src/FieldArray.test.js +++ b/src/FieldArray.test.js @@ -1,12 +1,5 @@ import React, { Fragment } from 'react' import TestUtils from 'react-dom/test-utils' -import { - render, - fireEvent, - cleanup, - waitForElement -} from 'react-testing-library' -import fc from 'fast-check' import { Form, Field } from 'react-final-form' import arrayMutators from 'final-form-arrays' import FieldArray from './FieldArray' @@ -611,164 +604,6 @@ describe('FieldArray', () => { }) }) -describe('FieldArray properties', () => { - it.only('should work', async () => { - function ChangeCommand(index, newValue) { - return { - check: model => { - console.log('I run check', model, index) - // return true; - return model.values.length >= index - }, - run: (model, real) => { - // abstract phase - model.values[index] = newValue - - // real phase - const inputEl = real.getByTestId(`input${index}`) - fireEvent.focus(inputEl) - fireEvent.change(inputEl, { target: { value: newValue } }) - fireEvent.blur(inputEl) - }, - toString: () => `change input value on index ${index} to ${newValue}` - } - } - - function AddFieldCommand() { - return { - check: () => true, - run: async (model, real) => { - model.push('') - - const addButton = real.getByText('add a new field') - fireEvent.click(addButton) - - const numberOfElementsInModel = model.length - await waitForElement(() => - real.getByTestId(`input${numberOfElementsInModel - 1}`) - ) - - const allInputElements = real.queryAllByTestId('input', { - exact: false - }) - const realValues = allInputElements.map(element => element.value) - // console.log(realValues); - - real.debug() - expect(allInputElements.length).toBe(numberOfElementsInModel) - }, - toString: () => 'Add a new field' - } - } - - const allCommands = [ - fc.constant(new AddFieldCommand()), - fc.nat(3).map(nat => { - // console.log(nat) - return new ChangeCommand(nat, 'abc') - }) - ] - - const generateRealSystem = () => - render( - ( - - - {({ fields }) => ( -
- {fields.map((name, i) => ( - - ))} -
- )} -
- -
- )} - /> - ) - - const realSystem = generateRealSystem() - - const generateState = () => ({ - model: [''], - real: generateRealSystem() - }) - - // const fieldCommand = new AddFieldCommand() - // await fieldCommand.run([], realSystem) - - // const result = fc.sample(allCommands[1]) - // console.log(result) - - await fc.assert( - fc.asyncProperty(fc.commands(allCommands, 6), async commands => { - await fc.asyncModelRun(generateState, commands) - console.log('I am cleaning up') - cleanup() - }), - { numRuns: 2 } - ) - }) -}) - -const generateInitialState = () => { - // generate random values for random number of inputs -} - -const initialState = generateInitialState() - -let model = { - state: initialState, - operations: { - handleChange: (state, index, newValue) => { - const newState = { ...state } - newState[index] = newValue - return newState - }, - handleMove: (state, from, to) => { - if (from === to) return { ...state } - const valueToMove = state[from] - const stateWithoutElement = [ - ...state.slice(0, from), - ...state.slice(from + 1) - ] - const newState = [ - ...stateWithoutElement.slice(0, to), - valueToMove, - ...stateWithoutElement.slice(to) - ] - return newState - } - } -} - -let avaliableCommands = { - change: { - preconditions: state => {}, - valuesGenerator: () => {}, - postconditions: state => {} - }, - move: { - preconditions: state => {}, - valuesGenerator: () => {}, - postconditions: state => {} - } -} - // sources // https://propertesting.com/book_stateful_properties.html // https://hypothesis.readthedocs.io/en/latest/stateful.html From 23c967aa6c827d83b23ed2afe14f524e61e4f6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 19:32:54 +0100 Subject: [PATCH 12/27] Check if metadata is correct --- src/FieldArray.property.test.js | 67 +++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 26bec60..525e97d 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -12,13 +12,13 @@ const waitForFormToRerender = () => sleep(0) const INITIAL_NUMBER_OF_FIELDS = 2 const getDefaultFieldState = () => ({ value: '', - dirty: false, + pristine: true, touched: false }) const setup = async () => { const Input = ({ input, meta, ...restProps }) => { const dataAttrs = { - 'data-dirty': meta.dirty, + 'data-pristine': meta.pristine, 'data-touched': meta.touched } return @@ -95,6 +95,35 @@ const correctValues = (Model, DOM) => { expect(realValues).toEqual(modelValues) } +const correctMetadata = (Model, DOM) => { + const inputElements = selectAllInputs(DOM) + const realMetadata = [...inputElements].map( + ({ dataset: { pristine, touched } }) => ({ + pristine, + touched + }) + ) + const modelMetadata = Model.map( + ({ value, ...fieldMetadata }) => fieldMetadata + ).map(fieldMetadata => { + // data attributes in DOM are string + // so transform these bools to strings + // for comparison purposes + let modifiedObject = {} + Object.keys(fieldMetadata).forEach(property => { + modifiedObject[property] = String(fieldMetadata[property]) + }) + return modifiedObject + }) + expect(realMetadata).toEqual(modelMetadata) +} + +const validateAttributes = (Model, DOM) => { + correctNumberOfInputs(Model, DOM) + correctValues(Model, DOM) + correctMetadata(Model, DOM) +} + class AddField { static generate = () => fc.constant(new commands.AddField()) toString = () => 'add field' @@ -107,8 +136,7 @@ class AddField { fireEvent.click(buttonEl) await waitForFormToRerender() // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) + validateAttributes(Model, DOM) } } @@ -128,14 +156,22 @@ class ChangeValue { } run = (Model, DOM) => { // abstract - Model[this.index].value = this.newValue + const DEFAULT_FIELD_STATE = getDefaultFieldState() + const pristine = this.newValue === DEFAULT_FIELD_STATE.value + Model[this.index] = { + ...DEFAULT_FIELD_STATE, + value: this.newValue, + touched: true, + pristine + } // real const label = `Fruit ${this.index + 1} name` const inputEl = DOM.getByLabelText(label) + fireEvent.focus(inputEl) fireEvent.change(inputEl, { target: { value: this.newValue } }) + fireEvent.blur(inputEl) // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) + validateAttributes(Model, DOM) } } @@ -169,8 +205,7 @@ class Move { }) await waitForFormToRerender() // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) + validateAttributes(Model, DOM) } } @@ -190,7 +225,8 @@ class Insert { const indexOfTheNewElement = Math.min(Model.length, this.index) Model.splice(indexOfTheNewElement, 0, { ...getDefaultFieldState(), - value: this.value + value: this.value, + pristine: this.value === undefined }) // real const buttonEl = DOM.getByText('Insert fruit') @@ -200,8 +236,7 @@ class Insert { }) await waitForFormToRerender() // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) + validateAttributes(Model, DOM) } } @@ -219,8 +254,7 @@ class Pop { await waitForFormToRerender() // postconditions - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) + validateAttributes(Model, DOM) } } @@ -235,7 +269,7 @@ const commands = { const generateCommands = [ commands.AddField.generate(), commands.ChangeValue.generate(), - // commands.Move.generate(), + commands.Move.generate(), commands.Insert.generate(), commands.Pop.generate() ] @@ -261,10 +295,11 @@ describe('FieldArray', () => { verbose: true, // seed: 1842023377, // seed: 1842107356, + // seed: 1881850827, + // seed: 1882099238, examples: [ // https://github.com/final-form/final-form-arrays/issues/15#issuecomment-442126496 // [[new commands.Move(1, 0), new commands.ChangeValue(0, 'apple')]] - // form is not pristine after inserting ] } ) From a4c360156362f73dc7bbfffa206d5b0ec14d6aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 19:50:43 +0100 Subject: [PATCH 13/27] Add push command --- src/FieldArray.property.test.js | 47 +++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 525e97d..045c492 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -28,6 +28,7 @@ const setup = async () => { {({ form: { + mutators, mutators: { push, move, insert, pop } } }) => { @@ -65,6 +66,13 @@ const setup = async () => { Insert fruit + ) }} @@ -223,10 +231,11 @@ class Insert { run = async (Model, DOM) => { // abstract const indexOfTheNewElement = Math.min(Model.length, this.index) + const DEFAULT_FIELD_STATE = getDefaultFieldState() Model.splice(indexOfTheNewElement, 0, { - ...getDefaultFieldState(), + ...DEFAULT_FIELD_STATE, value: this.value, - pristine: this.value === undefined + pristine: this.value === DEFAULT_FIELD_STATE.value }) // real const buttonEl = DOM.getByText('Insert fruit') @@ -258,20 +267,48 @@ class Pop { } } +class Push { + constructor(value) { + this.value = value + } + static generate = () => fc.string().map(value => new commands.Push(value)) + toString = () => `push a new field with value ${this.value}` + check = () => true + run = async (Model, DOM) => { + // abstract + Model.push({ + ...getDefaultFieldState(), + value: this.value, + pristine: this.value === getDefaultFieldState().value + }) + + // real + const buttonEl = DOM.getByText('Push fruit') + TestUtils.Simulate.keyPress(buttonEl, { + key: this.value + }) + await waitForFormToRerender() + // postconditions + validateAttributes(Model, DOM) + } +} + const commands = { AddField, ChangeValue, Move, Insert, - Pop + Pop, + Push } const generateCommands = [ commands.AddField.generate(), commands.ChangeValue.generate(), - commands.Move.generate(), - commands.Insert.generate(), + // commands.Move.generate(), + // commands.Insert.generate(), commands.Pop.generate() + // commands.Push.generate() ] const getInitialState = async () => { From 9bdef17540dda1c6a7c7f84f8d97aa571de641ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 19:55:42 +0100 Subject: [PATCH 14/27] add remove command --- src/FieldArray.property.test.js | 43 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 045c492..ba9fc64 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -29,7 +29,7 @@ const setup = async () => { {({ form: { mutators, - mutators: { push, move, insert, pop } + mutators: { push, move, insert, pop, remove } } }) => { return ( @@ -73,6 +73,13 @@ const setup = async () => { > Push fruit + ) }} @@ -293,13 +300,42 @@ class Push { } } +class Remove { + constructor(index) { + this.index = index + } + static generate = () => + fc + .nat(INITIAL_NUMBER_OF_FIELDS * 2) + .map(index => new commands.Remove(index)) + toString = () => `remove a field from index ${this.index}` + check = Model => { + if (Model.length >= this.index) return false + return true + } + run = async (Model, DOM) => { + // abstract + Model.splice(this.index, 1) + + // real + const buttonEl = DOM.getByText('Remove fruit') + TestUtils.Simulate.keyPress(buttonEl, { + which: this.index + }) + await waitForFormToRerender() + // postconditions + validateAttributes(Model, DOM) + } +} + const commands = { AddField, ChangeValue, Move, Insert, Pop, - Push + Push, + Remove } const generateCommands = [ @@ -307,8 +343,9 @@ const generateCommands = [ commands.ChangeValue.generate(), // commands.Move.generate(), // commands.Insert.generate(), - commands.Pop.generate() + commands.Pop.generate(), // commands.Push.generate() + commands.Remove.generate() ] const getInitialState = async () => { From afcc1f7ac4ab5d7b8b60224aaffcdd7be83a0d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 20:00:19 +0100 Subject: [PATCH 15/27] Add shift command --- src/FieldArray.property.test.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index ba9fc64..c53cbc6 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -29,7 +29,7 @@ const setup = async () => { {({ form: { mutators, - mutators: { push, move, insert, pop, remove } + mutators: { push, move, insert, pop, remove, shift } } }) => { return ( @@ -80,6 +80,7 @@ const setup = async () => { > Remove fruit + ) }} @@ -328,6 +329,23 @@ class Remove { } } +class Shift { + static generate = () => fc.constant(new commands.Shift()) + toString = () => `shift()` + check = () => true + run = async (Model, DOM) => { + // abstract + Model.shift() + + // real + const buttonEl = DOM.getByText('Shift fruit') + fireEvent.click(buttonEl) + await waitForFormToRerender() + // postconditions + validateAttributes(Model, DOM) + } +} + const commands = { AddField, ChangeValue, @@ -335,7 +353,8 @@ const commands = { Insert, Pop, Push, - Remove + Remove, + Shift } const generateCommands = [ @@ -346,6 +365,7 @@ const generateCommands = [ commands.Pop.generate(), // commands.Push.generate() commands.Remove.generate() + // commands.Shift.generate() ] const getInitialState = async () => { From 6bf157b3ef42aaeea8d447f406fba76f5ed8122d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 20:09:52 +0100 Subject: [PATCH 16/27] Remove AddField command, improve command names --- src/FieldArray.property.test.js | 85 ++++++++++----------------------- 1 file changed, 24 insertions(+), 61 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index c53cbc6..d350b73 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -44,13 +44,6 @@ const setup = async () => { )) }} - + ) }} @@ -321,14 +328,51 @@ class Shift { } } +class Swap { + constructor(a, b) { + this.a = a + this.b = b + } + static generate = () => + fc + .tuple( + fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), + fc.nat(INITIAL_NUMBER_OF_FIELDS * 2) + ) + .map(args => new Swap(...args)) + toString = () => ` swap(${this.a}, ${this.b})` + check = Model => { + if (this.a >= Model.length || this.b >= Model.length) return false + return true + } + run = async (Model, DOM) => { + // abstract + const cache = Model[this.a] + Model[this.a] = Model[this.b] + Model[this.b] = cache + // real + const buttonEl = DOM.getByText('Swap fruits') + TestUtils.Simulate.keyPress(buttonEl, { + which: this.a, + location: this.b + }) + await waitForFormToRerender() + // postconditions + validateAttributes(Model, DOM) + } +} + const generateCommands = [ ChangeValue.generate(), - // Move.generate(), // Insert.generate(), + // Move.generate(), Pop.generate(), // Push.generate() Remove.generate() // Shift.generate() + // Swap.generate() + // Update.generate() + // Unshift.generate() ] const getInitialState = async () => { From fad5118c69753a411b06b5a76c0c5528fd000e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 21:47:49 +0100 Subject: [PATCH 18/27] simplify postconditions --- src/FieldArray.property.test.js | 39 ++++++--------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 012a910..bf66868 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -99,45 +99,20 @@ const setup = async () => { } const selectAllInputs = DOM => DOM.container.querySelectorAll('input') -const correctNumberOfInputs = (Model, DOM) => { - const inputElements = selectAllInputs(DOM) - expect(inputElements.length).toBe(Model.length) -} - -const correctValues = (Model, DOM) => { - const inputElements = selectAllInputs(DOM) - const realValues = [...inputElements.values()].map(element => element.value) - const modelValues = Model.map(fieldState => fieldState.value) - expect(realValues).toEqual(modelValues) -} - -const correctMetadata = (Model, DOM) => { +const realMatchesModel = (Model, DOM) => { const inputElements = selectAllInputs(DOM) const realMetadata = [...inputElements].map( - ({ dataset: { pristine, touched } }) => ({ - pristine, - touched + ({ value, dataset: { pristine, touched } }) => ({ + value, + pristine: pristine === 'true', + touched: touched === 'true' }) ) - const modelMetadata = Model.map( - ({ value, ...fieldMetadata }) => fieldMetadata - ).map(fieldMetadata => { - // data attributes in DOM are string - // so transform these bools to strings - // for comparison purposes - let modifiedObject = {} - Object.keys(fieldMetadata).forEach(property => { - modifiedObject[property] = String(fieldMetadata[property]) - }) - return modifiedObject - }) - expect(realMetadata).toEqual(modelMetadata) + expect(realMetadata).toEqual(Model) } const validateAttributes = (Model, DOM) => { - correctNumberOfInputs(Model, DOM) - correctValues(Model, DOM) - correctMetadata(Model, DOM) + realMatchesModel(Model, DOM) } class ChangeValue { From 696b98c50b8b67f39d301bbd15ad3575f6012368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 22:37:23 +0100 Subject: [PATCH 19/27] Add update command, refactor events --- src/FieldArray.property.test.js | 131 +++++++++++++++++++------------- 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index bf66868..e52d4b3 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -10,11 +10,10 @@ const nope = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForFormToRerender = () => sleep(0) const INITIAL_NUMBER_OF_FIELDS = 2 -const getDefaultFieldState = () => ({ - value: '', - pristine: true, - touched: false -}) +const getFieldState = ({ value = '', touched = false } = {}) => { + const pristine = value === '' + return { value: value || '', pristine, touched } +} const setup = async () => { const Input = ({ input, meta, ...restProps }) => { const dataAttrs = { @@ -29,7 +28,7 @@ const setup = async () => { {({ form: { mutators, - mutators: { push, move, insert, pop, remove, shift, swap } + mutators: { push, move, insert, pop, remove, shift, swap, update } } }) => { return ( @@ -45,42 +44,50 @@ const setup = async () => { }} + ) }} @@ -91,8 +98,8 @@ const setup = async () => { const buttonEl = DOM.getByText('Push fruit') ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { - TestUtils.Simulate.keyPress(buttonEl) - Model.push(getDefaultFieldState()) + TestUtils.Simulate.click(buttonEl) + Model.push(getFieldState()) }) await waitForFormToRerender() return { DOM, Model } @@ -131,14 +138,8 @@ class ChangeValue { } run = (Model, DOM) => { // abstract - const DEFAULT_FIELD_STATE = getDefaultFieldState() - const pristine = this.newValue === DEFAULT_FIELD_STATE.value - Model[this.index] = { - ...DEFAULT_FIELD_STATE, - value: this.newValue, - touched: true, - pristine - } + Model[this.index] = getFieldState({ value: this.newValue, touched: true }) + // real const label = `Fruit ${this.index + 1} name` const inputEl = DOM.getByLabelText(label) @@ -174,9 +175,9 @@ class Move { Model.splice(this.to, 0, cache) // real const buttonEl = DOM.getByText('Move fruit') - TestUtils.Simulate.keyPress(buttonEl, { - which: this.from, - location: this.to + TestUtils.Simulate.click(buttonEl, { + from: this.from, + to: this.to }) await waitForFormToRerender() // postconditions @@ -198,17 +199,13 @@ class Insert { run = async (Model, DOM) => { // abstract const indexOfTheNewElement = Math.min(Model.length, this.index) - const DEFAULT_FIELD_STATE = getDefaultFieldState() - Model.splice(indexOfTheNewElement, 0, { - ...DEFAULT_FIELD_STATE, - value: this.value, - pristine: this.value === DEFAULT_FIELD_STATE.value - }) + Model.splice(indexOfTheNewElement, 0, getFieldState({ value: this.value })) + // real const buttonEl = DOM.getByText('Insert fruit') - TestUtils.Simulate.keyPress(buttonEl, { - which: this.index, - key: this.value + TestUtils.Simulate.click(buttonEl, { + index: this.index, + value: this.value }) await waitForFormToRerender() // postconditions @@ -243,16 +240,12 @@ class Push { check = () => true run = async (Model, DOM) => { // abstract - Model.push({ - ...getDefaultFieldState(), - value: this.value, - pristine: this.value === getDefaultFieldState().value - }) + Model.push(getFieldState({ value: this.value })) // real const buttonEl = DOM.getByText('Push fruit') - TestUtils.Simulate.keyPress(buttonEl, { - key: this.value + TestUtils.Simulate.click(buttonEl, { + value: this.value }) await waitForFormToRerender() // postconditions @@ -277,8 +270,8 @@ class Remove { // real const buttonEl = DOM.getByText('Remove fruit') - TestUtils.Simulate.keyPress(buttonEl, { - which: this.index + TestUtils.Simulate.click(buttonEl, { + index: this.index }) await waitForFormToRerender() // postconditions @@ -327,9 +320,39 @@ class Swap { Model[this.b] = cache // real const buttonEl = DOM.getByText('Swap fruits') - TestUtils.Simulate.keyPress(buttonEl, { - which: this.a, - location: this.b + TestUtils.Simulate.click(buttonEl, { + a: this.a, + b: this.b + }) + await waitForFormToRerender() + // postconditions + validateAttributes(Model, DOM) + } +} + +class Update { + constructor(index, newValue) { + this.index = index + this.newValue = newValue + } + static generate = () => + fc + .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) + .map(args => new Update(...args)) + toString = () => ` update(${this.index}, ${this.newValue})` + check = Model => { + if (this.index >= Model.length) return false + return true + } + run = async (Model, DOM) => { + // abstract + Model[this.index] = getFieldState({ value: this.newValue, touched: true }) + + // real + const buttonEl = DOM.getByText('Update fruit') + TestUtils.Simulate.click(buttonEl, { + index: this.index, + value: this.newValue }) await waitForFormToRerender() // postconditions @@ -339,10 +362,10 @@ class Swap { const generateCommands = [ ChangeValue.generate(), - // Insert.generate(), + // Insert.generate() // Move.generate(), Pop.generate(), - // Push.generate() + // Push.generate(), Remove.generate() // Shift.generate() // Swap.generate() From 72347c04c26ab0eaafbf74acc714b4d7e7bf5e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 22:44:42 +0100 Subject: [PATCH 20/27] Implement unshift command --- src/FieldArray.property.test.js | 56 ++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index e52d4b3..cd39639 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -28,7 +28,17 @@ const setup = async () => { {({ form: { mutators, - mutators: { push, move, insert, pop, remove, shift, swap, update } + mutators: { + push, + move, + insert, + pop, + remove, + shift, + swap, + update, + unshift + } } }) => { return ( @@ -88,6 +98,13 @@ const setup = async () => { > Update fruit + ) }} @@ -131,7 +148,7 @@ class ChangeValue { fc .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) .map(args => new ChangeValue(...args)) - toString = () => ` change value at ${this.index} to ${this.newValue}` + toString = () => ` change value at ${this.index} to '${this.newValue}'` check = Model => { if (this.index >= Model.length) return false return true @@ -194,7 +211,7 @@ class Insert { fc .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) .map(args => new Insert(...args)) - toString = () => ` insert(${this.index}, ${this.value})` + toString = () => ` insert(${this.index}, '${this.value}')` check = () => true run = async (Model, DOM) => { // abstract @@ -236,7 +253,7 @@ class Push { this.value = value } static generate = () => fc.option(fc.string()).map(value => new Push(value)) - toString = () => ` push(${this.value})` + toString = () => ` push('${this.value}')` check = () => true run = async (Model, DOM) => { // abstract @@ -339,7 +356,7 @@ class Update { fc .tuple(fc.nat(INITIAL_NUMBER_OF_FIELDS * 2), fc.string()) .map(args => new Update(...args)) - toString = () => ` update(${this.index}, ${this.newValue})` + toString = () => ` update(${this.index}, '${this.newValue}')` check = Model => { if (this.index >= Model.length) return false return true @@ -360,6 +377,29 @@ class Update { } } +class Unshift { + constructor(value) { + this.value = value + } + static generate = () => + fc.option(fc.string()).map(value => new Unshift(value)) + toString = () => ` unshift('${this.value}')` + check = () => true + run = async (Model, DOM) => { + // abstract + Model.unshift(getFieldState({ value: this.value })) + + // real + const buttonEl = DOM.getByText('Unshift fruit') + TestUtils.Simulate.click(buttonEl, { + value: this.value + }) + await waitForFormToRerender() + // postconditions + validateAttributes(Model, DOM) + } +} + const generateCommands = [ ChangeValue.generate(), // Insert.generate() @@ -367,9 +407,9 @@ const generateCommands = [ Pop.generate(), // Push.generate(), Remove.generate() - // Shift.generate() - // Swap.generate() - // Update.generate() + // Shift.generate(), + // Swap.generate(), + // Update.generate(), // Unshift.generate() ] From 588753243446ed63dbf6145f284647abc624a1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Sun, 2 Dec 2018 22:53:24 +0100 Subject: [PATCH 21/27] Simplify setup function --- src/FieldArray.property.test.js | 140 ++++++++++++++------------------ 1 file changed, 63 insertions(+), 77 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index cd39639..715ebae 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -25,87 +25,73 @@ const setup = async () => { const DOM = render( - {({ - form: { - mutators, - mutators: { - push, - move, - insert, - pop, - remove, - shift, - swap, - update, - unshift - } - } - }) => { + {() => { return ( - - - {({ fields }) => { - return fields.map((name, index) => ( + + {({ fields }) => ( + + {fields.map((name, index) => ( - )) - }} - - - - - - - - - - - + ))} + + + + + + + + + + + )} + ) }} @@ -438,7 +424,7 @@ describe('FieldArray', () => { // seed: 1882099238, examples: [ // https://github.com/final-form/final-form-arrays/issues/15#issuecomment-442126496 - // [[new Move(1, 0), new ChangeValue(0, 'apple')]] + // https://github.com/final-form/react-final-form-arrays lacks `update` mutator documentation ] } ) From bdac0a2cdab55fd84705e171debb0c455eeac36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Mon, 3 Dec 2018 05:41:56 +0100 Subject: [PATCH 22/27] Generate initial values --- src/FieldArray.property.test.js | 63 +++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 715ebae..861b2c1 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -10,11 +10,15 @@ const nope = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForFormToRerender = () => sleep(0) const INITIAL_NUMBER_OF_FIELDS = 2 -const getFieldState = ({ value = '', touched = false } = {}) => { - const pristine = value === '' - return { value: value || '', pristine, touched } +const getFieldState = ({ + value = '', + touched = false, + initialValue = '' +} = {}) => { + const pristine = value === initialValue + return { value: value || '', pristine, touched, initialValue } } -const setup = async () => { +const setup = async ({ initialValues }) => { const Input = ({ input, meta, ...restProps }) => { const dataAttrs = { 'data-pristine': meta.pristine, @@ -24,7 +28,11 @@ const setup = async () => { } const DOM = render( -
+ {() => { return ( @@ -97,14 +105,17 @@ const setup = async () => { ) - const Model = [] + const Model = (initialValues.fruits || []).map(value => + getFieldState({ value, initialValue: value }) + ) + // console.log(Model) - const buttonEl = DOM.getByText('Push fruit') - ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { - TestUtils.Simulate.click(buttonEl) - Model.push(getFieldState()) - }) - await waitForFormToRerender() + // const buttonEl = DOM.getByText('Push fruit') + // ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { + // TestUtils.Simulate.click(buttonEl) + // Model.push(getFieldState()) + // }) + // await waitForFormToRerender() return { DOM, Model } } const selectAllInputs = DOM => DOM.container.querySelectorAll('input') @@ -118,7 +129,10 @@ const realMatchesModel = (Model, DOM) => { touched: touched === 'true' }) ) - expect(realMetadata).toEqual(Model) + const formattedModelData = Model.map( + ({ initialValue, ...restData }) => restData + ) + expect(realMetadata).toEqual(formattedModelData) } const validateAttributes = (Model, DOM) => { @@ -141,7 +155,11 @@ class ChangeValue { } run = (Model, DOM) => { // abstract - Model[this.index] = getFieldState({ value: this.newValue, touched: true }) + Model[this.index] = getFieldState({ + value: this.newValue, + touched: true, + initialValue: Model[this.index].initialValue + }) // real const label = `Fruit ${this.index + 1} name` @@ -399,21 +417,28 @@ const generateCommands = [ // Unshift.generate() ] -const getInitialState = async () => { - const { Model, DOM } = await setup() +const getInitialState = initialValues => async () => { + const { Model, DOM } = await setup({ initialValues }) return { model: Model, real: DOM } } +const initialValues = fc.record({ fruits: fc.option(fc.array(fc.string())) }) + describe('FieldArray', () => { it('should work', async () => { await fc.assert( fc - .asyncProperty(fc.commands(generateCommands), async commands => { - await fc.asyncModelRun(getInitialState, commands) - }) + .asyncProperty( + fc.commands(generateCommands), + initialValues, + async (commands, initialValues) => { + const stateBuilder = getInitialState(initialValues) + await fc.asyncModelRun(stateBuilder, commands) + } + ) .afterEach(cleanup), { numRuns: 100, From 40463c6dd068106159ad93950ec62d4ee05af5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Mon, 3 Dec 2018 06:54:55 +0100 Subject: [PATCH 23/27] Fix the way pristine is calculated --- src/FieldArray.property.test.js | 171 ++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 41 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 861b2c1..726d532 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -18,6 +18,112 @@ const getFieldState = ({ const pristine = value === initialValue return { value: value || '', pristine, touched, initialValue } } +const modifyFieldState = ({ Model, index, newState = {} }) => { + const initialValueForThisField = Model.initialValues[index] + const value = newState.value === undefined ? '' : newState.value + const pristine = value === initialValueForThisField + const touched = newState.touched || false + Model.fields[index] = { + value, + pristine, + touched + } + Model.initialValues.forEach((initialValue, index) => { + const field = Model.fields[index] + field.pristine = field.value === initialValue + }) +} + +class ModelConstruct { + constructor(initialValues) { + this.state = { + initialValues: initialValues || [], + fields: (initialValues || []).map(value => ({ + value, + touched: false, + pristine: true + })) + } + } + + getFieldsState() { + return this.state.fields + } + + getFieldsLength() { + return this.state.fields.length + } + + recalculatePristine() { + ;(this.state.fields || []).forEach((fieldState, index) => { + const initialValue = this.state.initialValues[index] || '' + fieldState.pristine = fieldState.value === initialValue + }) + } + + changeValue(index, newValue) { + this.state.fields[index] = { + value: newValue, + touched: true + } + this.recalculatePristine() + } + + insert(index, value) { + const indexOfTheNewElement = Math.min(this.state.fields.length, index) + this.state.fields.splice(indexOfTheNewElement, 0, { + value, + touched: false + }) + this.recalculatePristine() + } + + move(from, to) { + const cache = this.state.fields[from] + this.state.fields.splice(from, 1) + this.state.fields.splice(to, 0, cache) + this.recalculatePristine() + } + + pop() { + this.state.fields.pop() + this.recalculatePristine() + } + + push(value) { + this.state.fields.push({ value: value || '', touched: false }) + this.recalculatePristine() + } + + remove(index) { + this.state.fields.splice(index, 1) + this.recalculatePristine() + } + + shift() { + this.state.fields.shift() + this.recalculatePristine() + } + + swap(a, b) { + const cache = this.state.fields[a] + this.state.fields[a] = this.state.fields[b] + this.state.fields[b] = cache + this.recalculatePristine() + } + + update(index, newValue) { + const field = this.state.fields[index] + this.state.fields[index] = { value: newValue, touched: field.touched } + this.recalculatePristine() + } + + unshift(value) { + this.state.fields.unshift({ value, touched: false }) + this.recalculatePristine() + } +} + const setup = async ({ initialValues }) => { const Input = ({ input, meta, ...restProps }) => { const dataAttrs = { @@ -105,17 +211,7 @@ const setup = async ({ initialValues }) => { ) - const Model = (initialValues.fruits || []).map(value => - getFieldState({ value, initialValue: value }) - ) - // console.log(Model) - - // const buttonEl = DOM.getByText('Push fruit') - // ;[...Array(INITIAL_NUMBER_OF_FIELDS)].forEach(() => { - // TestUtils.Simulate.click(buttonEl) - // Model.push(getFieldState()) - // }) - // await waitForFormToRerender() + const Model = new ModelConstruct(initialValues.fruits) return { DOM, Model } } const selectAllInputs = DOM => DOM.container.querySelectorAll('input') @@ -129,10 +225,7 @@ const realMatchesModel = (Model, DOM) => { touched: touched === 'true' }) ) - const formattedModelData = Model.map( - ({ initialValue, ...restData }) => restData - ) - expect(realMetadata).toEqual(formattedModelData) + expect(realMetadata).toEqual(Model.getFieldsState()) } const validateAttributes = (Model, DOM) => { @@ -150,16 +243,12 @@ class ChangeValue { .map(args => new ChangeValue(...args)) toString = () => ` change value at ${this.index} to '${this.newValue}'` check = Model => { - if (this.index >= Model.length) return false + if (this.index >= Model.getFieldsLength()) return false return true } run = (Model, DOM) => { // abstract - Model[this.index] = getFieldState({ - value: this.newValue, - touched: true, - initialValue: Model[this.index].initialValue - }) + Model.changeValue(this.index, this.newValue) // real const label = `Fruit ${this.index + 1} name` @@ -186,14 +275,14 @@ class Move { .map(args => new Move(...args)) toString = () => ` move(${this.from}, ${this.to})` check = Model => { - if (this.from >= Model.length || this.to >= Model.length) return false + const length = Model.getFieldsLength() + if (this.from >= length || this.to >= length) return false return true } run = async (Model, DOM) => { // abstract - const cache = Model[this.from] - Model.splice(this.from, 1) - Model.splice(this.to, 0, cache) + Model.move(this.from, this.to) + // real const buttonEl = DOM.getByText('Move fruit') TestUtils.Simulate.click(buttonEl, { @@ -201,6 +290,7 @@ class Move { to: this.to }) await waitForFormToRerender() + // postconditions validateAttributes(Model, DOM) } @@ -219,8 +309,7 @@ class Insert { check = () => true run = async (Model, DOM) => { // abstract - const indexOfTheNewElement = Math.min(Model.length, this.index) - Model.splice(indexOfTheNewElement, 0, getFieldState({ value: this.value })) + Model.insert(this.index, this.value) // real const buttonEl = DOM.getByText('Insert fruit') @@ -261,7 +350,7 @@ class Push { check = () => true run = async (Model, DOM) => { // abstract - Model.push(getFieldState({ value: this.value })) + Model.push(this.value) // real const buttonEl = DOM.getByText('Push fruit') @@ -287,7 +376,7 @@ class Remove { } run = async (Model, DOM) => { // abstract - Model.splice(this.index, 1) + Model.remove(this.index) // real const buttonEl = DOM.getByText('Remove fruit') @@ -331,14 +420,14 @@ class Swap { .map(args => new Swap(...args)) toString = () => ` swap(${this.a}, ${this.b})` check = Model => { - if (this.a >= Model.length || this.b >= Model.length) return false + const length = Model.getFieldsLength() + if (this.a >= length || this.b >= length) return false return true } run = async (Model, DOM) => { // abstract - const cache = Model[this.a] - Model[this.a] = Model[this.b] - Model[this.b] = cache + Model.swap(this.a, this.b) + // real const buttonEl = DOM.getByText('Swap fruits') TestUtils.Simulate.click(buttonEl, { @@ -362,12 +451,12 @@ class Update { .map(args => new Update(...args)) toString = () => ` update(${this.index}, '${this.newValue}')` check = Model => { - if (this.index >= Model.length) return false + if (this.index >= Model.getFieldsLength()) return false return true } run = async (Model, DOM) => { // abstract - Model[this.index] = getFieldState({ value: this.newValue, touched: true }) + Model.update(this.index, this.newValue) // real const buttonEl = DOM.getByText('Update fruit') @@ -391,7 +480,7 @@ class Unshift { check = () => true run = async (Model, DOM) => { // abstract - Model.unshift(getFieldState({ value: this.value })) + Model.unshift(this.value) // real const buttonEl = DOM.getByText('Unshift fruit') @@ -405,16 +494,16 @@ class Unshift { } const generateCommands = [ - ChangeValue.generate(), - // Insert.generate() + ChangeValue.generate() + // Insert.generate(), // Move.generate(), - Pop.generate(), + // Pop.generate(), // Push.generate(), - Remove.generate() + // Remove.generate(), // Shift.generate(), // Swap.generate(), // Update.generate(), - // Unshift.generate() + // Unshift.generate(), ] const getInitialState = initialValues => async () => { From dfec50bf0eb0bedf7e5703af37944c18ea08afe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Mon, 3 Dec 2018 09:48:01 +0100 Subject: [PATCH 24/27] Convert null values to empty string --- src/FieldArray.property.test.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 726d532..a3978a6 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -63,7 +63,7 @@ class ModelConstruct { changeValue(index, newValue) { this.state.fields[index] = { - value: newValue, + value: newValue || '', touched: true } this.recalculatePristine() @@ -72,7 +72,7 @@ class ModelConstruct { insert(index, value) { const indexOfTheNewElement = Math.min(this.state.fields.length, index) this.state.fields.splice(indexOfTheNewElement, 0, { - value, + value: value || '', touched: false }) this.recalculatePristine() @@ -114,12 +114,12 @@ class ModelConstruct { update(index, newValue) { const field = this.state.fields[index] - this.state.fields[index] = { value: newValue, touched: field.touched } + this.state.fields[index] = { value: newValue || '', touched: field.touched } this.recalculatePristine() } unshift(value) { - this.state.fields.unshift({ value, touched: false }) + this.state.fields.unshift({ value: value || '', touched: false }) this.recalculatePristine() } } @@ -494,16 +494,16 @@ class Unshift { } const generateCommands = [ - ChangeValue.generate() + ChangeValue.generate(), // Insert.generate(), // Move.generate(), - // Pop.generate(), + Pop.generate(), // Push.generate(), // Remove.generate(), // Shift.generate(), // Swap.generate(), - // Update.generate(), - // Unshift.generate(), + Update.generate() + // Unshift.generate() ] const getInitialState = initialValues => async () => { @@ -532,10 +532,6 @@ describe('FieldArray', () => { { numRuns: 100, verbose: true, - // seed: 1842023377, - // seed: 1842107356, - // seed: 1881850827, - // seed: 1882099238, examples: [ // https://github.com/final-form/final-form-arrays/issues/15#issuecomment-442126496 // https://github.com/final-form/react-final-form-arrays lacks `update` mutator documentation From b14f6086b078ffea34042c7acd87d0307fce75a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Mon, 3 Dec 2018 09:51:58 +0100 Subject: [PATCH 25/27] undo changes in irrelevant files --- src/FieldArray.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/FieldArray.test.js b/src/FieldArray.test.js index 90e3274..5d9ff44 100644 --- a/src/FieldArray.test.js +++ b/src/FieldArray.test.js @@ -603,7 +603,3 @@ describe('FieldArray', () => { expect(formRender.mock.calls[3][0]).toMatchObject({ pristine: false }) }) }) - -// sources -// https://propertesting.com/book_stateful_properties.html -// https://hypothesis.readthedocs.io/en/latest/stateful.html From b62ed5dc3f6fb548a55b784782dce162638fbdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20My=C5=9Bli=C5=84ski?= Date: Mon, 4 Feb 2019 22:49:03 +0100 Subject: [PATCH 26/27] Uncomment failing commands --- src/FieldArray.property.test.js | 49 ++++++--------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index a3978a6..2c8d1c8 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -10,29 +10,6 @@ const nope = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForFormToRerender = () => sleep(0) const INITIAL_NUMBER_OF_FIELDS = 2 -const getFieldState = ({ - value = '', - touched = false, - initialValue = '' -} = {}) => { - const pristine = value === initialValue - return { value: value || '', pristine, touched, initialValue } -} -const modifyFieldState = ({ Model, index, newState = {} }) => { - const initialValueForThisField = Model.initialValues[index] - const value = newState.value === undefined ? '' : newState.value - const pristine = value === initialValueForThisField - const touched = newState.touched || false - Model.fields[index] = { - value, - pristine, - touched - } - Model.initialValues.forEach((initialValue, index) => { - const field = Model.fields[index] - field.pristine = field.value === initialValue - }) -} class ModelConstruct { constructor(initialValues) { @@ -495,15 +472,15 @@ class Unshift { const generateCommands = [ ChangeValue.generate(), - // Insert.generate(), - // Move.generate(), + Insert.generate(), + Move.generate(), Pop.generate(), - // Push.generate(), - // Remove.generate(), - // Shift.generate(), - // Swap.generate(), - Update.generate() - // Unshift.generate() + Push.generate(), + Remove.generate(), + Shift.generate(), + Swap.generate(), + Update.generate(), + Unshift.generate() ] const getInitialState = initialValues => async () => { @@ -528,15 +505,7 @@ describe('FieldArray', () => { await fc.asyncModelRun(stateBuilder, commands) } ) - .afterEach(cleanup), - { - numRuns: 100, - verbose: true, - examples: [ - // https://github.com/final-form/final-form-arrays/issues/15#issuecomment-442126496 - // https://github.com/final-form/react-final-form-arrays lacks `update` mutator documentation - ] - } + .afterEach(cleanup) ) }) }) From c1f202e0ce05e5e16ced3e1cfd1f1f3cdc7292e5 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Mon, 15 Jul 2019 13:39:10 -0400 Subject: [PATCH 27/27] Some fixes to align with library assumptions --- src/FieldArray.property.test.js | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/FieldArray.property.test.js b/src/FieldArray.property.test.js index 2c8d1c8..b712e46 100644 --- a/src/FieldArray.property.test.js +++ b/src/FieldArray.property.test.js @@ -1,12 +1,11 @@ import React, { Fragment } from 'react' -import TestUtils from 'react-dom/test-utils' -import { render, fireEvent, cleanup } from 'react-testing-library' +import { render, fireEvent, cleanup } from '@testing-library/react' import { Form, Field } from 'react-final-form' import arrayMutators from 'final-form-arrays' import fc from 'fast-check' import FieldArray from './FieldArray' -const nope = () => {} +const noop = () => {} const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForFormToRerender = () => sleep(0) const INITIAL_NUMBER_OF_FIELDS = 2 @@ -112,7 +111,7 @@ const setup = async ({ initialValues }) => { const DOM = render(
@@ -261,8 +260,7 @@ class Move { Model.move(this.from, this.to) // real - const buttonEl = DOM.getByText('Move fruit') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Move fruit'), { from: this.from, to: this.to }) @@ -289,8 +287,7 @@ class Insert { Model.insert(this.index, this.value) // real - const buttonEl = DOM.getByText('Insert fruit') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Insert fruit'), { index: this.index, value: this.value }) @@ -330,8 +327,7 @@ class Push { Model.push(this.value) // real - const buttonEl = DOM.getByText('Push fruit') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Push fruit'), { value: this.value }) await waitForFormToRerender() @@ -356,8 +352,7 @@ class Remove { Model.remove(this.index) // real - const buttonEl = DOM.getByText('Remove fruit') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Remove fruit'), { index: this.index }) await waitForFormToRerender() @@ -406,8 +401,7 @@ class Swap { Model.swap(this.a, this.b) // real - const buttonEl = DOM.getByText('Swap fruits') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Swap fruits'), { a: this.a, b: this.b }) @@ -436,8 +430,7 @@ class Update { Model.update(this.index, this.newValue) // real - const buttonEl = DOM.getByText('Update fruit') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Update fruit'), { index: this.index, value: this.newValue }) @@ -460,8 +453,7 @@ class Unshift { Model.unshift(this.value) // real - const buttonEl = DOM.getByText('Unshift fruit') - TestUtils.Simulate.click(buttonEl, { + fireEvent.click(DOM.getByText('Unshift fruit'), { value: this.value }) await waitForFormToRerender()