diff --git a/src/__tests__/byDisplayValue.test.js b/src/__tests__/byDisplayValue.test.js index 4bdc743d9..953a3157e 100644 --- a/src/__tests__/byDisplayValue.test.js +++ b/src/__tests__/byDisplayValue.test.js @@ -100,3 +100,65 @@ test('findBy queries work asynchronously', async () => { await expect(findByDisplayValue('Display Value')).resolves.toBeTruthy(); await expect(findAllByDisplayValue('Display Value')).resolves.toHaveLength(1); }, 20000); + +test('all queries should respect accessibility', async () => { + const OtherComp = () => ( + + + + + + + + + ); + const Comp = () => ( + + + + + + + + + + + + ); + + const { + findAllByDisplayValue, + findByDisplayValue, + getAllByDisplayValue, + queryAllByDisplayValue, + getByDisplayValue, + queryByDisplayValue, + } = render(, { respectAccessibility: true }); + + await expect(getAllByDisplayValue('test_02')).toHaveLength(2); + await expect(() => getAllByDisplayValue('test_01')).toThrow( + 'Unable to find an element with displayValue: test_01' + ); + await expect(() => getByDisplayValue('test_02')).toThrow( + 'Found multiple elements with display value: test_02 ' + ); + await expect(() => getByDisplayValue('test_01')).toThrow( + 'Unable to find an element with displayValue: test_01' + ); + await expect(queryAllByDisplayValue('test_01')).toHaveLength(0); + await expect(queryAllByDisplayValue('test_02')).toHaveLength(2); + await expect(queryByDisplayValue('test_01')).toBeNull(); + await expect(() => queryByDisplayValue('test_02')).toThrow( + 'Found multiple elements with display value: test_02 ' + ); + await expect(findAllByDisplayValue('test_01')).rejects.toEqual( + new Error('Unable to find an element with displayValue: test_01') + ); + await expect(findAllByDisplayValue('test_02')).resolves.toHaveLength(2); + await expect(findByDisplayValue('test_01')).rejects.toEqual( + new Error('Unable to find an element with displayValue: test_01') + ); + await expect(findByDisplayValue('test_02')).rejects.toEqual( + new Error('Found multiple elements with display value: test_02 ') + ); +}); diff --git a/src/__tests__/byPlaceholderText.test.js b/src/__tests__/byPlaceholderText.test.js index 6e4ee1c00..120320bad 100644 --- a/src/__tests__/byPlaceholderText.test.js +++ b/src/__tests__/byPlaceholderText.test.js @@ -59,3 +59,67 @@ test('getAllByPlaceholderText, queryAllByPlaceholderText', () => { expect(queryAllByPlaceholderText(/fresh/i)).toEqual(inputs); expect(queryAllByPlaceholderText('no placeholder')).toHaveLength(0); }); + +test('queries should respect accessibility', async () => { + const OtherComp = () => ( + + + + + + + + + ); + const Comp = () => ( + + + + + + + + + + + + ); + + const { + findAllByPlaceholderText, + findByPlaceholderText, + getAllByPlaceholderText, + getByPlaceholderText, + queryAllByPlaceholderText, + queryByPlaceholderText, + } = render(, { + respectAccessibility: true, + }); + + await expect(() => getAllByPlaceholderText('test_01')).toThrow( + 'Unable to find an element with placeholder: test_01' + ); + await expect(getAllByPlaceholderText('test_02')).toHaveLength(2); + await expect(() => getByPlaceholderText('test_01')).toThrow( + 'Unable to find an element with placeholder: test_01' + ); + await expect(() => getByPlaceholderText('test_02')).toThrow( + 'Found multiple elements with placeholder: test_02 ' + ); + await expect(queryAllByPlaceholderText('test_01')).toHaveLength(0); + await expect(queryAllByPlaceholderText('test_02')).toHaveLength(2); + await expect(queryByPlaceholderText('test_01')).toBeNull(); + await expect(() => queryByPlaceholderText('test_02')).toThrow( + 'Found multiple elements with placeholder: test_02 ' + ); + await expect(findAllByPlaceholderText('test_01')).rejects.toEqual( + new Error('Unable to find an element with placeholder: test_01') + ); + await expect(findAllByPlaceholderText('test_02')).resolves.toHaveLength(2); + await expect(findByPlaceholderText('test_01')).rejects.toEqual( + new Error('Unable to find an element with placeholder: test_01') + ); + await expect(findByPlaceholderText('test_02')).rejects.toEqual( + new Error('Found multiple elements with placeholder: test_02 ') + ); +}); diff --git a/src/__tests__/byTestId.test.js b/src/__tests__/byTestId.test.js index 3f8363e5a..c5d913af5 100644 --- a/src/__tests__/byTestId.test.js +++ b/src/__tests__/byTestId.test.js @@ -133,3 +133,67 @@ test('findByTestId and findAllByTestId work asynchronously', async () => { await expect(findByTestId('aTestId')).resolves.toBeTruthy(); await expect(findAllByTestId('aTestId')).resolves.toHaveLength(1); }, 20000); + +test('queries should respect accessibility', async () => { + const OtherComp = () => ( + + + + + + + + + ); + const Comp = () => ( + + + + + + + + + + + + ); + + const { + findAllByTestId, + findByTestId, + getAllByTestId, + getByTestId, + queryAllByTestId, + queryByTestId, + } = render(, { + respectAccessibility: true, + }); + + await expect(() => getAllByTestId('test_01')).toThrow( + 'Unable to find an element with testID: test' + ); + await expect(getAllByTestId('test_02')).toHaveLength(2); + await expect(() => getByTestId('test_01')).toThrow( + 'Unable to find an element with testID: test_01' + ); + await expect(() => getByTestId('test_02')).toThrow( + 'Found multiple elements with testID: test_02' + ); + await expect(queryAllByTestId('test_01')).toHaveLength(0); + await expect(queryAllByTestId('test_02')).toHaveLength(2); + await expect(queryByTestId('test_01')).toBeNull(); + await expect(() => queryByTestId('test_02')).toThrow( + 'Found multiple elements with testID: test_02' + ); + await expect(findAllByTestId('test_01')).rejects.toEqual( + new Error('Unable to find an element with testID: test_01') + ); + await expect(findAllByTestId('test_02')).resolves.toHaveLength(2); + await expect(findByTestId('test_01')).rejects.toEqual( + new Error('Unable to find an element with testID: test_01') + ); + await expect(findByTestId('test_02')).rejects.toEqual( + new Error('Found multiple elements with testID: test_02') + ); +}); diff --git a/src/__tests__/byText.test.js b/src/__tests__/byText.test.js index 5b4ff36dd..1de4ae702 100644 --- a/src/__tests__/byText.test.js +++ b/src/__tests__/byText.test.js @@ -85,6 +85,68 @@ test('getAllByText, queryAllByText', () => { expect(queryAllByText('InExistent')).toHaveLength(0); }); +test('queries should respect accessibility', async () => { + const OtherComp = () => ( + + + test_01 + + + test_02 + + + ); + const Comp = () => ( + + + + + test_02 + + test_01 + + + ); + + const { + findAllByText, + findByText, + getAllByText, + getByText, + queryAllByText, + queryByText, + } = render(, { + respectAccessibility: true, + }); + + await expect(() => getAllByText('test_01')).toThrow( + 'Unable to find an element with text: test_01' + ); + await expect(getAllByText('test_02')).toHaveLength(2); + await expect(() => getByText('test_01')).toThrow( + 'Unable to find an element with text: test_01' + ); + await expect(() => getByText('test_02')).toThrow( + 'Found multiple elements with text: test_02' + ); + await expect(queryAllByText('test_01')).toHaveLength(0); + await expect(queryAllByText('test_02')).toHaveLength(2); + await expect(queryByText('test_01')).toBeNull(); + await expect(() => queryByText('test_02')).toThrow( + 'Found multiple elements with text: test_02' + ); + await expect(findAllByText('test_01')).rejects.toEqual( + new Error('Unable to find an element with text: test_01') + ); + await expect(findAllByText('test_02')).resolves.toHaveLength(2); + await expect(findByText('test_01')).rejects.toEqual( + new Error('Unable to find an element with text: test_01') + ); + await expect(findByText('test_02')).rejects.toEqual( + new Error('Found multiple elements with text: test_02') + ); +}); + test('findByText queries work asynchronously', async () => { const options = { timeout: 10 }; // Short timeout so that this test runs quickly const { rerender, findByText, findAllByText } = render(); diff --git a/src/render.js b/src/render.js index e6323d58d..b8e4a4e8f 100644 --- a/src/render.js +++ b/src/render.js @@ -13,6 +13,7 @@ import debugDeep from './helpers/debugDeep'; type Options = { wrapper?: React.ComponentType, createNodeMock?: (element: React.Element) => any, + respectAccessibility?: boolean, }; type TestRendererOptions = { createNodeMock: (element: React.Element) => any, @@ -24,7 +25,7 @@ type TestRendererOptions = { */ export default function render( component: React.Element, - { wrapper: Wrapper, createNodeMock }: Options = {} + { wrapper: Wrapper, createNodeMock, respectAccessibility }: Options = {} ): { ...FindByAPI, ...QueryByAPI, @@ -45,7 +46,9 @@ export default function render( createNodeMock ? { createNodeMock } : undefined ); const update = updateWithAct(renderer, wrap); - const instance = renderer.root; + const instance = respectAccessibility + ? appendFindAllTrap(renderer) + : renderer.root; const unmount = () => { act(() => { renderer.unmount(); @@ -107,3 +110,64 @@ function debug( debugImpl.shallow = (message) => debugShallow(instance, message); return debugImpl; } + +function appendFindAllTrap(renderer: ReactTestRenderer) { + return new Proxy(renderer.root, { + get(target, prop) { + const isFindAllProp = prop === 'findAll'; + + return isFindAllProp ? newFindAll(target) : target[prop]; + }, + }); +} + +function newFindAll(instance: ReactTestInstance) { + return ( + predicate: (instance: ReactTestInstance) => boolean + ): ReactTestInstance[] => { + const elements = instance.findAll(predicate); + + return elements.filter(isReactTestElementVisibleToAccessibility); + }; +} + +function isReactTestElementVisibleToAccessibility( + instance: ReactTestInstance +): boolean { + const isElementVisible = + !accessibilityHiddenIOS(instance) && + !accessibilityHiddenAndroid(instance) && + !hiddenByStyles(instance); + + if (!instance.parent) { + return isElementVisible; + } + + const isParentVisible = isReactTestElementVisibleToAccessibility( + instance.parent + ); + + return isParentVisible && isElementVisible; +} + +function accessibilityHiddenIOS(instance: ReactTestInstance): boolean { + const siblingHasAccessibilityViewIsModal = + instance.parent && + instance.parent.children.some( + (c) => + c.props && c.props.accessibilityViewIsModal && !Object.is(c, instance) + ); + + return ( + instance.props.accessibilityElementsHidden || + siblingHasAccessibilityViewIsModal + ); +} + +function accessibilityHiddenAndroid(instance: ReactTestInstance) { + return instance.props.importantForAccessibility === 'no-hide-descendants'; +} + +function hiddenByStyles(instance: ReactTestInstance) { + return instance.props.style && instance.props.style.display === 'none'; +} diff --git a/typings/index.d.ts b/typings/index.d.ts index c0188727d..6683e347f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -296,6 +296,7 @@ export interface Thenable { export interface RenderOptions { wrapper?: React.ComponentType; createNodeMock?: (element: React.ReactElement) => any; + respectAccessibility?: boolean; } type Debug = {