diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index f0dcaedc5..1c23015d2 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -72,9 +72,7 @@ test('getByTestId', () => { const component = getByTestId('bananaFresh'); expect(component.props.children).toBe('not fresh'); - expect(() => { - getByTestId('InExistent'); - }).toThrow(); + expect(() => getByTestId('InExistent')).toThrow(); }); test('getByName', () => { @@ -91,9 +89,7 @@ test('getByName', () => { expect(bananaFresh.props.children).toBe('not fresh'); - expect(() => { - getByName('InExistent'); - }).toThrow(); + expect(() => getByName('InExistent')).toThrow(); }); test('getAllByName', () => { @@ -103,7 +99,7 @@ test('getAllByName', () => { expect(text.props.children).toBe('Is the banana fresh?'); expect(status.props.children).toBe('not fresh'); expect(button.props.children).toBe('Change freshness!'); - expect(getAllByName('InExistent')).toHaveLength(0); + expect(() => getAllByName('InExistent')).toThrow(); }); test('getByText', () => { @@ -115,9 +111,7 @@ test('getByText', () => { const sameButton = getByText('not fresh'); expect(sameButton.props.children).toBe('not fresh'); - expect(() => { - getByText('InExistent'); - }).toThrow(); + expect(() => getByText('InExistent')).toThrow(); }); test('getAllByText', () => { @@ -125,7 +119,7 @@ test('getAllByText', () => { const button = getAllByText(/fresh/i); expect(button).toHaveLength(3); - expect(getAllByText('InExistent')).toHaveLength(0); + expect(() => getAllByText('InExistent')).toThrow(); }); test('getByProps', () => { @@ -133,9 +127,7 @@ test('getByProps', () => { const primaryType = getByProps({ type: 'primary' }); expect(primaryType.props.children).toBe('Change freshness!'); - expect(() => { - getByProps({ type: 'inexistent' }); - }).toThrow(); + expect(() => getByProps({ type: 'inexistent' })).toThrow(); }); test('getAllByProps', () => { @@ -143,7 +135,7 @@ test('getAllByProps', () => { const primaryTypes = getAllByProps({ type: 'primary' }); expect(primaryTypes).toHaveLength(1); - expect(getAllByProps({ type: 'inexistent' })).toHaveLength(0); + expect(() => getAllByProps({ type: 'inexistent' })).toThrow(); }); test('update', () => { diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 000000000..ce45289ce --- /dev/null +++ b/src/debug.js @@ -0,0 +1,21 @@ +// @flow +import * as React from 'react'; +import prettyFormat, { plugins } from 'pretty-format'; // eslint-disable-line import/no-extraneous-dependencies +import shallow from './shallow'; + +/** + * Log pretty-printed shallow test component instance + */ +export default function debug( + instance: ReactTestInstance | React.Element<*>, + message?: any +) { + const { output } = shallow(instance); + // eslint-disable-next-line no-console + console.log(format(output), message || ''); +} + +const format = input => + prettyFormat(input, { + plugins: [plugins.ReactTestComponent, plugins.ReactElement], + }); diff --git a/src/flushMicrotasksQueue.js b/src/flushMicrotasksQueue.js new file mode 100644 index 000000000..864257cca --- /dev/null +++ b/src/flushMicrotasksQueue.js @@ -0,0 +1,7 @@ +// @flow +/** + * Wait for microtasks queue to flush + */ +export default function flushMicrotasksQueue(): Promise { + return new Promise(resolve => setImmediate(resolve)); +} diff --git a/src/helpers/errorWithStack.js b/src/helpers/errorWithStack.js new file mode 100644 index 000000000..1ee1cb3c2 --- /dev/null +++ b/src/helpers/errorWithStack.js @@ -0,0 +1,9 @@ +// @flow +export default class ErrorWithStack extends Error { + constructor(message: ?string, callsite: Function) { + super(message); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, callsite); + } + } +} diff --git a/src/helpers/getBy.js b/src/helpers/getBy.js new file mode 100644 index 000000000..783c9e2c6 --- /dev/null +++ b/src/helpers/getBy.js @@ -0,0 +1,77 @@ +// @flow +import * as React from 'react'; +import ErrorWithStack from './errorWithStack'; + +const getNodeByName = (node, name) => + node.type.name === name || + node.type.displayName === name || + node.type === name; + +const getNodeByText = (node, text) => + (getNodeByName(node, 'Text') || getNodeByName(node, 'TextInput')) && + (typeof text === 'string' + ? text === node.props.children + : text.test(node.props.children)); + +export const getByName = (instance: ReactTestInstance) => ( + name: string | React.Element<*> +) => { + try { + return instance.find(node => getNodeByName(node, name)); + } catch (error) { + throw new ErrorWithStack(`Error: Component not found.`, getByName); + } +}; + +export const getByText = (instance: ReactTestInstance) => ( + text: string | RegExp +) => { + try { + return instance.find(node => getNodeByText(node, text)); + } catch (error) { + throw new ErrorWithStack(`Error: Component not found.`, getByText); + } +}; + +export const getByProps = (instance: ReactTestInstance) => (props: { + [propName: string]: any, +}) => { + try { + return instance.findByProps(props); + } catch (error) { + throw new ErrorWithStack(`Error: Component not found.`, getByProps); + } +}; + +export const getByTestId = (instance: ReactTestInstance) => (testID: string) => + getByProps(instance)({ testID }); + +export const getAllByName = (instance: ReactTestInstance) => ( + name: string | React.Element<*> +) => { + const results = instance.findAll(node => getNodeByName(node, name)); + if (results.length === 0) { + throw new ErrorWithStack(`Error: Components not found.`, getAllByName); + } + return results; +}; + +export const getAllByText = (instance: ReactTestInstance) => ( + text: string | RegExp +) => { + const results = instance.findAll(node => getNodeByText(node, text)); + if (results.length === 0) { + throw new ErrorWithStack(`Error: Components not found.`, getAllByText); + } + return results; +}; + +export const getAllByProps = (instance: ReactTestInstance) => (props: { + [propName: string]: any, +}) => { + const results = instance.findAllByProps(props); + if (results.length === 0) { + throw new ErrorWithStack(`Error: Components not found.`, getAllByProps); + } + return results; +}; diff --git a/src/index.js b/src/index.js index 1feb8a405..c9014bd68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,118 +1,10 @@ // @flow -import * as React from 'react'; -import { isValidElementType } from 'react-is'; -import TestRenderer from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies -import ShallowRenderer from 'react-test-renderer/shallow'; // eslint-disable-line import/no-extraneous-dependencies -import prettyFormat, { plugins } from 'pretty-format'; // eslint-disable-line import/no-extraneous-dependencies - -const getNodeByName = (node, name) => - node.type.name === name || - node.type.displayName === name || - node.type === name; - -const getNodeByText = (node, text) => - (getNodeByName(node, 'Text') || getNodeByName(node, 'TextInput')) && - (typeof text === 'string' - ? text === node.props.children - : text.test(node.props.children)); - -/** - * Wait for microtasks queue to flush - */ -export const flushMicrotasksQueue = (): Promise => - new Promise(resolve => setImmediate(resolve)); - -/** - * Renders test component deeply using react-test-renderer and exposes helpers - * to assert on the output. - */ -export const render = ( - component: React.Element<*>, - options?: { createNodeMock: (element: React.Element<*>) => any } -) => { - const renderer = TestRenderer.create(component, options); - const instance = renderer.root; - - const getByName = (name: string | React.Element<*>) => { - try { - return instance.find(node => getNodeByName(node, name)); - } catch (error) { - throw new ErrorWithStack(`Error: Component not found.`, getByName); - } - }; - - const getByText = (text: string | RegExp) => { - try { - return instance.find(node => getNodeByText(node, text)); - } catch (error) { - throw new ErrorWithStack(`Error: Component not found.`, getByText); - } - }; - - const getByProps = (props: { [propName: string]: any }) => { - try { - return instance.findByProps(props); - } catch (error) { - throw new ErrorWithStack(`Error: Component not found.`, getByProps); - } - }; - - return { - getByTestId: (testID: string) => instance.findByProps({ testID }), - getByName, - getAllByName: (name: string | React.Element<*>) => - instance.findAll(node => getNodeByName(node, name)), - getByText, - getAllByText: (text: string | RegExp) => - instance.findAll(node => getNodeByText(node, text)), - getByProps, - getAllByProps: (props: { [propName: string]: any }) => - instance.findAllByProps(props), - update: renderer.update, - unmount: renderer.unmount, - }; -}; - -/** - * Renders test component shallowly using react-test-renderer/shallow - */ -export const shallow = (instance: ReactTestInstance | React.Element<*>) => { - const renderer = new ShallowRenderer(); - if (isValidElementType(instance)) { - // $FlowFixMe - instance is React.Element<*> in this branch - renderer.render(instance); - } else { - renderer.render(React.createElement(instance.type, instance.props)); - } - const output = renderer.getRenderOutput(); - - return { - output, - }; -}; - -/** - * Log pretty-printed shallow test component instance - */ -export const debug = ( - instance: ReactTestInstance | React.Element<*>, - message?: any -) => { - const { output } = shallow(instance); - // eslint-disable-next-line no-console - console.log(format(output), message || ''); -}; - -const format = input => - prettyFormat(input, { - plugins: [plugins.ReactTestComponent, plugins.ReactElement], - }); - -class ErrorWithStack extends Error { - constructor(message: ?string, callsite: Function) { - super(message); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, callsite); - } - } -} +import render from './render'; +import shallow from './shallow'; +import flushMicrotasksQueue from './flushMicrotasksQueue'; +import debug from './debug'; + +export { render }; +export { shallow }; +export { flushMicrotasksQueue }; +export { debug }; diff --git a/src/render.js b/src/render.js new file mode 100644 index 000000000..f41583655 --- /dev/null +++ b/src/render.js @@ -0,0 +1,36 @@ +// @flow +import * as React from 'react'; +import TestRenderer from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies +import { + getByTestId, + getByName, + getByText, + getByProps, + getAllByName, + getAllByText, + getAllByProps, +} from './helpers/getBy'; + +/** + * Renders test component deeply using react-test-renderer and exposes helpers + * to assert on the output. + */ +export default function render( + component: React.Element<*>, + options?: { createNodeMock: (element: React.Element<*>) => any } +) { + const renderer = TestRenderer.create(component, options); + const instance = renderer.root; + + return { + getByTestId: getByTestId(instance), + getByName: getByName(instance), + getByText: getByText(instance), + getByProps: getByProps(instance), + getAllByName: getAllByName(instance), + getAllByText: getAllByText(instance), + getAllByProps: getAllByProps(instance), + update: renderer.update, + unmount: renderer.unmount, + }; +} diff --git a/src/shallow.js b/src/shallow.js new file mode 100644 index 000000000..41e92e875 --- /dev/null +++ b/src/shallow.js @@ -0,0 +1,24 @@ +// @flow +import * as React from 'react'; +import { isValidElementType } from 'react-is'; +import ShallowRenderer from 'react-test-renderer/shallow'; // eslint-disable-line import/no-extraneous-dependencies + +/** + * Renders test component shallowly using react-test-renderer/shallow + */ +export default function shallow( + instance: ReactTestInstance | React.Element<*> +) { + const renderer = new ShallowRenderer(); + if (isValidElementType(instance)) { + // $FlowFixMe - instance is React.Element<*> in this branch + renderer.render(instance); + } else { + renderer.render(React.createElement(instance.type, instance.props)); + } + const output = renderer.getRenderOutput(); + + return { + output, + }; +}