From 61895c941cf27413d09d49bedbe00901e4a8d419 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Thu, 18 Apr 2019 16:48:01 -0400 Subject: [PATCH 1/5] feat(queries): throw when multiple elements are returned from getBy BREAKING CHANGE: `queryBy` and `getBy` now throw when multiple elements are returned and prompt users to use other queries if multiple results were intentional. This was done to remain in feature parity with dom-testing-library. --- src/__tests__/get-by-errors.js | 187 +++++++++++++++++++++++++++++++++ src/queries.js | 91 ++++++++++++---- src/query-helpers.js | 12 ++- 3 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 src/__tests__/get-by-errors.js diff --git a/src/__tests__/get-by-errors.js b/src/__tests__/get-by-errors.js new file mode 100644 index 0000000..9edeb04 --- /dev/null +++ b/src/__tests__/get-by-errors.js @@ -0,0 +1,187 @@ +import React from 'react'; +import { Text, TextInput, View } from 'react-native'; +import cases from 'jest-in-case'; + +import { render } from '../'; + +cases( + 'getBy* queries throw an error when there are multiple elements returned', + ({ name, query, tree }) => { + const utils = render(tree); + expect(() => utils[name](query)).toThrow(/multiple elements/i); + }, + { + getByA11yHint: { + query: /his/, + tree: ( + + + + + ), + }, + getByA11yLabel: { + query: /his/, + tree: ( + + + + + ), + }, + getByA11yRole: { + query: 'button', + tree: ( + + + + + ), + }, + getByA11yStates: { + query: ['selected'], + tree: ( + + + + + ), + }, + getByA11yTraits: { + query: ['button'], + tree: ( + + + + + ), + }, + getByPlaceholder: { + query: /his/, + tree: ( + + + + + ), + }, + getByTestId: { + query: /his/, + tree: ( + + text + other + + ), + }, + getByText: { + query: /his/, + tree: ( + + his + history + + ), + }, + getByValue: { + query: /his/, + tree: ( + + + + + ), + }, + }, +); + +cases( + 'queryBy* queries throw an error when there are multiple elements returned', + ({ name, query, tree }) => { + const utils = render(tree); + expect(() => utils[name](query)).toThrow(/multiple elements/i); + }, + { + queryByA11yHint: { + query: /his/, + tree: ( + + + + + ), + }, + queryByA11yLabel: { + query: /his/, + tree: ( + + + + + ), + }, + queryByA11yRole: { + query: 'button', + tree: ( + + + + + ), + }, + queryByA11yStates: { + query: ['selected'], + tree: ( + + + + + ), + }, + queryByA11yTraits: { + query: ['button'], + tree: ( + + + + + ), + }, + queryByPlaceholder: { + query: /his/, + tree: ( + + + + + ), + }, + queryByTestId: { + query: /his/, + tree: ( + + text + other + + ), + }, + queryByText: { + query: /his/, + tree: ( + + his + history + + ), + }, + queryByValue: { + query: /his/, + tree: ( + + + + + ), + }, + } +); diff --git a/src/queries.js b/src/queries.js index 90c5b2a..5d88f38 100644 --- a/src/queries.js +++ b/src/queries.js @@ -6,6 +6,7 @@ import { waitForElement } from './wait-for-element'; import { fuzzyMatches, makeNormalizer, matches } from './matches'; import { getBaseElement, + getGetByElementError, getElementError, firstResultOrNull, queryAllByProp, @@ -107,40 +108,94 @@ function getAllByValue(container, value, ...rest) { | Get by... |-------------------------------------------------------------------------- */ -function getByA11yHint(...args) { - return firstResultOrNull(getAllByA11yHint, ...args); +function getByA11yHint(container, hint, ...args) { + const els = getAllByA11yHint(container, hint, ...args); + if (els.length > 1) { + throw getGetByElementError( + `Found multiple elements with the accessibilityHint: ${hint}`, + container, + ); + } + return els[0]; } -function getByA11yLabel(...args) { - return firstResultOrNull(getAllByA11yLabel, ...args); +function getByA11yLabel(container, label, ...args) { + const els = getAllByA11yLabel(container, label, ...args); + if (els.length > 1) { + throw getGetByElementError( + `Found multiple elements with the accessibilityLabel: ${label}`, + container, + ); + } + return els[0]; } -function getByPlaceholder(...args) { - return firstResultOrNull(getAllByPlaceholder, ...args); +function getByA11yRole(container, role, ...args) { + const els = getAllByA11yRole(container, role, ...args); + if (els.length > 1) { + throw getGetByElementError( + `Found multiple elements with the accessibilityRole: "${role}"`, + container, + ); + } + return els[0]; } -function getByA11yRole(...args) { - return firstResultOrNull(getAllByA11yRole, ...args); +function getByA11yStates(container, states, ...args) { + const els = getAllByA11yStates(container, states, ...args); + if (els.length > 1) { + throw getGetByElementError( + `Found multiple elements with the accessibilityStates: ${JSON.stringify(states)}`, + container, + ); + } + return els[0]; } -function getByA11yStates(...args) { - return firstResultOrNull(getAllByA11yStates, ...args); +function getByA11yTraits(container, traits, ...args) { + const els = getAllByA11yTraits(container, traits, ...args); + if (els.length > 1) { + throw getGetByElementError( + `Found multiple elements with the accessibilityTraits: ${JSON.stringify(traits)}`, + container, + ); + } + return els[0]; } -function getByA11yTraits(...args) { - return firstResultOrNull(getAllByA11yTraits, ...args); +function getByPlaceholder(container, placeholder, ...args) { + const els = getAllByPlaceholder(container, placeholder, ...args); + if (els.length > 1) { + throw getGetByElementError( + `Found multiple elements with the placeholder: "${placeholder}"`, + container, + ); + } + return els[0]; } -function getByTestId(...args) { - return firstResultOrNull(getAllByTestId, ...args); +function getByTestId(container, id, ...args) { + const els = getAllByTestId(container, id, ...args); + if (els.length > 1) { + throw getGetByElementError(`Found multiple elements with the testID: "${id}"`, container); + } + return els[0]; } -function getByText(...args) { - return firstResultOrNull(getAllByText, ...args); +function getByText(container, text, ...args) { + const els = getAllByText(container, text, ...args); + if (els.length > 1) { + throw getGetByElementError(`Found multiple elements with the text: ${text}`, container); + } + return els[0]; } -function getByValue(...args) { - return firstResultOrNull(getAllByValue, ...args); +function getByValue(container, value, ...args) { + const els = getAllByValue(container, value, ...args); + if (els.length > 1) { + throw getGetByElementError(`Found multiple elements with the value: ${value}`, container); + } + return els[0]; } /* diff --git a/src/query-helpers.js b/src/query-helpers.js index 2f6ef63..60d8566 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -15,6 +15,13 @@ function filterNodeByType(node, type) { return node.type === type; } +function getGetByElementError(message, container) { + return getElementError( + `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`getAllByText\` or \`findAllByText\`)).`, + container, + ) +} + function firstResultOrNull(queryFunction, ...args) { const result = queryFunction(...args); if (result.length === 0) return null; @@ -90,10 +97,11 @@ function queryByProp(...args) { export { defaultFilter, + filterNodeByType, + firstResultOrNull, getBaseElement, getElementError, - firstResultOrNull, - filterNodeByType, + getGetByElementError, queryAllByProp, queryByProp, removeBadProperties, From 9e226cfb15020a805372889c00d815aeee3356dd Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Thu, 18 Apr 2019 21:41:48 -0400 Subject: [PATCH 2/5] refactor: use new query abstractions --- README.md | 4 +- src/__tests__/get-by-errors.js | 2 +- src/__tests__/misc.js | 18 ++ src/index.d.ts | 1 - src/queries.js | 330 --------------------------------- src/queries/a11y-hint.js | 25 +++ src/queries/a11y-label.js | 25 +++ src/queries/a11y-role.js | 25 +++ src/queries/a11y-states.js | 25 +++ src/queries/a11y-traits.js | 25 +++ src/queries/all-utils.js | 4 + src/queries/index.js | 9 + src/queries/placeholder.js | 23 +++ src/queries/test-id.js | 21 +++ src/queries/text.js | 43 +++++ src/queries/value.js | 25 +++ src/query-helpers.js | 81 ++++++-- 17 files changed, 337 insertions(+), 349 deletions(-) create mode 100644 src/__tests__/misc.js delete mode 100644 src/queries.js create mode 100644 src/queries/a11y-hint.js create mode 100644 src/queries/a11y-label.js create mode 100644 src/queries/a11y-role.js create mode 100644 src/queries/a11y-states.js create mode 100644 src/queries/a11y-traits.js create mode 100644 src/queries/all-utils.js create mode 100644 src/queries/index.js create mode 100644 src/queries/placeholder.js create mode 100644 src/queries/test-id.js create mode 100644 src/queries/text.js create mode 100644 src/queries/value.js diff --git a/README.md b/README.md index c90e4e2..5703434 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,8 @@ that library with this one somehow. ## Other Solutions -* [`react-native-testing-library`](https://github.com/callstack/react-native-testing-library) -* [`enzyme`](https://airbnb.io/enzyme/docs/guides/react-native.html) +- [`react-native-testing-library`](https://github.com/callstack/react-native-testing-library) +- [`enzyme`](https://airbnb.io/enzyme/docs/guides/react-native.html) ## Guiding Principles diff --git a/src/__tests__/get-by-errors.js b/src/__tests__/get-by-errors.js index 9edeb04..0fa5a27 100644 --- a/src/__tests__/get-by-errors.js +++ b/src/__tests__/get-by-errors.js @@ -183,5 +183,5 @@ cases( ), }, - } + }, ); diff --git a/src/__tests__/misc.js b/src/__tests__/misc.js new file mode 100644 index 0000000..804cfad --- /dev/null +++ b/src/__tests__/misc.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { View } from 'react-native'; + +import { render } from '../'; +import { queryByProp } from '../'; + +// we used to use queryByAttribute internally, but we don't anymore. Some people +// use it as an undocumented part of the API, so we'll keep it around. +test('queryByAttribute', () => { + const { container } = render( + + + + , + ); + expect(queryByProp('pointerEvents', container, 'none')).not.toBeNull(); + expect(queryByProp('collapsable', container, false)).toBeNull(); +}); diff --git a/src/index.d.ts b/src/index.d.ts index 27e62c2..ad3f9fe 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -227,7 +227,6 @@ export interface Queries { export declare function defaultFilter(node: NativeTestInstance): boolean export declare function getBaseElement(container: ReactTestRenderer | ReactTestInstance): ReactTestInstance export declare function getElementError(message: string, container: ReactTestRenderer): Error -export declare function firstResultOrNull(query: (...args: T) => U[], ...args: T): U | null export declare function filterNodeByType(node: NativeTestInstance, type: string): boolean export declare function queryAllByProp( attribute: string, diff --git a/src/queries.js b/src/queries.js deleted file mode 100644 index 5d88f38..0000000 --- a/src/queries.js +++ /dev/null @@ -1,330 +0,0 @@ -import React from 'react'; -import { Text, TextInput } from 'react-native'; - -import { getNodeText } from './get-node-text'; -import { waitForElement } from './wait-for-element'; -import { fuzzyMatches, makeNormalizer, matches } from './matches'; -import { - getBaseElement, - getGetByElementError, - getElementError, - firstResultOrNull, - queryAllByProp, - queryByProp, - removeBadProperties, -} from './query-helpers'; - -/* - |-------------------------------------------------------------------------- - | Get all by... - |-------------------------------------------------------------------------- - */ -function getAllByA11yHint(container, text, ...rest) { - const els = queryAllByA11yHint(container, text, ...rest); - if (!els.length) { - throw getElementError(`Unable to find an element by accessibilityHint="${text}"`, container); - } - return els; -} - -function getAllByA11yLabel(container, text, ...rest) { - const els = queryAllByA11yLabel(container, text, ...rest); - if (!els.length) { - throw getElementError(`Unable to find an element by accessibilityLabel="${text}"`, container); - } - return els; -} - -function getAllByA11yRole(container, value, ...rest) { - const els = queryAllByA11yRole(container, value, ...rest); - if (!els.length) { - throw getElementError(`Unable to find an element by accessibilityRole="${value}".`, container); - } - return els; -} - -function getAllByA11yStates(container, value, ...rest) { - const els = queryAllByA11yStates(container, value, ...rest); - if (!els.length) { - throw getElementError( - `Unable to find an element by accessibilityStates="${value}".`, - container, - ); - } - return els; -} - -function getAllByA11yTraits(container, value, ...rest) { - const els = queryAllByA11yTraits(container, value, ...rest); - if (!els.length) { - throw getElementError( - `Unable to find an element by accessibilityStates="${value}".`, - container, - ); - } - return els; -} - -function getAllByPlaceholder(container, text, ...rest) { - const els = queryAllByPlaceholder(container, text, ...rest); - if (!els.length) { - throw getElementError( - `Unable to find an element with the placeholder text of: ${text}`, - container, - ); - } - return els; -} - -function getAllByTestId(container, id, ...rest) { - const els = queryAllByTestId(container, id, ...rest); - if (!els.length) { - throw getElementError(`Unable to find an element with the testID: ${id}`, container); - } - return els; -} - -function getAllByText(container, text, ...rest) { - const els = queryAllByText(container, text, ...rest); - if (!els.length) { - throw getElementError( - `Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`, - container, - ); - } - return els; -} - -function getAllByValue(container, value, ...rest) { - const els = queryAllByValue(container, value, ...rest); - if (!els.length) { - throw getElementError(`Unable to find an element with the value: ${value}.`, container); - } - return els; -} - -/* - |-------------------------------------------------------------------------- - | Get by... - |-------------------------------------------------------------------------- - */ -function getByA11yHint(container, hint, ...args) { - const els = getAllByA11yHint(container, hint, ...args); - if (els.length > 1) { - throw getGetByElementError( - `Found multiple elements with the accessibilityHint: ${hint}`, - container, - ); - } - return els[0]; -} - -function getByA11yLabel(container, label, ...args) { - const els = getAllByA11yLabel(container, label, ...args); - if (els.length > 1) { - throw getGetByElementError( - `Found multiple elements with the accessibilityLabel: ${label}`, - container, - ); - } - return els[0]; -} - -function getByA11yRole(container, role, ...args) { - const els = getAllByA11yRole(container, role, ...args); - if (els.length > 1) { - throw getGetByElementError( - `Found multiple elements with the accessibilityRole: "${role}"`, - container, - ); - } - return els[0]; -} - -function getByA11yStates(container, states, ...args) { - const els = getAllByA11yStates(container, states, ...args); - if (els.length > 1) { - throw getGetByElementError( - `Found multiple elements with the accessibilityStates: ${JSON.stringify(states)}`, - container, - ); - } - return els[0]; -} - -function getByA11yTraits(container, traits, ...args) { - const els = getAllByA11yTraits(container, traits, ...args); - if (els.length > 1) { - throw getGetByElementError( - `Found multiple elements with the accessibilityTraits: ${JSON.stringify(traits)}`, - container, - ); - } - return els[0]; -} - -function getByPlaceholder(container, placeholder, ...args) { - const els = getAllByPlaceholder(container, placeholder, ...args); - if (els.length > 1) { - throw getGetByElementError( - `Found multiple elements with the placeholder: "${placeholder}"`, - container, - ); - } - return els[0]; -} - -function getByTestId(container, id, ...args) { - const els = getAllByTestId(container, id, ...args); - if (els.length > 1) { - throw getGetByElementError(`Found multiple elements with the testID: "${id}"`, container); - } - return els[0]; -} - -function getByText(container, text, ...args) { - const els = getAllByText(container, text, ...args); - if (els.length > 1) { - throw getGetByElementError(`Found multiple elements with the text: ${text}`, container); - } - return els[0]; -} - -function getByValue(container, value, ...args) { - const els = getAllByValue(container, value, ...args); - if (els.length > 1) { - throw getGetByElementError(`Found multiple elements with the value: ${value}`, container); - } - return els[0]; -} - -/* - |-------------------------------------------------------------------------- - | Query by... - |-------------------------------------------------------------------------- - */ -const queryAllByA11yHint = queryAllByProp.bind(null, 'accessibilityHint'); -const queryAllByA11yLabel = queryAllByProp.bind(null, 'accessibilityLabel'); -const queryAllByA11yRole = queryAllByProp.bind(null, 'accessibilityRole'); -const queryAllByA11yStates = queryAllByProp.bind(null, 'accessibilityStates'); -const queryAllByA11yTraits = queryAllByProp.bind(null, 'accessibilityTraits'); -const queryAllByPlaceholder = queryAllByProp.bind(null, 'placeholder'); -const queryAllByTestId = queryAllByProp.bind(null, 'testID'); -const queryAllByValue = queryAllByProp.bind(null, 'value'); - -/* - |-------------------------------------------------------------------------- - | Query all by... - |-------------------------------------------------------------------------- - */ -const queryByA11yHint = queryByProp.bind(null, 'accessibilityHint'); -const queryByA11yLabel = queryByProp.bind(null, 'accessibilityLabel'); -const queryByA11yRole = queryByProp.bind(null, 'accessibilityRole'); -const queryByA11yStates = queryByProp.bind(null, 'accessibilityStates'); -const queryByA11yTraits = queryByProp.bind(null, 'accessibilityTraits'); -const queryByPlaceholder = queryByProp.bind(null, 'placeholder'); -const queryByTestId = queryByProp.bind(null, 'testID'); -const queryByValue = queryByProp.bind(null, 'value'); - -function queryAllByText( - container, - text, - { types = ['Text', 'TextInput'], exact = true, collapseWhitespace, trim, normalizer } = {}, -) { - const baseElement = getBaseElement(container); - const matcher = exact ? matches : fuzzyMatches; - const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer }); - - const baseArray = types.reduce( - (accumulator, currentValue) => [ - ...accumulator, - ...baseElement.findAll(n => n.type === currentValue), - ], - [], - ); - - return baseArray - .filter(node => matcher(getNodeText(node), node, text, matchNormalizer)) - .map(removeBadProperties); -} - -function queryByText(...args) { - return firstResultOrNull(queryAllByText, ...args); -} - -/* - |-------------------------------------------------------------------------- - | Finders - |-------------------------------------------------------------------------- - */ -function makeFinder(getter) { - return (container, text, options, waitForElementOptions) => - waitForElement(() => getter(container, text, options), waitForElementOptions); -} - -/* - |-------------------------------------------------------------------------- - | Find all by... - |-------------------------------------------------------------------------- - */ -export const findAllByA11yHint = makeFinder(getAllByA11yHint); -export const findAllByA11yLabel = makeFinder(getAllByA11yLabel); -export const findAllByA11yRole = makeFinder(getAllByA11yRole); -export const findAllByA11yStates = makeFinder(getAllByA11yStates); -export const findAllByA11yTraits = makeFinder(getAllByA11yTraits); -export const findAllByPlaceholder = makeFinder(getAllByPlaceholder); -export const findAllByTestId = makeFinder(getAllByTestId); -export const findAllByText = makeFinder(getAllByText); -export const findAllByValue = makeFinder(getAllByValue); - -/* - |-------------------------------------------------------------------------- - | Find by... - |-------------------------------------------------------------------------- - */ -export const findByA11yHint = makeFinder(getByA11yHint); -export const findByA11yLabel = makeFinder(getByA11yLabel); -export const findByA11yRole = makeFinder(getByA11yRole); -export const findByA11yStates = makeFinder(getByA11yStates); -export const findByA11yTraits = makeFinder(getByA11yTraits); -export const findByPlaceholder = makeFinder(getByPlaceholder); -export const findByTestId = makeFinder(getByTestId); -export const findByText = makeFinder(getByText); -export const findByValue = makeFinder(getByValue); - -export { - getAllByA11yHint, - getAllByA11yLabel, - getAllByA11yRole, - getAllByA11yStates, - getAllByA11yTraits, - getAllByPlaceholder, - getAllByTestId, - getAllByText, - getAllByValue, - getByA11yHint, - getByA11yLabel, - getByA11yRole, - getByA11yStates, - getByA11yTraits, - getByPlaceholder, - getByTestId, - getByText, - getByValue, - queryAllByA11yHint, - queryAllByA11yLabel, - queryAllByA11yRole, - queryAllByA11yTraits, - queryAllByPlaceholder, - queryAllByTestId, - queryAllByText, - queryAllByValue, - queryByA11yHint, - queryByA11yLabel, - queryByA11yRole, - queryByA11yTraits, - queryByPlaceholder, - queryByTestId, - queryByText, - queryByValue, -}; diff --git a/src/queries/a11y-hint.js b/src/queries/a11y-hint.js new file mode 100644 index 0000000..cca2ef7 --- /dev/null +++ b/src/queries/a11y-hint.js @@ -0,0 +1,25 @@ +import { queryAllByProp, buildQueries, getPropMatchAsString } from './all-utils'; + +const queryAllByA11yHint = queryAllByProp.bind(null, 'accessibilityHint'); + +const getMultipleError = (c, hint) => + `Found multiple elements with the accessibilityHint of: ${hint}`; +const getMissingError = (c, hint) => + `Unable to find an element with the accessibilityHint of: ${hint}`; + +const [ + queryByA11yHint, + getAllByA11yHint, + getByA11yHint, + findAllByA11yHint, + findByA11yHint, +] = buildQueries(queryAllByA11yHint, getMultipleError, getMissingError); + +export { + queryByA11yHint, + queryAllByA11yHint, + getByA11yHint, + getAllByA11yHint, + findAllByA11yHint, + findByA11yHint, +}; diff --git a/src/queries/a11y-label.js b/src/queries/a11y-label.js new file mode 100644 index 0000000..46598d3 --- /dev/null +++ b/src/queries/a11y-label.js @@ -0,0 +1,25 @@ +import { queryAllByProp, buildQueries } from './all-utils'; + +const queryAllByA11yLabel = queryAllByProp.bind(null, 'accessibilityLabel'); + +const getMultipleError = (c, label) => + `Found multiple elements with the accessibilityLabel of: ${label}`; +const getMissingError = (c, label) => + `Unable to find an element with the accessibilityLabel of: ${label}`; + +const [ + queryByA11yLabel, + getAllByA11yLabel, + getByA11yLabel, + findAllByA11yLabel, + findByA11yLabel, +] = buildQueries(queryAllByA11yLabel, getMultipleError, getMissingError); + +export { + queryByA11yLabel, + queryAllByA11yLabel, + getByA11yLabel, + getAllByA11yLabel, + findAllByA11yLabel, + findByA11yLabel, +}; diff --git a/src/queries/a11y-role.js b/src/queries/a11y-role.js new file mode 100644 index 0000000..ba2cd8d --- /dev/null +++ b/src/queries/a11y-role.js @@ -0,0 +1,25 @@ +import { queryAllByProp, buildQueries } from './all-utils'; + +const queryAllByA11yRole = queryAllByProp.bind(null, 'accessibilityRole'); + +const getMultipleError = (c, role) => + `Found multiple elements with the accessibilityRole of: ${role}`; +const getMissingError = (c, role) => + `Unable to find an element with the accessibilityRole of: ${role}`; + +const [ + queryByA11yRole, + getAllByA11yRole, + getByA11yRole, + findAllByA11yRole, + findByA11yRole, +] = buildQueries(queryAllByA11yRole, getMultipleError, getMissingError); + +export { + queryByA11yRole, + queryAllByA11yRole, + getByA11yRole, + getAllByA11yRole, + findAllByA11yRole, + findByA11yRole, +}; diff --git a/src/queries/a11y-states.js b/src/queries/a11y-states.js new file mode 100644 index 0000000..3bda43e --- /dev/null +++ b/src/queries/a11y-states.js @@ -0,0 +1,25 @@ +import { queryAllByProp, buildQueries, getPropMatchAsString } from './all-utils'; + +const queryAllByA11yStates = queryAllByProp.bind(null, 'accessibilityStates'); + +const getMultipleError = (c, states) => + `Found multiple elements with the accessibilityStates of: ${getPropMatchAsString(states)}`; +const getMissingError = (c, states) => + `Unable to find an element with the accessibilityStates of: ${getPropMatchAsString(states)}`; + +const [ + queryByA11yStates, + getAllByA11yStates, + getByA11yStates, + findAllByA11yStates, + findByA11yStates, +] = buildQueries(queryAllByA11yStates, getMultipleError, getMissingError); + +export { + queryByA11yStates, + queryAllByA11yStates, + getByA11yStates, + getAllByA11yStates, + findAllByA11yStates, + findByA11yStates, +}; diff --git a/src/queries/a11y-traits.js b/src/queries/a11y-traits.js new file mode 100644 index 0000000..96a5a41 --- /dev/null +++ b/src/queries/a11y-traits.js @@ -0,0 +1,25 @@ +import { queryAllByProp, buildQueries, getPropMatchAsString } from './all-utils'; + +const queryAllByA11yTraits = queryAllByProp.bind(null, 'accessibilityTraits'); + +const getMultipleError = (c, traits) => + `Found multiple elements with the accessibilityTraits of: ${getPropMatchAsString(traits)}`; +const getMissingError = (c, traits) => + `Unable to find an element with the accessibilityTraits of: ${getPropMatchAsString(traits)}`; + +const [ + queryByA11yTraits, + getAllByA11yTraits, + getByA11yTraits, + findAllByA11yTraits, + findByA11yTraits, +] = buildQueries(queryAllByA11yTraits, getMultipleError, getMissingError); + +export { + queryByA11yTraits, + queryAllByA11yTraits, + getByA11yTraits, + getAllByA11yTraits, + findAllByA11yTraits, + findByA11yTraits, +}; diff --git a/src/queries/all-utils.js b/src/queries/all-utils.js new file mode 100644 index 0000000..eb0db6e --- /dev/null +++ b/src/queries/all-utils.js @@ -0,0 +1,4 @@ +export * from '../matches'; +export * from '../get-node-text'; +export * from '../query-helpers'; +export * from '../config'; diff --git a/src/queries/index.js b/src/queries/index.js new file mode 100644 index 0000000..6cb9081 --- /dev/null +++ b/src/queries/index.js @@ -0,0 +1,9 @@ +export * from './a11y-label'; +export * from './a11y-role'; +export * from './a11y-states'; +export * from './a11y-hint'; +export * from './a11y-traits'; +export * from './placeholder'; +export * from './test-id'; +export * from './text'; +export * from './value'; diff --git a/src/queries/placeholder.js b/src/queries/placeholder.js new file mode 100644 index 0000000..173956d --- /dev/null +++ b/src/queries/placeholder.js @@ -0,0 +1,23 @@ +import { queryAllByProp, buildQueries } from './all-utils'; + +const queryAllByPlaceholder = queryAllByProp.bind(null, 'placeholder'); + +const getMultipleError = (c, text) => `Found multiple elements with the placeholder of: ${text}`; +const getMissingError = (c, text) => `Unable to find an element with the placeholder of: ${text}`; + +const [ + queryByPlaceholder, + getAllByPlaceholder, + getByPlaceholder, + findAllByPlaceholder, + findByPlaceholder, +] = buildQueries(queryAllByPlaceholder, getMultipleError, getMissingError); + +export { + queryByPlaceholder, + queryAllByPlaceholder, + getByPlaceholder, + getAllByPlaceholder, + findAllByPlaceholder, + findByPlaceholder, +}; diff --git a/src/queries/test-id.js b/src/queries/test-id.js new file mode 100644 index 0000000..b62ddcf --- /dev/null +++ b/src/queries/test-id.js @@ -0,0 +1,21 @@ +import { queryAllByProp, buildQueries } from './all-utils'; + +const queryAllByTestId = queryAllByProp.bind(null, 'testID'); + +const getMultipleError = (c, id) => `Found multiple elements with the testID of: ${id}`; +const getMissingError = (c, id) => `Unable to find an element with the testID of: ${id}`; + +const [queryByTestId, getAllByTestId, getByTestId, findAllByTestId, findByTestId] = buildQueries( + queryAllByTestId, + getMultipleError, + getMissingError, +); + +export { + queryByTestId, + queryAllByTestId, + getByTestId, + getAllByTestId, + findAllByTestId, + findByTestId, +}; diff --git a/src/queries/text.js b/src/queries/text.js new file mode 100644 index 0000000..a5d722b --- /dev/null +++ b/src/queries/text.js @@ -0,0 +1,43 @@ +import { + fuzzyMatches, + matches, + makeNormalizer, + getNodeText, + buildQueries, + getBaseElement, + removeBadProperties, +} from './all-utils'; + +function queryAllByText( + container, + text, + { exact = true, collapseWhitespace, trim, normalizer } = {}, +) { + const baseElement = getBaseElement(container); + const matcher = exact ? matches : fuzzyMatches; + const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer }); + + const baseArray = ['Text', 'TextInput'].reduce( + (accumulator, currentValue) => [ + ...accumulator, + ...baseElement.findAll(n => n.type === currentValue), + ], + [], + ); + + return baseArray + .filter(node => matcher(getNodeText(node), node, text, matchNormalizer)) + .map(removeBadProperties); +} + +const getMultipleError = (c, text) => `Found multiple elements with the text: ${text}`; +const getMissingError = (c, text) => + `Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`; + +const [queryByText, getAllByText, getByText, findAllByText, findByText] = buildQueries( + queryAllByText, + getMultipleError, + getMissingError, +); + +export { queryByText, queryAllByText, getByText, getAllByText, findAllByText, findByText }; diff --git a/src/queries/value.js b/src/queries/value.js new file mode 100644 index 0000000..00b91f3 --- /dev/null +++ b/src/queries/value.js @@ -0,0 +1,25 @@ +import { matches, fuzzyMatches, makeNormalizer, buildQueries, getBaseElement } from './all-utils'; + +function queryAllByValue( + container, + value, + { exact = true, collapseWhitespace, trim, normalizer } = {}, +) { + const matcher = exact ? matches : fuzzyMatches; + const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer }); + + const baseElement = getBaseElement(container); + return Array.from(baseElement.findAll(n => n.type === 'TextInput')).filter(node => + matcher(node.props.value, node, value, matchNormalizer), + ); +} + +const getMultipleError = (c, value) => `Found multiple elements with the value: ${value}.`; +const getMissingError = (c, value) => `Unable to find an element with the value: ${value}.`; +const [queryByValue, getAllByValue, getByValue, findAllByValue, findByValue] = buildQueries( + queryAllByValue, + getMultipleError, + getMissingError, +); + +export { queryByValue, queryAllByValue, getByValue, getAllByValue, findAllByValue, findByValue }; diff --git a/src/query-helpers.js b/src/query-helpers.js index 60d8566..a3875a6 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -1,4 +1,5 @@ import { prettyPrint } from './pretty-print'; +import { waitForElement } from './wait-for-element'; import { fuzzyMatches, makeNormalizer, matches } from './matches'; function debugTree(htmlElement) { @@ -15,17 +16,11 @@ function filterNodeByType(node, type) { return node.type === type; } -function getGetByElementError(message, container) { +function getMultipleElementsFoundError(message, container) { return getElementError( - `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`getAllByText\` or \`findAllByText\`)).`, + `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`, container, - ) -} - -function firstResultOrNull(queryFunction, ...args) { - const result = queryFunction(...args); - if (result.length === 0) return null; - return result[0]; + ); } function defaultFilter(node) { @@ -70,6 +65,10 @@ function removeBadProperties(node) { return node; } +function getPropMatchAsString(prop) { + return typeof prop === 'string' ? prop : JSON.stringify(prop); +} + function queryAllByProp( attribute, container, @@ -79,9 +78,8 @@ function queryAllByProp( const baseElement = getBaseElement(container); const matcher = exact ? matches : fuzzyMatches; const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer }); - const allNodes = Array.from(baseElement.findAll(c => c.props[attribute])); - return allNodes + return Array.from(baseElement.findAll(c => c.props[attribute])) .filter((node, index) => (filter ? filter(node, index) : defaultFilter(node, index))) .filter(({ props }) => typeof props[attribute] === 'string' @@ -91,17 +89,70 @@ function queryAllByProp( .map(removeBadProperties); } -function queryByProp(...args) { - return firstResultOrNull(queryAllByProp, ...args); +function queryByProp(prop, container, match, ...args) { + const els = queryAllByProp(prop, container, match, ...args); + if (els.length > 1) { + throw getMultipleElementsFoundError( + `Found multiple elements by [${prop}=${getPropMatchAsString(match)}]`, + container, + ); + } + return els[0] || null; +} + +// this accepts a query function and returns a function which throws an error +// if more than one elements is returned, otherwise it returns the first +// element or null +function makeSingleQuery(allQuery, getMultipleError) { + return (container, ...args) => { + const els = allQuery(container, ...args); + if (els.length > 1) { + throw getMultipleElementsFoundError(getMultipleError(container, ...args), container); + } + return els[0] || null; + }; +} + +// this accepts a query function and returns a function which throws an error +// if an empty list of elements is returned +function makeGetAllQuery(allQuery, getMissingError) { + return (container, ...args) => { + const els = allQuery(container, ...args); + if (!els.length) { + throw getElementError(getMissingError(container, ...args), container); + } + return els; + }; +} + +// this accepts a getter query function and returns a function which calls +// waitForElement and passing a function which invokes the getter. +function makeFindQuery(getter) { + return (container, text, options, waitForElementOptions) => + waitForElement(() => getter(container, text, options), waitForElementOptions); +} + +function buildQueries(queryAllBy, getMultipleError, getMissingError) { + const queryBy = makeSingleQuery(queryAllBy, getMultipleError); + const getAllBy = makeGetAllQuery(queryAllBy, getMissingError); + const getBy = makeSingleQuery(getAllBy, getMultipleError); + const findAllBy = makeFindQuery(getAllBy); + const findBy = makeFindQuery(getBy); + + return [queryBy, getAllBy, getBy, findAllBy, findBy]; } export { + buildQueries, defaultFilter, filterNodeByType, - firstResultOrNull, getBaseElement, getElementError, - getGetByElementError, + getMultipleElementsFoundError, + getPropMatchAsString, + makeFindQuery, + makeGetAllQuery, + makeSingleQuery, queryAllByProp, queryByProp, removeBadProperties, From 921d90b000804451ea188be22ffa049c64d991e1 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Thu, 18 Apr 2019 22:55:51 -0400 Subject: [PATCH 3/5] chore: bump test coverage to 100% --- src/__tests__/misc.js | 14 ++++++++------ src/__tests__/text-matchers.js | 5 +++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/__tests__/misc.js b/src/__tests__/misc.js index 804cfad..398557d 100644 --- a/src/__tests__/misc.js +++ b/src/__tests__/misc.js @@ -4,15 +4,17 @@ import { View } from 'react-native'; import { render } from '../'; import { queryByProp } from '../'; -// we used to use queryByAttribute internally, but we don't anymore. Some people +// we used to use queryByProp internally, but we don't anymore. Some people // use it as an undocumented part of the API, so we'll keep it around. -test('queryByAttribute', () => { +test('queryByProp', () => { const { container } = render( - - + + , ); - expect(queryByProp('pointerEvents', container, 'none')).not.toBeNull(); - expect(queryByProp('collapsable', container, false)).toBeNull(); + + expect(queryByProp('importantForAccessibility', container, 'no')).not.toBeNull(); + expect(queryByProp('importantForAccessibility', container, 'auto')).toBeNull(); + expect(() => queryByProp('importantForAccessibility', container, /no/)).toThrow(/multiple/); }); diff --git a/src/__tests__/text-matchers.js b/src/__tests__/text-matchers.js index c4a5ffa..b3fd6a4 100644 --- a/src/__tests__/text-matchers.js +++ b/src/__tests__/text-matchers.js @@ -141,6 +141,11 @@ cases( query: `Dwayne 'The Rock' Johnson`, queryFn: `queryAllByPlaceholder`, }, + queryAllByValue: { + tree: , + query: `Dwayne 'The Rock' Johnson`, + queryFn: `queryAllByValue`, + }, queryAllByAccessibilityLabel: { tree: , query: `Finding Nemo poster`, From 1bb9f9ae7b8d94bf33a37ed3e6aea98f43ebe726 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Wed, 24 Apr 2019 20:49:29 -0400 Subject: [PATCH 4/5] fix: change wording on comments --- src/__tests__/misc.js | 6 +++--- src/query-helpers.js | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/__tests__/misc.js b/src/__tests__/misc.js index 398557d..0015091 100644 --- a/src/__tests__/misc.js +++ b/src/__tests__/misc.js @@ -2,19 +2,19 @@ import React from 'react'; import { View } from 'react-native'; import { render } from '../'; -import { queryByProp } from '../'; +import { queryByProp, queryByTestId } from '../'; // we used to use queryByProp internally, but we don't anymore. Some people // use it as an undocumented part of the API, so we'll keep it around. test('queryByProp', () => { const { container } = render( - + , ); - expect(queryByProp('importantForAccessibility', container, 'no')).not.toBeNull(); + expect(queryByTestId(container, 'foo')).not.toBeNull(); expect(queryByProp('importantForAccessibility', container, 'auto')).toBeNull(); expect(() => queryByProp('importantForAccessibility', container, /no/)).toThrow(/multiple/); }); diff --git a/src/query-helpers.js b/src/query-helpers.js index a3875a6..8a2c8a8 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -100,9 +100,8 @@ function queryByProp(prop, container, match, ...args) { return els[0] || null; } -// this accepts a query function and returns a function which throws an error -// if more than one elements is returned, otherwise it returns the first -// element or null +// accepts a query and returns a function that throws if more than one element is returned, otherwise +// returns the result or null function makeSingleQuery(allQuery, getMultipleError) { return (container, ...args) => { const els = allQuery(container, ...args); @@ -113,8 +112,7 @@ function makeSingleQuery(allQuery, getMultipleError) { }; } -// this accepts a query function and returns a function which throws an error -// if an empty list of elements is returned +// accepts a query and returns a function that throws if an empty list is returned function makeGetAllQuery(allQuery, getMissingError) { return (container, ...args) => { const els = allQuery(container, ...args); @@ -125,8 +123,7 @@ function makeGetAllQuery(allQuery, getMissingError) { }; } -// this accepts a getter query function and returns a function which calls -// waitForElement and passing a function which invokes the getter. +// accepts a getter and returns a function that calls waitForElement which invokes the getter. function makeFindQuery(getter) { return (container, text, options, waitForElementOptions) => waitForElement(() => getter(container, text, options), waitForElementOptions); From 94a1349abedb41492cfa89a27db57ad858b99e28 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Wed, 24 Apr 2019 20:50:38 -0400 Subject: [PATCH 5/5] fix: be consistent with checking for exceptions --- src/__tests__/misc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/__tests__/misc.js b/src/__tests__/misc.js index 0015091..3608f03 100644 --- a/src/__tests__/misc.js +++ b/src/__tests__/misc.js @@ -16,5 +16,7 @@ test('queryByProp', () => { expect(queryByTestId(container, 'foo')).not.toBeNull(); expect(queryByProp('importantForAccessibility', container, 'auto')).toBeNull(); - expect(() => queryByProp('importantForAccessibility', container, /no/)).toThrow(/multiple/); + expect(() => queryByProp('importantForAccessibility', container, /no/)).toThrow( + /multiple elements/, + ); });