diff --git a/package.json b/package.json index 6a92521a8..b294677a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@testing-library/react-native", - "version": "9.1.0", + "version": "9.0.0-alpha.0", "description": "Simple and complete React Native testing utilities that encourage good testing practices.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/__tests__/a11yAPI.test.tsx b/src/__tests__/a11yAPI.test.tsx index 31107484c..aa39de144 100644 --- a/src/__tests__/a11yAPI.test.tsx +++ b/src/__tests__/a11yAPI.test.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; -import { TouchableOpacity, Text } from 'react-native'; +import { TouchableOpacity, Text, View } from 'react-native'; import { render } from '../index'; const BUTTON_LABEL = 'cool button'; const BUTTON_HINT = 'click this button'; const TEXT_LABEL = 'cool text'; const TEXT_HINT = 'static text'; +const ONE_OCCURANCE = 'more words'; +const TWO_OCCURANCE = 'cooler text'; // Little hack to make all the methods happy with type const NO_MATCHES_TEXT: any = 'not-existent-element'; const FOUND_TWO_INSTANCES = 'Expected 1 but found 2 instances'; @@ -69,6 +71,22 @@ function Section() { ); } +function ButtonsWithText() { + return ( + <> + + {TWO_OCCURANCE} + + + {TWO_OCCURANCE} + + + {ONE_OCCURANCE} + + + ); +} + test('getByLabelText, queryByLabelText, findByLabelText', async () => { const { getByLabelText, queryByLabelText, findByLabelText } = render(
@@ -183,7 +201,7 @@ test('getByRole, queryByRole, findByRole', async () => { const asyncButton = await findByRole('button'); expect(asyncButton.props.accessibilityRole).toEqual('button'); - await expect(findByRole(NO_MATCHES_TEXT, waitForOptions)).rejects.toThrow( + await expect(findByRole(NO_MATCHES_TEXT, {}, waitForOptions)).rejects.toThrow( getNoInstancesFoundMessage('accessibilityRole') ); await expect(findByRole('link')).rejects.toThrow(FOUND_TWO_INSTANCES); @@ -201,9 +219,64 @@ test('getAllByRole, queryAllByRole, findAllByRole', async () => { expect(queryAllByRole(NO_MATCHES_TEXT)).toEqual([]); await expect(findAllByRole('link')).resolves.toHaveLength(2); - await expect(findAllByRole(NO_MATCHES_TEXT, waitForOptions)).rejects.toThrow( - getNoInstancesFoundMessage('accessibilityRole') + await expect( + findAllByRole(NO_MATCHES_TEXT, {}, waitForOptions) + ).rejects.toThrow(getNoInstancesFoundMessage('accessibilityRole')); +}); + +describe('findBy options deprecations', () => { + let warnSpy: jest.SpyInstance; + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + afterEach(() => { + warnSpy.mockRestore(); + }); + + test('findByText queries warn on deprecated use of WaitForOptions', async () => { + const options = { timeout: 10 }; + // mock implementation to avoid warning in the test suite + const { rerender, findByText } = render(); + await expect(findByText('Some Text', options)).rejects.toBeTruthy(); + + setTimeout( + () => + rerender( + + Some Text + + ), + 20 + ); + + await expect(findByText('Some Text')).resolves.toBeTruthy(); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Use of option "timeout"') + ); + }, 20000); +}); + +test('getAllByRole, queryAllByRole, findAllByRole with name', async () => { + const { getAllByRole, queryAllByRole, findAllByRole } = render( + + ); + + expect(getAllByRole('button', { name: TWO_OCCURANCE })).toHaveLength(2); + expect(getAllByRole('button', { name: ONE_OCCURANCE })).toHaveLength(1); + expect(queryAllByRole('button', { name: TWO_OCCURANCE })).toHaveLength(2); + + expect(() => getAllByRole('button', { name: NO_MATCHES_TEXT })).toThrow( + getNoInstancesFoundMessage('accessibilityRole', 'button') ); + expect(queryAllByRole('button', { name: NO_MATCHES_TEXT })).toEqual([]); + + await expect( + findAllByRole('button', { name: TWO_OCCURANCE }) + ).resolves.toHaveLength(2); + await expect( + findAllByRole('button', { name: NO_MATCHES_TEXT }) + ).rejects.toThrow(getNoInstancesFoundMessage('accessibilityRole', 'button')); }); // TODO: accessibilityStates was removed from RN 0.62 diff --git a/src/helpers/a11yAPI.ts b/src/helpers/a11yAPI.ts index 4b9f42e53..c5b4c0ffc 100644 --- a/src/helpers/a11yAPI.ts +++ b/src/helpers/a11yAPI.ts @@ -1,5 +1,5 @@ import type { ReactTestInstance } from 'react-test-renderer'; -import type { AccessibilityRole, AccessibilityState } from 'react-native'; +import type { AccessibilityState } from 'react-native'; import type { WaitForOptions } from '../waitFor'; import type { TextMatch } from '../matches'; import makeA11yQuery from './makeA11yQuery'; @@ -11,6 +11,34 @@ type A11yValue = { now?: number; text?: string; }; +type A11yRole = + | 'none' + | 'button' + | 'link' + | 'search' + | 'image' + | 'keyboardkey' + | 'text' + | 'adjustable' + | 'imagebutton' + | 'header' + | 'summary' + | 'alert' + | 'checkbox' + | 'combobox' + | 'menu' + | 'menubar' + | 'menuitem' + | 'progressbar' + | 'radio' + | 'radiogroup' + | 'scrollbar' + | 'spinbutton' + | 'switch' + | 'tab' + | 'tablist' + | 'timer' + | 'toolbar'; type GetReturn = ReactTestInstance; type GetAllReturn = Array; @@ -19,6 +47,10 @@ type QueryAllReturn = Array; type FindReturn = Promise; type FindAllReturn = Promise; +export type QueryOptions = { + name?: string | RegExp; +}; + export type A11yAPI = { // Label getByLabelText: (label: TextMatch) => GetReturn; @@ -61,16 +93,30 @@ export type A11yAPI = { ) => FindAllReturn; // Role - getByRole: (role: AccessibilityRole | RegExp) => GetReturn; - getAllByRole: (role: AccessibilityRole | RegExp) => GetAllReturn; - queryByRole: (role: AccessibilityRole | RegExp) => QueryReturn; - queryAllByRole: (role: AccessibilityRole | RegExp) => QueryAllReturn; + getByRole: ( + role: A11yRole | RegExp, + queryOptions?: QueryOptions + ) => GetReturn; + getAllByRole: ( + role: A11yRole | RegExp, + queryOptions?: QueryOptions + ) => GetAllReturn; + queryByRole: ( + role: A11yRole | RegExp, + queryOptions?: QueryOptions + ) => QueryReturn; + queryAllByRole: ( + role: A11yRole | RegExp, + queryOptions?: QueryOptions + ) => QueryAllReturn; findByRole: ( - role: AccessibilityRole, + role: A11yRole, + queryOptions?: QueryOptions & WaitForOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByRole: ( - role: AccessibilityRole, + role: A11yRole, + queryOptions?: QueryOptions & WaitForOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; diff --git a/src/helpers/makeA11yQuery.ts b/src/helpers/makeA11yQuery.ts index 658f36f96..5fa7d5b99 100644 --- a/src/helpers/makeA11yQuery.ts +++ b/src/helpers/makeA11yQuery.ts @@ -1,5 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import waitFor from '../waitFor'; +import { getQueriesForElement } from '../within'; import type { WaitForOptions } from '../waitFor'; import { ErrorWithStack, @@ -17,6 +18,39 @@ function makeAliases(aliases: Array, query: Function) { .reduce((acc, query) => ({ ...acc, ...query }), {}); } +// The WaitForOptions has been moved to the third param of findBy* methods with the addition of QueryOptions. +// To make the migration easier and to avoid a breaking change, keep reading these options from second param +// but warn. +const deprecatedKeys: (keyof WaitForOptions)[] = [ + 'timeout', + 'interval', + 'stackTraceError', +]; +const warnDeprectedWaitForOptionsUsage = (queryOptions?: WaitForOptions) => { + if (queryOptions) { + const waitForOptions: WaitForOptions = { + timeout: queryOptions.timeout, + interval: queryOptions.interval, + stackTraceError: queryOptions.stackTraceError, + }; + deprecatedKeys.forEach((key) => { + if (queryOptions[key]) { + // eslint-disable-next-line no-console + console.warn( + `Use of option "${key}" in a findBy* query's second parameter, QueryOptions, is deprecated. Please pass this option in the third, WaitForOptions, parameter. +Example: + findByText(text, {}, { ${key}: ${queryOptions[key]} })` + ); + } + }); + return waitForOptions; + } +}; + +type QueryOptions = { + name: string | RegExp; +}; + type QueryNames = { getBy: Array; getAllBy: Array; @@ -31,8 +65,27 @@ const makeA11yQuery =

( queryNames: QueryNames, matcherFn: (prop: P, value: M) => boolean ) => (instance: ReactTestInstance) => { - const getBy = (matcher: M) => { + const filterWithName = ( + node: ReactTestInstance, + options: QueryOptions, + matcher: M + ) => { + const matchesRole = + isNodeValid(node) && matcherFn(node.props[name], matcher); + + return ( + matchesRole && !!getQueriesForElement(node).queryByText(options.name) + ); + }; + + const getBy = (matcher: M, queryOptions?: QueryOptions) => { try { + if (queryOptions?.name) { + return instance.find((node) => + filterWithName(node, queryOptions, matcher) + ); + } + return instance.find( (node) => isNodeValid(node) && matcherFn(node.props[name], matcher) ); @@ -44,10 +97,18 @@ const makeA11yQuery =

( } }; - const getAllBy = (matcher: M) => { - const results = instance.findAll( - (node) => isNodeValid(node) && matcherFn(node.props[name], matcher) - ); + const getAllBy = (matcher: M, options?: QueryOptions) => { + let results = []; + + if (options?.name) { + results = instance.findAll((node) => + filterWithName(node, options, matcher) + ); + } else { + results = instance.findAll( + (node) => isNodeValid(node) && matcherFn(node.props[name], matcher) + ); + } if (results.length === 0) { throw new ErrorWithStack( @@ -59,28 +120,50 @@ const makeA11yQuery =

( return results; }; - const queryBy = (matcher: M) => { + const queryBy = (matcher: M, options?: QueryOptions) => { try { - return getBy(matcher); + return getBy(matcher, options); } catch (error) { return createQueryByError(error, queryBy); } }; - const queryAllBy = (matcher: M) => { + const queryAllBy = (matcher: M, options?: QueryOptions) => { try { - return getAllBy(matcher); + return getAllBy(matcher, options); } catch (error) { return []; } }; - const findBy = (matcher: M, waitForOptions?: WaitForOptions) => { - return waitFor(() => getBy(matcher), waitForOptions); + const findBy = ( + matcher: M, + queryOptions?: QueryOptions & WaitForOptions, + waitForOptions?: WaitForOptions + ) => { + const deprecatedWaitForOptions = warnDeprectedWaitForOptionsUsage( + queryOptions + ); + + return waitFor(() => getBy(matcher, queryOptions), { + ...deprecatedWaitForOptions, + ...waitForOptions, + }); }; - const findAllBy = (matcher: M, waitForOptions?: WaitForOptions) => { - return waitFor(() => getAllBy(matcher), waitForOptions); + const findAllBy = ( + matcher: M, + queryOptions?: QueryOptions & WaitForOptions, + waitForOptions?: WaitForOptions + ) => { + const deprecatedWaitForOptions = warnDeprectedWaitForOptionsUsage( + queryOptions + ); + + return waitFor(() => getAllBy(matcher, queryOptions), { + ...deprecatedWaitForOptions, + ...waitForOptions, + }); }; return {