From 797b463aeb0ca9cb5445d306677abeb4775ccf60 Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Wed, 7 Feb 2018 17:01:31 -0500 Subject: [PATCH 1/8] Add snapshot test suite for React Native support This adds a rudimentary test suite for the React Native build of Downshift, leveraging Jest snapshots. The goal of this suite is to ensure that Downshift can render React Native components, without calling into methods that are specific to ReactDOM. --- other/react-native/.babelrc | 3 + other/react-native/BasicAutocomplete.js | 59 ++++ .../__snapshots__/render-tests.js.snap | 315 ++++++++++++++++++ other/react-native/__tests__/render-tests.js | 41 +++ other/react-native/jest.config.js | 9 + package.json | 7 +- 6 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 other/react-native/.babelrc create mode 100644 other/react-native/BasicAutocomplete.js create mode 100644 other/react-native/__tests__/__snapshots__/render-tests.js.snap create mode 100644 other/react-native/__tests__/render-tests.js create mode 100644 other/react-native/jest.config.js diff --git a/other/react-native/.babelrc b/other/react-native/.babelrc new file mode 100644 index 000000000..805c73ebb --- /dev/null +++ b/other/react-native/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env", "react-native"] +} diff --git a/other/react-native/BasicAutocomplete.js b/other/react-native/BasicAutocomplete.js new file mode 100644 index 000000000..58e3fcb50 --- /dev/null +++ b/other/react-native/BasicAutocomplete.js @@ -0,0 +1,59 @@ +/* eslint-disable react/prop-types */ + +import React from 'react' +import {Text, TextInput, TouchableOpacity, View} from 'react-native' +import Downshift from '../../dist/downshift.native.cjs' + +const MyView = ({innerRef, ...rest}) => + +const BasicAutocomplete = ({items, ...rest}) => ( + ( + + + {isOpen ? ( + + {items + .filter( + i => + !inputValue || + i.toLowerCase().includes(inputValue.toLowerCase()), + ) + .map((item, index) => { + const props = getItemProps({item, index}) + return ( + + + + {item} + + + + ) + })} + + ) : null} + + )} + /> +) + +export default BasicAutocomplete diff --git a/other/react-native/__tests__/__snapshots__/render-tests.js.snap b/other/react-native/__tests__/__snapshots__/render-tests.js.snap new file mode 100644 index 000000000..8a0039cac --- /dev/null +++ b/other/react-native/__tests__/__snapshots__/render-tests.js.snap @@ -0,0 +1,315 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders input value 1`] = ` + + + + + + + apple + + + + + +`; + +exports[`renders open menus 1`] = ` + + + + + + + apple + + + + + + + orange + + + + + + + carrot + + + + + +`; + +exports[`renders selected items 1`] = ` + + + + + + + apple + + + + + +`; diff --git a/other/react-native/__tests__/render-tests.js b/other/react-native/__tests__/render-tests.js new file mode 100644 index 000000000..b14b5a42b --- /dev/null +++ b/other/react-native/__tests__/render-tests.js @@ -0,0 +1,41 @@ +// eslint-disable-next-line import/no-unassigned-import +import 'react-native' +import React from 'react' + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer' + +import BasicAutocomplete from '../BasicAutocomplete' + +test('renders open menus', () => { + const tree = renderer + .create() + .toJSON() + expect(tree).toMatchSnapshot() +}) + +test('renders input value', () => { + const tree = renderer + .create( + , + ) + .toJSON() + expect(tree).toMatchSnapshot() +}) + +test('renders selected items', () => { + const tree = renderer + .create( + , + ) + .toJSON() + expect(tree).toMatchSnapshot() +}) diff --git a/other/react-native/jest.config.js b/other/react-native/jest.config.js new file mode 100644 index 000000000..d44cecc66 --- /dev/null +++ b/other/react-native/jest.config.js @@ -0,0 +1,9 @@ +const jestConfig = require('kcd-scripts/config').jest + +module.exports = Object.assign(jestConfig, { + preset: 'react-native', + testEnvironment: 'node', + transformIgnorePatterns: ['/node_modules/(?!react-native)/'], + roots: ['.'], + testMatch: ['/other/react-native/__tests__/**/*.js?(x)'], +}) diff --git a/package.json b/package.json index 14c48560b..49ddb63fd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint": "kcd-scripts lint", "test": "kcd-scripts test", "test:cover": "kcd-scripts test --coverage", + "test:native": "kcd-scripts test --config other/react-native/jest.config.js --rootDir .", "test:ssr": "kcd-scripts test --config other/ssr/jest.config.js --no-watch", "test:update": "npm run test:cover -s -- --updateSnapshot", "test:ts": "tsc --noEmit -p ./tsconfig.json", @@ -30,7 +31,7 @@ "storybook:install": "cd stories && npm install", "storybook:serve": "serve ./storybook-static -p 6006", "setup": "npm install && npm run storybook:install -s && npm run storybook:build -s && npm run validate", - "validate": "kcd-scripts validate lint,build-and-test,test:cover,test:ts,test:ssr,test:cypress", + "validate": "kcd-scripts validate lint,build-and-test,test:cover,test:ts,test:ssr,test:native,test:cypress", "precommit": "kcd-scripts precommit" }, "files": [ @@ -61,6 +62,9 @@ }, "devDependencies": { "@storybook/react": "^3.3.14", + "babel-jest": "^22.2.0", + "babel-preset-env": "^1.6.1", + "babel-preset-react-native": "^4.0.0", "cross-env": "^5.1.3", "cypress": "^2.0.3", "enzyme": "^3.3.0", @@ -76,6 +80,7 @@ "prop-types": "^15.6.0", "react": "^16.2.0", "react-dom": "^16.2.0", + "react-native": "^0.53.0", "react-test-renderer": "^16.2.0", "serve": "^6.4.11", "typescript": "^2.7.2" From 2f6df10e6587ab6823ed3874d11174efb9e527d1 Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Thu, 8 Feb 2018 10:54:20 -0500 Subject: [PATCH 2/8] Disable ESLint import/extensions and import/no-unresolved Since we're using a unique "file extension" here (read: `downshift.native.cjs.js`), ESLint thinks the module isn't a valid module. --- other/react-native/BasicAutocomplete.js | 9 +++++++-- other/react-native/__tests__/render-tests.js | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/other/react-native/BasicAutocomplete.js b/other/react-native/BasicAutocomplete.js index 58e3fcb50..7dd6c74cf 100644 --- a/other/react-native/BasicAutocomplete.js +++ b/other/react-native/BasicAutocomplete.js @@ -1,5 +1,3 @@ -/* eslint-disable react/prop-types */ - import React from 'react' import {Text, TextInput, TouchableOpacity, View} from 'react-native' import Downshift from '../../dist/downshift.native.cjs' @@ -57,3 +55,10 @@ const BasicAutocomplete = ({items, ...rest}) => ( ) export default BasicAutocomplete + +/* + eslint + react/prop-types: 0, + import/extensions: 0, + import/no-unresolved: 0 + */ diff --git a/other/react-native/__tests__/render-tests.js b/other/react-native/__tests__/render-tests.js index b14b5a42b..54cb84d8c 100644 --- a/other/react-native/__tests__/render-tests.js +++ b/other/react-native/__tests__/render-tests.js @@ -39,3 +39,10 @@ test('renders selected items', () => { .toJSON() expect(tree).toMatchSnapshot() }) + +/* + eslint + react/prop-types: 0, + import/extensions: 0, + import/no-unresolved: 0 + */ From c2d4c95061189bc4de94e4463010ba1bfa64daff Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Thu, 8 Feb 2018 10:59:05 -0500 Subject: [PATCH 3/8] Add React Native test to test:build command This leverages Jest projects to use both different configs for misc-tests and react-native --- other/react-native/jest.config.js | 1 + package.json | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/other/react-native/jest.config.js b/other/react-native/jest.config.js index d44cecc66..2253fd68d 100644 --- a/other/react-native/jest.config.js +++ b/other/react-native/jest.config.js @@ -4,6 +4,7 @@ module.exports = Object.assign(jestConfig, { preset: 'react-native', testEnvironment: 'node', transformIgnorePatterns: ['/node_modules/(?!react-native)/'], + rootDir: '../../', roots: ['.'], testMatch: ['/other/react-native/__tests__/**/*.js?(x)'], }) diff --git a/package.json b/package.json index 49ddb63fd..265a240e9 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,10 @@ "lint": "kcd-scripts lint", "test": "kcd-scripts test", "test:cover": "kcd-scripts test --coverage", - "test:native": "kcd-scripts test --config other/react-native/jest.config.js --rootDir .", "test:ssr": "kcd-scripts test --config other/ssr/jest.config.js --no-watch", "test:update": "npm run test:cover -s -- --updateSnapshot", "test:ts": "tsc --noEmit -p ./tsconfig.json", - "test:build": "kcd-scripts test --config other/misc-tests/jest.config.js --no-watch", + "test:build": "jest --projects other/misc-tests other/react-native --no-watch", "test:cypress:dev": "npm-run-all --parallel --race storybook cy:open", "pretest:cypress": "npm run storybook:build --silent", "test:cypress": "npm-run-all --parallel --race storybook:serve cy:run", @@ -31,7 +30,7 @@ "storybook:install": "cd stories && npm install", "storybook:serve": "serve ./storybook-static -p 6006", "setup": "npm install && npm run storybook:install -s && npm run storybook:build -s && npm run validate", - "validate": "kcd-scripts validate lint,build-and-test,test:cover,test:ts,test:ssr,test:native,test:cypress", + "validate": "kcd-scripts validate lint,build-and-test,test:cover,test:ts,test:ssr,test:cypress", "precommit": "kcd-scripts precommit" }, "files": [ From c223adca742bad48e04b849d0184dfa7ec5008dd Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Thu, 8 Feb 2018 12:33:57 -0500 Subject: [PATCH 4/8] Create explicit assertions for React Native test cases This emulates the style of the Preact tests, ensuring that we can use different parts of the Downshift API in another environment. --- other/react-native/BasicAutocomplete.js | 64 ---- .../__snapshots__/render-tests.js.snap | 314 ++---------------- other/react-native/__tests__/render-tests.js | 99 ++++-- 3 files changed, 91 insertions(+), 386 deletions(-) delete mode 100644 other/react-native/BasicAutocomplete.js diff --git a/other/react-native/BasicAutocomplete.js b/other/react-native/BasicAutocomplete.js deleted file mode 100644 index 7dd6c74cf..000000000 --- a/other/react-native/BasicAutocomplete.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react' -import {Text, TextInput, TouchableOpacity, View} from 'react-native' -import Downshift from '../../dist/downshift.native.cjs' - -const MyView = ({innerRef, ...rest}) => - -const BasicAutocomplete = ({items, ...rest}) => ( - ( - - - {isOpen ? ( - - {items - .filter( - i => - !inputValue || - i.toLowerCase().includes(inputValue.toLowerCase()), - ) - .map((item, index) => { - const props = getItemProps({item, index}) - return ( - - - - {item} - - - - ) - })} - - ) : null} - - )} - /> -) - -export default BasicAutocomplete - -/* - eslint - react/prop-types: 0, - import/extensions: 0, - import/no-unresolved: 0 - */ diff --git a/other/react-native/__tests__/__snapshots__/render-tests.js.snap b/other/react-native/__tests__/__snapshots__/render-tests.js.snap index 8a0039cac..7aa438306 100644 --- a/other/react-native/__tests__/__snapshots__/render-tests.js.snap +++ b/other/react-native/__tests__/__snapshots__/render-tests.js.snap @@ -1,315 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders input value 1`] = ` +exports[`renders with React Native components 1`] = ` - - - - - apple - - - - - -`; - -exports[`renders open menus 1`] = ` - - - - - - - apple - - - - - - - orange - - - - + - - - carrot - - - - - -`; - -exports[`renders selected items 1`] = ` - - - - + - - - apple - - - + bar + `; diff --git a/other/react-native/__tests__/render-tests.js b/other/react-native/__tests__/render-tests.js index 54cb84d8c..b9acf8cc3 100644 --- a/other/react-native/__tests__/render-tests.js +++ b/other/react-native/__tests__/render-tests.js @@ -1,43 +1,84 @@ +/* eslint-disable react/prop-types */ // eslint-disable-next-line import/no-unassigned-import -import 'react-native' +import {Text, TextInput, View} from 'react-native' import React from 'react' // Note: test renderer must be required after react-native. -import renderer from 'react-test-renderer' +import TestRenderer from 'react-test-renderer' -import BasicAutocomplete from '../BasicAutocomplete' +import Downshift from '../../../dist/downshift.native.cjs' -test('renders open menus', () => { - const tree = renderer - .create() - .toJSON() +test('renders with React Native components', () => { + const RootView = ({innerRef, ...rest}) => + const renderSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => ( + + + + foo + bar + + + )) + const element = + const renderer = TestRenderer.create(element) + expect(renderSpy).toHaveBeenCalledWith( + expect.objectContaining({ + isOpen: false, + highlightedIndex: null, + selectedItem: null, + inputValue: '', + }), + ) + const tree = renderer.toJSON() expect(tree).toMatchSnapshot() }) -test('renders input value', () => { - const tree = renderer - .create( - , - ) - .toJSON() - expect(tree).toMatchSnapshot() +test('can use children instead of render prop', () => { + const RootView = ({innerRef, ...rest}) => + const renderSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => ( + + + + foo + bar + + + )) + const element = {renderSpy} + TestRenderer.create(element) + expect(renderSpy).toHaveBeenCalledTimes(1) }) -test('renders selected items', () => { - const tree = renderer - .create( - , - ) - .toJSON() - expect(tree).toMatchSnapshot() +test('getInputProps composes onChange with onChangeText', () => { + const onChange = jest.fn() + const onInput = jest.fn() + + const RootView = ({innerRef, ...rest}) => + const Input = jest.fn(props => ) + const renderSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => ( + + + + foo + bar + + + )) + const element = + TestRenderer.create(element) + + expect(Input).toHaveBeenCalledTimes(1) + const [[firstArg]] = Input.mock.calls + expect(firstArg).toMatchObject({ + onChangeText: expect.any(Function), + }) + expect(firstArg.onChange).toBeUndefined() + const fakeEvent = 'foobar' + firstArg.onChangeText(fakeEvent) + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(fakeEvent) + expect(onInput).toHaveBeenCalledTimes(1) + expect(onInput).toHaveBeenCalledWith(fakeEvent) }) /* From e5e9c446833372dc0731354e13ef0318a7cc0059 Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Thu, 8 Feb 2018 14:16:54 -0500 Subject: [PATCH 5/8] Remove unnecessary --no-watch argument This is the default behavior for `jest` anyway. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 265a240e9..12cbcd8a6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test:ssr": "kcd-scripts test --config other/ssr/jest.config.js --no-watch", "test:update": "npm run test:cover -s -- --updateSnapshot", "test:ts": "tsc --noEmit -p ./tsconfig.json", - "test:build": "jest --projects other/misc-tests other/react-native --no-watch", + "test:build": "jest --projects other/misc-tests other/react-native", "test:cypress:dev": "npm-run-all --parallel --race storybook cy:open", "pretest:cypress": "npm run storybook:build --silent", "test:cypress": "npm-run-all --parallel --race storybook:serve cy:run", From 21baedb254482860ce2846f6ffde64a59e74422e Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Tue, 6 Mar 2018 16:57:53 -0500 Subject: [PATCH 6/8] Remove invalid test for React Native This test doesn't assert the intended behavior, which is composing onChange and onChangeText --- other/react-native/__tests__/render-tests.js | 32 -------------------- 1 file changed, 32 deletions(-) diff --git a/other/react-native/__tests__/render-tests.js b/other/react-native/__tests__/render-tests.js index b9acf8cc3..ae43f5dfd 100644 --- a/other/react-native/__tests__/render-tests.js +++ b/other/react-native/__tests__/render-tests.js @@ -49,38 +49,6 @@ test('can use children instead of render prop', () => { expect(renderSpy).toHaveBeenCalledTimes(1) }) -test('getInputProps composes onChange with onChangeText', () => { - const onChange = jest.fn() - const onInput = jest.fn() - - const RootView = ({innerRef, ...rest}) => - const Input = jest.fn(props => ) - const renderSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => ( - - - - foo - bar - - - )) - const element = - TestRenderer.create(element) - - expect(Input).toHaveBeenCalledTimes(1) - const [[firstArg]] = Input.mock.calls - expect(firstArg).toMatchObject({ - onChangeText: expect.any(Function), - }) - expect(firstArg.onChange).toBeUndefined() - const fakeEvent = 'foobar' - firstArg.onChangeText(fakeEvent) - expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange).toHaveBeenCalledWith(fakeEvent) - expect(onInput).toHaveBeenCalledTimes(1) - expect(onInput).toHaveBeenCalledWith(fakeEvent) -}) - /* eslint react/prop-types: 0, From 4619b813eb17ca376fb37c8d6cb035ba8c2d621c Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Tue, 6 Mar 2018 17:07:56 -0500 Subject: [PATCH 7/8] Update snapshots --- .../__tests__/__snapshots__/render-tests.js.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/other/react-native/__tests__/__snapshots__/render-tests.js.snap b/other/react-native/__tests__/__snapshots__/render-tests.js.snap index 7aa438306..5651b00df 100644 --- a/other/react-native/__tests__/__snapshots__/render-tests.js.snap +++ b/other/react-native/__tests__/__snapshots__/render-tests.js.snap @@ -8,7 +8,7 @@ exports[`renders with React Native components 1`] = ` aria-autocomplete="list" aria-expanded={false} autoComplete="off" - id="downshift-input-2" + id="downshift-0-input" onBlur={[Function]} onChangeText={[Function]} onKeyDown={[Function]} @@ -20,7 +20,7 @@ exports[`renders with React Native components 1`] = ` accessible={true} allowFontScaling={true} ellipsizeMode="tail" - id="downshift-1-item-0" + id="downshift-0-item-0" onMouseDown={[Function]} onMouseMove={[Function]} onPress={[Function]} @@ -31,7 +31,7 @@ exports[`renders with React Native components 1`] = ` accessible={true} allowFontScaling={true} ellipsizeMode="tail" - id="downshift-1-item-1" + id="downshift-0-item-1" onMouseDown={[Function]} onMouseMove={[Function]} onPress={[Function]} From 4d35baf56c1e44afe2fc22d2474dae3e68411c43 Mon Sep 17 00:00:00 2001 From: Eli Perkins Date: Tue, 6 Mar 2018 17:08:20 -0500 Subject: [PATCH 8/8] Add test to call onChangeText on TextInput components for React Native --- other/react-native/__tests__/render-tests.js | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/other/react-native/__tests__/render-tests.js b/other/react-native/__tests__/render-tests.js index ae43f5dfd..a9474c5c9 100644 --- a/other/react-native/__tests__/render-tests.js +++ b/other/react-native/__tests__/render-tests.js @@ -49,6 +49,37 @@ test('can use children instead of render prop', () => { expect(renderSpy).toHaveBeenCalledTimes(1) }) +test('calls onChange when TextInput changes values', () => { + const onChange = jest.fn() + const Input = jest.fn(props => ) + + const RootView = ({innerRef, ...rest}) => + const renderSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => ( + + + + foo + bar + + + )) + const element = {renderSpy} + TestRenderer.create(element) + expect(renderSpy).toHaveBeenCalledTimes(1) + + const [[firstArg]] = Input.mock.calls + expect(firstArg).toMatchObject({ + // TODO: We shouldn't need to know about the internals of how we're affecting the TextInput and what props we're supplying. + // See https://github.com/paypal/downshift/issues/361 + onChangeText: expect.any(Function), + }) + const fakeEvent = 'foobar' + firstArg.onChangeText(fakeEvent) + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(fakeEvent) +}) + /* eslint react/prop-types: 0,