From 02011821ca27d749ef5c87b4581383fb163c7f5e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 14 Feb 2023 10:08:30 +0100 Subject: [PATCH 01/10] chore: v12 & update contributors --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fca5a71c6..83d9c1bbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@testing-library/react-native", - "version": "11.5.4", + "version": "12.0.0-rc.0", "description": "Simple and complete React Native testing utilities that encourage good testing practices.", "main": "build/index.js", "types": "build/index.d.ts", @@ -10,6 +10,12 @@ }, "homepage": "https://callstack.github.io/react-native-testing-library", "author": "Michał Pierzchała ", + "contributors": [ + "Maciej Jastrzębski (https://github.com/mdjastrzebski)", + "Augustin Le Fèvre (https://github.com/AugustinLF)", + "Pierre Zimmermann (https://github.com/pierrezimmermannbam)", + "MattAgn (https://github.com/MattAgn)" + ], "license": "MIT", "private": false, "keywords": [ From 20fcb18ec2809f7814c2e263222ff84e90427a76 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 16 Feb 2023 14:27:29 +0100 Subject: [PATCH 02/10] [v12] refactor: enable all breaking changes (#1313) * chore: enable all breaking changes * fix: ci issues * chore: fix lint * chore: cleanup * chore: add migration guide * chore: update docs * chore: docs tweaks * chore: docs tweaks * chore: remove useBreakingChanges config flag * refactor: code review changes * docs: tweaks --- .../render.breaking.test.tsx.snap | 39 - src/__tests__/config.test.ts | 8 +- src/__tests__/render.breaking.test.tsx | 247 ------ src/__tests__/render.test.tsx | 41 +- src/config.ts | 4 - .../__tests__/displayValue.breaking.test.tsx | 138 ---- src/queries/__tests__/displayValue.test.tsx | 14 +- .../placeholderText.breaking.test.tsx | 95 --- .../__tests__/placeholderText.test.tsx | 4 +- src/queries/__tests__/role.breaking.test.tsx | 769 ------------------ src/queries/__tests__/role.test.tsx | 34 +- src/queries/__tests__/text.breaking.test.tsx | 514 ------------ src/queries/__tests__/text.test.tsx | 4 +- src/queries/displayValue.ts | 8 +- src/queries/placeholderText.ts | 8 +- src/queries/role.ts | 6 +- src/queries/text.ts | 29 - src/render.tsx | 6 +- typings/index.flow.js | 1 - website/docs/MigrationV12.md | 60 ++ website/sidebars.js | 1 + 21 files changed, 121 insertions(+), 1909 deletions(-) delete mode 100644 src/__tests__/__snapshots__/render.breaking.test.tsx.snap delete mode 100644 src/__tests__/render.breaking.test.tsx delete mode 100644 src/queries/__tests__/displayValue.breaking.test.tsx delete mode 100644 src/queries/__tests__/placeholderText.breaking.test.tsx delete mode 100644 src/queries/__tests__/role.breaking.test.tsx delete mode 100644 src/queries/__tests__/text.breaking.test.tsx create mode 100644 website/docs/MigrationV12.md diff --git a/src/__tests__/__snapshots__/render.breaking.test.tsx.snap b/src/__tests__/__snapshots__/render.breaking.test.tsx.snap deleted file mode 100644 index 018ae7d63..000000000 --- a/src/__tests__/__snapshots__/render.breaking.test.tsx.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`toJSON renders host output 1`] = ` - - - press me - - -`; diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 03de47adc..f8f5c9f42 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -6,7 +6,6 @@ import { } from '../config'; test('getConfig() returns existing configuration', () => { - expect(getConfig().useBreakingChanges).toEqual(false); expect(getConfig().asyncUtilTimeout).toEqual(1000); expect(getConfig().defaultIncludeHiddenElements).toEqual(true); }); @@ -18,7 +17,6 @@ test('configure() overrides existing config values', () => { asyncUtilTimeout: 5000, defaultDebugOptions: { message: 'debug message' }, defaultIncludeHiddenElements: true, - useBreakingChanges: false, }); }); @@ -37,12 +35,12 @@ test('resetToDefaults() resets config to defaults', () => { test('resetToDefaults() resets internal config to defaults', () => { configureInternal({ - useBreakingChanges: true, + hostComponentNames: { text: 'A', textInput: 'A' }, }); - expect(getConfig().useBreakingChanges).toEqual(true); + expect(getConfig().hostComponentNames).toEqual({ text: 'A', textInput: 'A' }); resetToDefaults(); - expect(getConfig().useBreakingChanges).toEqual(false); + expect(getConfig().hostComponentNames).toBe(undefined); }); test('configure handles alias option defaultHidden', () => { diff --git a/src/__tests__/render.breaking.test.tsx b/src/__tests__/render.breaking.test.tsx deleted file mode 100644 index ac6492664..000000000 --- a/src/__tests__/render.breaking.test.tsx +++ /dev/null @@ -1,247 +0,0 @@ -/** This is a copy of regular tests with `useBreakingChanges` flag turned on. */ - -/* eslint-disable no-console */ -import * as React from 'react'; -import { View, Text, TextInput, Pressable } from 'react-native'; -import { render, screen, fireEvent, RenderAPI } from '..'; -import { configureInternal } from '../config'; - -beforeEach(() => { - configureInternal({ useBreakingChanges: true }); -}); - -const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; -const PLACEHOLDER_CHEF = 'Who inspected freshness?'; -const INPUT_FRESHNESS = 'Custom Freshie'; -const INPUT_CHEF = 'I inspected freshie'; -const DEFAULT_INPUT_CHEF = 'What did you inspect?'; -const DEFAULT_INPUT_CUSTOMER = 'What banana?'; - -class MyButton extends React.Component { - render() { - return ( - - {this.props.children} - - ); - } -} - -class Banana extends React.Component { - state = { - fresh: false, - }; - - componentDidUpdate() { - if (this.props.onUpdate) { - this.props.onUpdate(); - } - } - - componentWillUnmount() { - if (this.props.onUnmount) { - this.props.onUnmount(); - } - } - - changeFresh = () => { - this.setState((state) => ({ - fresh: !state.fresh, - })); - }; - - render() { - const test = 0; - return ( - - Is the banana fresh? - - {this.state.fresh ? 'fresh' : 'not fresh'} - - - - - - - Change freshness! - - First Text - Second Text - {test} - - ); - } -} - -test('UNSAFE_getAllByType, UNSAFE_queryAllByType', () => { - const { UNSAFE_getAllByType, UNSAFE_queryAllByType } = render(); - const [text, status, button] = UNSAFE_getAllByType(Text); - const InExistent = () => null; - - expect(text.props.children).toBe('Is the banana fresh?'); - expect(status.props.children).toBe('not fresh'); - expect(button.props.children).toBe('Change freshness!'); - expect(() => UNSAFE_getAllByType(InExistent)).toThrow('No instances found'); - - expect(UNSAFE_queryAllByType(Text)[1]).toBe(status); - expect(UNSAFE_queryAllByType(InExistent)).toHaveLength(0); -}); - -test('UNSAFE_getByProps, UNSAFE_queryByProps', () => { - const { UNSAFE_getByProps, UNSAFE_queryByProps } = render(); - const primaryType = UNSAFE_getByProps({ type: 'primary' }); - - expect(primaryType.props.children).toBe('Change freshness!'); - expect(() => UNSAFE_getByProps({ type: 'inexistent' })).toThrow( - 'No instances found' - ); - - expect(UNSAFE_queryByProps({ type: 'primary' })).toBe(primaryType); - expect(UNSAFE_queryByProps({ type: 'inexistent' })).toBeNull(); -}); - -test('UNSAFE_getAllByProp, UNSAFE_queryAllByProps', () => { - const { UNSAFE_getAllByProps, UNSAFE_queryAllByProps } = render(); - const primaryTypes = UNSAFE_getAllByProps({ type: 'primary' }); - - expect(primaryTypes).toHaveLength(1); - expect(() => UNSAFE_getAllByProps({ type: 'inexistent' })).toThrow( - 'No instances found' - ); - - expect(UNSAFE_queryAllByProps({ type: 'primary' })).toEqual(primaryTypes); - expect(UNSAFE_queryAllByProps({ type: 'inexistent' })).toHaveLength(0); -}); - -test('update', () => { - const fn = jest.fn(); - const { getByText, update, rerender } = render(); - - fireEvent.press(getByText('Change freshness!')); - - update(); - rerender(); - - expect(fn).toHaveBeenCalledTimes(3); -}); - -test('unmount', () => { - const fn = jest.fn(); - const { unmount } = render(); - unmount(); - expect(fn).toHaveBeenCalled(); -}); - -test('unmount should handle cleanup functions', () => { - const cleanup = jest.fn(); - const Component = () => { - React.useEffect(() => cleanup); - return null; - }; - - const { unmount } = render(); - - unmount(); - - expect(cleanup).toHaveBeenCalledTimes(1); -}); - -test('toJSON renders host output', () => { - const { toJSON } = render(press me); - expect(toJSON()).toMatchSnapshot(); -}); - -test('renders options.wrapper around node', () => { - type WrapperComponentProps = { children: React.ReactNode }; - const WrapperComponent = ({ children }: WrapperComponentProps) => ( - {children} - ); - - const { toJSON, getByTestId } = render(, { - wrapper: WrapperComponent, - }); - - expect(getByTestId('wrapper')).toBeTruthy(); - expect(toJSON()).toMatchInlineSnapshot(` - - - - `); -}); - -test('renders options.wrapper around updated node', () => { - type WrapperComponentProps = { children: React.ReactNode }; - const WrapperComponent = ({ children }: WrapperComponentProps) => ( - {children} - ); - - const { toJSON, getByTestId, rerender } = render(, { - wrapper: WrapperComponent, - }); - - rerender( - - ); - - expect(getByTestId('wrapper')).toBeTruthy(); - expect(toJSON()).toMatchInlineSnapshot(` - - - - `); -}); - -test('returns host root', () => { - const { root } = render(); - - expect(root).toBeDefined(); - expect(root.type).toBe('View'); - expect(root.props.testID).toBe('inner'); -}); - -test('returns composite UNSAFE_root', () => { - const { UNSAFE_root } = render(); - - expect(UNSAFE_root).toBeDefined(); - expect(UNSAFE_root.type).toBe(View); - expect(UNSAFE_root.props.testID).toBe('inner'); -}); - -test('container displays deprecation', () => { - const view = render(); - - expect(() => view.container).toThrowErrorMatchingInlineSnapshot(` - "'container' property has been renamed to 'UNSAFE_root'. - - Consider using 'root' property which returns root host element." - `); - expect(() => screen.container).toThrowErrorMatchingInlineSnapshot(` - "'container' property has been renamed to 'UNSAFE_root'. - - Consider using 'root' property which returns root host element." - `); -}); - -test('RenderAPI type', () => { - render() as RenderAPI; - expect(true).toBeTruthy(); -}); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 088d476bd..b08db541e 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -1,11 +1,7 @@ /* eslint-disable no-console */ import * as React from 'react'; -import { View, Text, TextInput, Pressable, SafeAreaView } from 'react-native'; -import { render, fireEvent, RenderAPI } from '..'; - -beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); -}); +import { View, Text, TextInput, Pressable } from 'react-native'; +import { render, screen, fireEvent, RenderAPI } from '..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; const PLACEHOLDER_CHEF = 'Who inspected freshness?'; @@ -154,7 +150,6 @@ test('unmount should handle cleanup functions', () => { test('toJSON renders host output', () => { const { toJSON } = render(press me); - expect(toJSON()).toMatchSnapshot(); }); @@ -224,31 +219,19 @@ test('returns composite UNSAFE_root', () => { expect(UNSAFE_root.props.testID).toBe('inner'); }); -test('returns container', () => { - const { container } = render(); +test('container displays deprecation', () => { + const view = render(); - expect(container).toBeDefined(); - // `View` composite component is returned. This behavior will break if we - // start returning only host components. - expect(container.type).toBe(View); - expect(container.props.testID).toBe('inner'); -}); - -test('returns wrapper component as container', () => { - type WrapperComponentProps = { children: React.ReactNode }; - const WrapperComponent = ({ children }: WrapperComponentProps) => ( - {children} - ); + expect(() => view.container).toThrowErrorMatchingInlineSnapshot(` + "'container' property has been renamed to 'UNSAFE_root'. - const { container } = render(, { - wrapper: WrapperComponent, - }); + Consider using 'root' property which returns root host element." + `); + expect(() => screen.container).toThrowErrorMatchingInlineSnapshot(` + "'container' property has been renamed to 'UNSAFE_root'. - expect(container).toBeDefined(); - // `WrapperComponent` composite component is returned with no testID passed to - // it. This behavior will break if we start returning only host components. - expect(container.type).toBe(WrapperComponent); - expect(container.props.testID).not.toBeDefined(); + Consider using 'root' property which returns root host element." + `); }); test('RenderAPI type', () => { diff --git a/src/config.ts b/src/config.ts index 732206e38..6b215b872 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,15 +26,11 @@ export type HostComponentNames = { }; export type InternalConfig = Config & { - /** Whether to use breaking changes intended for next major version release. */ - useBreakingChanges: boolean; - /** Names for key React Native host components. */ hostComponentNames?: HostComponentNames; }; const defaultConfig: InternalConfig = { - useBreakingChanges: false, asyncUtilTimeout: 1000, defaultIncludeHiddenElements: true, }; diff --git a/src/queries/__tests__/displayValue.breaking.test.tsx b/src/queries/__tests__/displayValue.breaking.test.tsx deleted file mode 100644 index 01949d375..000000000 --- a/src/queries/__tests__/displayValue.breaking.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/** This is a copy of regular tests with `useBreakingChanges` flag turned on. */ - -import * as React from 'react'; -import { View, TextInput } from 'react-native'; - -import { render } from '../..'; -import { configureInternal } from '../../config'; - -const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; -const PLACEHOLDER_CHEF = 'Who inspected freshness?'; -const INPUT_FRESHNESS = 'Custom Freshie'; -const INPUT_CHEF = 'I inspected freshie'; -const DEFAULT_INPUT_CHEF = 'What did you inspect?'; -const DEFAULT_INPUT_CUSTOMER = 'What banana?'; - -beforeEach(() => configureInternal({ useBreakingChanges: true })); - -const Banana = () => ( - - - - - - -); - -test('getByDisplayValue, queryByDisplayValue', () => { - const { getByDisplayValue, queryByDisplayValue } = render(); - const input = getByDisplayValue(/custom/i); - - expect(input.props.value).toBe(INPUT_FRESHNESS); - - const sameInput = getByDisplayValue(INPUT_FRESHNESS); - - expect(sameInput.props.value).toBe(INPUT_FRESHNESS); - expect(() => getByDisplayValue('no value')).toThrow( - 'Unable to find an element with displayValue: no value' - ); - - expect(queryByDisplayValue(/custom/i)).toBe(input); - expect(queryByDisplayValue('no value')).toBeNull(); - expect(() => queryByDisplayValue(/fresh/i)).toThrow( - 'Found multiple elements with display value: /fresh/i' - ); -}); - -test('getByDisplayValue, queryByDisplayValue get element by default value only when value is undefined', () => { - const { getByDisplayValue, queryByDisplayValue } = render(); - expect(() => - getByDisplayValue(DEFAULT_INPUT_CHEF) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with displayValue: What did you inspect?"` - ); - expect(queryByDisplayValue(DEFAULT_INPUT_CHEF)).toBeNull(); - - expect(() => getByDisplayValue('hello')).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with displayValue: hello"` - ); - expect(queryByDisplayValue('hello')).toBeNull(); - - expect(getByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy(); - expect(queryByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy(); -}); - -test('getAllByDisplayValue, queryAllByDisplayValue', () => { - const { getAllByDisplayValue, queryAllByDisplayValue } = render(); - const inputs = getAllByDisplayValue(/fresh/i); - - expect(inputs).toHaveLength(2); - expect(() => getAllByDisplayValue('no value')).toThrow( - 'Unable to find an element with displayValue: no value' - ); - - expect(queryAllByDisplayValue(/fresh/i)).toEqual(inputs); - expect(queryAllByDisplayValue('no value')).toHaveLength(0); -}); - -test('findBy queries work asynchronously', async () => { - const options = { timeout: 10 }; // Short timeout so that this test runs quickly - const { rerender, findByDisplayValue, findAllByDisplayValue } = render( - - ); - - await expect( - findByDisplayValue('Display Value', {}, options) - ).rejects.toBeTruthy(); - await expect( - findAllByDisplayValue('Display Value', {}, options) - ).rejects.toBeTruthy(); - - setTimeout( - () => - rerender( - - - - ), - 20 - ); - - await expect(findByDisplayValue('Display Value')).resolves.toBeTruthy(); - await expect(findAllByDisplayValue('Display Value')).resolves.toHaveLength(1); -}, 20000); - -test('byDisplayValue queries support hidden option', () => { - const { getByDisplayValue, queryByDisplayValue } = render( - - ); - - expect(getByDisplayValue('hidden')).toBeTruthy(); - expect( - getByDisplayValue('hidden', { includeHiddenElements: true }) - ).toBeTruthy(); - - expect( - queryByDisplayValue('hidden', { includeHiddenElements: false }) - ).toBeFalsy(); - expect(() => - getByDisplayValue('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with displayValue: hidden"` - ); -}); - -test('byDisplayValue should return host component', () => { - const { getByDisplayValue } = render(); - - expect(getByDisplayValue('value').type).toBe('TextInput'); -}); diff --git a/src/queries/__tests__/displayValue.test.tsx b/src/queries/__tests__/displayValue.test.tsx index e8f351c2b..db8a7147b 100644 --- a/src/queries/__tests__/displayValue.test.tsx +++ b/src/queries/__tests__/displayValue.test.tsx @@ -50,10 +50,16 @@ test('getByDisplayValue, queryByDisplayValue', () => { test('getByDisplayValue, queryByDisplayValue get element by default value only when value is undefined', () => { const { getByDisplayValue, queryByDisplayValue } = render(); - expect(() => getByDisplayValue(DEFAULT_INPUT_CHEF)).toThrow(); + expect(() => + getByDisplayValue(DEFAULT_INPUT_CHEF) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with displayValue: What did you inspect?"` + ); expect(queryByDisplayValue(DEFAULT_INPUT_CHEF)).toBeNull(); - expect(() => getByDisplayValue('hello')).toThrow(); + expect(() => getByDisplayValue('hello')).toThrowErrorMatchingInlineSnapshot( + `"Unable to find an element with displayValue: hello"` + ); expect(queryByDisplayValue('hello')).toBeNull(); expect(getByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy(); @@ -120,8 +126,8 @@ test('byDisplayValue queries support hidden option', () => { ); }); -test('byDisplayValue should return composite TextInput', () => { +test('byDisplayValue should return host component', () => { const { getByDisplayValue } = render(); - expect(getByDisplayValue('value').type).toBe(TextInput); + expect(getByDisplayValue('value').type).toBe('TextInput'); }); diff --git a/src/queries/__tests__/placeholderText.breaking.test.tsx b/src/queries/__tests__/placeholderText.breaking.test.tsx deleted file mode 100644 index ab2c82264..000000000 --- a/src/queries/__tests__/placeholderText.breaking.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/** This is a copy of regular tests with `useBreakingChanges` flag turned on. */ - -import * as React from 'react'; -import { View, TextInput } from 'react-native'; -import { render } from '../..'; -import { configureInternal } from '../../config'; - -const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; -const PLACEHOLDER_CHEF = 'Who inspected freshness?'; -const INPUT_FRESHNESS = 'Custom Freshie'; -const INPUT_CHEF = 'I inspected freshie'; -const DEFAULT_INPUT_CHEF = 'What did you inspect?'; - -beforeEach(() => { - configureInternal({ useBreakingChanges: true }); -}); - -const Banana = () => ( - - - - -); - -test('getByPlaceholderText, queryByPlaceholderText', () => { - const { getByPlaceholderText, queryByPlaceholderText } = render(); - const input = getByPlaceholderText(/custom/i); - - expect(input.props.placeholder).toBe(PLACEHOLDER_FRESHNESS); - - const sameInput = getByPlaceholderText(PLACEHOLDER_FRESHNESS); - - expect(sameInput.props.placeholder).toBe(PLACEHOLDER_FRESHNESS); - expect(() => getByPlaceholderText('no placeholder')).toThrow( - 'Unable to find an element with placeholder: no placeholder' - ); - - expect(queryByPlaceholderText(/add/i)).toBe(input); - expect(queryByPlaceholderText('no placeholder')).toBeNull(); - expect(() => queryByPlaceholderText(/fresh/)).toThrow( - 'Found multiple elements with placeholder: /fresh/ ' - ); -}); - -test('getAllByPlaceholderText, queryAllByPlaceholderText', () => { - const { getAllByPlaceholderText, queryAllByPlaceholderText } = render( - - ); - const inputs = getAllByPlaceholderText(/fresh/i); - - expect(inputs).toHaveLength(2); - expect(() => getAllByPlaceholderText('no placeholder')).toThrow( - 'Unable to find an element with placeholder: no placeholder' - ); - - expect(queryAllByPlaceholderText(/fresh/i)).toEqual(inputs); - expect(queryAllByPlaceholderText('no placeholder')).toHaveLength(0); -}); - -test('byPlaceholderText queries support hidden option', () => { - const { getByPlaceholderText, queryByPlaceholderText } = render( - - ); - - expect(getByPlaceholderText('hidden')).toBeTruthy(); - expect( - getByPlaceholderText('hidden', { includeHiddenElements: true }) - ).toBeTruthy(); - - expect( - queryByPlaceholderText('hidden', { includeHiddenElements: false }) - ).toBeFalsy(); - expect(() => - getByPlaceholderText('hidden', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with placeholder: hidden"` - ); -}); - -test('byPlaceHolderText should return host component', () => { - const { getByPlaceholderText } = render( - - ); - - expect(getByPlaceholderText('placeholder').type).toBe('TextInput'); -}); diff --git a/src/queries/__tests__/placeholderText.test.tsx b/src/queries/__tests__/placeholderText.test.tsx index c16cf1f59..b62d058f3 100644 --- a/src/queries/__tests__/placeholderText.test.tsx +++ b/src/queries/__tests__/placeholderText.test.tsx @@ -79,10 +79,10 @@ test('byPlaceholderText queries support hidden option', () => { ); }); -test('byPlaceHolderText should return composite component', () => { +test('byPlaceHolderText should return host component', () => { const { getByPlaceholderText } = render( ); - expect(getByPlaceholderText('placeholder').type).toBe(TextInput); + expect(getByPlaceholderText('placeholder').type).toBe('TextInput'); }); diff --git a/src/queries/__tests__/role.breaking.test.tsx b/src/queries/__tests__/role.breaking.test.tsx deleted file mode 100644 index dbca753ed..000000000 --- a/src/queries/__tests__/role.breaking.test.tsx +++ /dev/null @@ -1,769 +0,0 @@ -import * as React from 'react'; -import { - TouchableOpacity, - TouchableWithoutFeedback, - Text, - View, - Pressable, - Button as RNButton, -} from 'react-native'; -import { render } from '../..'; -import { configureInternal } from '../../config'; - -beforeEach(() => { - configureInternal({ useBreakingChanges: true }); -}); - -const TEXT_LABEL = 'cool text'; - -// Little hack to make all the methods happy with type -const NO_MATCHES_TEXT: any = 'not-existent-element'; - -const getMultipleInstancesFoundMessage = (value: string) => { - return `Found multiple elements with role: "${value}"`; -}; - -const getNoInstancesFoundMessage = (value: string) => { - return `Unable to find an element with role: "${value}"`; -}; - -const Typography = ({ children, ...rest }: any) => { - return {children}; -}; - -const Button = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - -const Section = () => ( - <> - Title - - -); - -test('getByRole, queryByRole, findByRole', async () => { - const { getByRole, queryByRole, findByRole } = render(
); - - expect(getByRole('button').props.accessibilityRole).toEqual('button'); - const button = queryByRole(/button/g); - expect(button?.props.accessibilityRole).toEqual('button'); - - expect(() => getByRole(NO_MATCHES_TEXT)).toThrow( - getNoInstancesFoundMessage(NO_MATCHES_TEXT) - ); - - expect(queryByRole(NO_MATCHES_TEXT)).toBeNull(); - - expect(() => getByRole('link')).toThrow( - getMultipleInstancesFoundMessage('link') - ); - expect(() => queryByRole('link')).toThrow( - getMultipleInstancesFoundMessage('link') - ); - - const asyncButton = await findByRole('button'); - expect(asyncButton.props.accessibilityRole).toEqual('button'); - await expect(findByRole(NO_MATCHES_TEXT)).rejects.toThrow( - getNoInstancesFoundMessage(NO_MATCHES_TEXT) - ); - await expect(findByRole('link')).rejects.toThrow( - getMultipleInstancesFoundMessage('link') - ); -}); - -test('getAllByRole, queryAllByRole, findAllByRole', async () => { - const { getAllByRole, queryAllByRole, findAllByRole } = render(
); - - expect(getAllByRole('link')).toHaveLength(2); - expect(queryAllByRole(/ink/g)).toHaveLength(2); - - expect(() => getAllByRole(NO_MATCHES_TEXT)).toThrow( - getNoInstancesFoundMessage(NO_MATCHES_TEXT) - ); - expect(queryAllByRole(NO_MATCHES_TEXT)).toEqual([]); - - await expect(findAllByRole('link')).resolves.toHaveLength(2); - await expect(findAllByRole(NO_MATCHES_TEXT)).rejects.toThrow( - getNoInstancesFoundMessage(NO_MATCHES_TEXT) - ); -}); - -describe('supports name option', () => { - test('returns an element that has the corresponding role and a children with the name', () => { - const { getByRole } = render( - - Save - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('button', { name: 'Save' }).props.testID).toBe( - 'target-button' - ); - }); - - test('returns an element that has the corresponding role when several children include the name', () => { - const { getByRole } = render( - - Save - Save - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('button', { name: 'Save' }).props.testID).toBe( - 'target-button' - ); - }); - - test('returns an element that has the corresponding role and a children with a matching accessibilityLabel', () => { - const { getByRole } = render( - - - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('button', { name: 'Save' }).props.testID).toBe( - 'target-button' - ); - }); - - test('returns an element that has the corresponding role and a matching accessibilityLabel', () => { - const { getByRole } = render( - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('button', { name: 'Save' }).props.testID).toBe( - 'target-button' - ); - }); - - test('returns an element when the direct child is text', () => { - const { getByRole, getByTestId } = render( - - About - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('header', { name: 'About' })).toBe( - getByTestId('target-header') - ); - expect(getByRole('header', { name: 'About' }).props.testID).toBe( - 'target-header' - ); - }); - - test('returns an element with nested Text as children', () => { - const { getByRole, getByTestId } = render( - - About - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('header', { name: 'About' })).toBe(getByTestId('parent')); - expect(getByRole('header', { name: 'About' }).props.testID).toBe('parent'); - }); - - test('returns a header with an accessibilityLabel', () => { - const { getByRole, getByTestId } = render( - - ); - - // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(getByRole('header', { name: 'About' })).toBe( - getByTestId('target-header') - ); - expect(getByRole('header', { name: 'About' }).props.testID).toBe( - 'target-header' - ); - }); -}); - -describe('supports accessibility states', () => { - describe('disabled', () => { - test('returns a disabled element when required', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { disabled: true })).toBeTruthy(); - expect(queryByRole('button', { disabled: false })).toBe(null); - }); - - test('returns the correct element when only one matches all the requirements', () => { - const { getByRole } = render( - <> - - Save - - - Save - - - ); - - expect( - getByRole('button', { name: 'Save', disabled: true }).props.testID - ).toBe('correct'); - }); - - test('returns an implicitly enabled element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { disabled: false })).toBeTruthy(); - expect(queryByRole('button', { disabled: true })).toBe(null); - }); - - test('returns an explicitly enabled element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { disabled: false })).toBeTruthy(); - expect(queryByRole('button', { disabled: true })).toBe(null); - }); - - test('does not return disabled elements when querying for non disabled', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('button', { disabled: false })).toBe(null); - }); - - test('returns elements using the built-in disabled prop', () => { - const { getByRole } = render( - <> - - Pressable - - - - - TouchableWithoutFeedback - - - {}} title="RNButton" /> - - ); - - expect( - getByRole('button', { name: 'Pressable', disabled: true }) - ).toBeTruthy(); - - expect( - getByRole('button', { - name: 'TouchableWithoutFeedback', - disabled: true, - }) - ).toBeTruthy(); - - expect( - getByRole('button', { name: 'RNButton', disabled: true }) - ).toBeTruthy(); - }); - }); - - describe('selected', () => { - test('returns a selected element when required', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('tab', { selected: true })).toBeTruthy(); - expect(queryByRole('tab', { selected: false })).toBe(null); - }); - - test('returns the correct element when only one matches all the requirements', () => { - const { getByRole } = render( - <> - - Save - - - Save - - - ); - - expect( - getByRole('tab', { name: 'Save', selected: true }).props.testID - ).toBe('correct'); - }); - - test('returns an implicitly non selected element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('tab', { selected: false })).toBeTruthy(); - expect(queryByRole('tab', { selected: true })).toBe(null); - }); - - test('returns an explicitly non selected element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('tab', { selected: false })).toBeTruthy(); - expect(queryByRole('tab', { selected: true })).toBe(null); - }); - - test('does not return selected elements when querying for non selected', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('tab', { selected: false })).toBe(null); - }); - }); - - describe('checked', () => { - test('returns a checked element when required', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('checkbox', { checked: true })).toBeTruthy(); - expect(queryByRole('checkbox', { checked: false })).toBe(null); - expect(queryByRole('checkbox', { checked: 'mixed' })).toBe(null); - }); - - it('returns `mixed` checkboxes', () => { - const { queryByRole, getByRole } = render( - - ); - - expect(getByRole('checkbox', { checked: 'mixed' })).toBeTruthy(); - expect(queryByRole('checkbox', { checked: true })).toBe(null); - expect(queryByRole('checkbox', { checked: false })).toBe(null); - }); - - it('does not return mixed checkboxes when querying for checked: true', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('checkbox', { checked: false })).toBe(null); - }); - - test('returns the correct element when only one matches all the requirements', () => { - const { getByRole } = render( - <> - - Save - - - Save - - - ); - - expect( - getByRole('checkbox', { name: 'Save', checked: true }).props.testID - ).toBe('correct'); - }); - - test('does not return return as non checked an element with checked: undefined', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('checkbox', { checked: false })).toBe(null); - }); - - test('returns an explicitly non checked element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('checkbox', { checked: false })).toBeTruthy(); - expect(queryByRole('checkbox', { checked: true })).toBe(null); - }); - - test('does not return checked elements when querying for non checked', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('checkbox', { checked: false })).toBe(null); - }); - - test('does not return mixed elements when querying for non checked', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('checkbox', { checked: false })).toBe(null); - }); - }); - - describe('busy', () => { - test('returns a busy element when required', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { busy: true })).toBeTruthy(); - expect(queryByRole('button', { busy: false })).toBe(null); - }); - - test('returns the correct element when only one matches all the requirements', () => { - const { getByRole } = render( - <> - - Save - - - Save - - - ); - - expect( - getByRole('button', { name: 'Save', busy: true }).props.testID - ).toBe('correct'); - }); - - test('returns an implicitly non busy element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { busy: false })).toBeTruthy(); - expect(queryByRole('button', { busy: true })).toBe(null); - }); - - test('returns an explicitly non busy element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { busy: false })).toBeTruthy(); - expect(queryByRole('button', { busy: true })).toBe(null); - }); - - test('does not return busy elements when querying for non busy', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('button', { selected: false })).toBe(null); - }); - }); - - describe('expanded', () => { - test('returns a expanded element when required', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { expanded: true })).toBeTruthy(); - expect(queryByRole('button', { expanded: false })).toBe(null); - }); - - test('returns the correct element when only one matches all the requirements', () => { - const { getByRole } = render( - <> - - Save - - - Save - - - ); - - expect( - getByRole('button', { name: 'Save', expanded: true }).props.testID - ).toBe('correct'); - }); - - test('does not return return as non expanded an element with expanded: undefined', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('button', { expanded: false })).toBe(null); - }); - - test('returns an explicitly non expanded element', () => { - const { getByRole, queryByRole } = render( - - ); - - expect(getByRole('button', { expanded: false })).toBeTruthy(); - expect(queryByRole('button', { expanded: true })).toBe(null); - }); - - test('does not return expanded elements when querying for non expanded', () => { - const { queryByRole } = render( - - ); - - expect(queryByRole('button', { expanded: false })).toBe(null); - }); - }); - - test('ignores non queried accessibilityState', () => { - const { getByRole, queryByRole } = render( - - Save - - ); - - expect( - getByRole('button', { - name: 'Save', - disabled: true, - }) - ).toBeTruthy(); - expect( - queryByRole('button', { - name: 'Save', - disabled: false, - }) - ).toBe(null); - }); - - test('matches an element combining all the options', () => { - const { getByRole } = render( - - Save - - ); - - expect( - getByRole('button', { - name: 'Save', - disabled: true, - selected: true, - checked: true, - busy: true, - expanded: true, - }) - ).toBeTruthy(); - }); -}); - -describe('error messages', () => { - test('gives a descriptive error message when querying with a role', () => { - const { getByRole } = render(); - - expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button""` - ); - }); - - test('gives a descriptive error message when querying with a role and a name', () => { - const { getByRole } = render(); - - expect(() => - getByRole('button', { name: 'Save' }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", name: "Save""` - ); - }); - - test('gives a descriptive error message when querying with a role, a name and accessibility state', () => { - const { getByRole } = render(); - - expect(() => - getByRole('button', { name: 'Save', disabled: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", name: "Save", disabled state: true"` - ); - }); - - test('gives a descriptive error message when querying with a role, a name and several accessibility state', () => { - const { getByRole } = render(); - - expect(() => - getByRole('button', { name: 'Save', disabled: true, selected: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true"` - ); - }); - - test('gives a descriptive error message when querying with a role and an accessibility state', () => { - const { getByRole } = render(); - - expect(() => - getByRole('button', { disabled: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button", disabled state: true"` - ); - }); - - test('gives a descriptive error message when querying with a role and an accessibility value', () => { - const { getByRole } = render(); - - expect(() => - getByRole('adjustable', { value: { min: 1 } }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", min value: 1"` - ); - - expect(() => - getByRole('adjustable', { - value: { min: 1, max: 2, now: 1, text: /hello/ }, - }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/"` - ); - }); -}); - -test('byRole queries support hidden option', () => { - const { getByRole, queryByRole } = render( - - Hidden from accessibility - - ); - - expect(getByRole('button')).toBeTruthy(); - expect(getByRole('button', { includeHiddenElements: true })).toBeTruthy(); - - expect(queryByRole('button', { includeHiddenElements: false })).toBeFalsy(); - expect(() => - getByRole('button', { includeHiddenElements: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to find an element with role: "button""` - ); -}); - -describe('matches only accessible elements', () => { - test('matches elements with accessible={true}', () => { - const { queryByRole } = render( - - Action - - ); - expect(queryByRole('menu', { name: 'Action' })).toBeTruthy(); - }); - - test('ignores elements with accessible={false}', () => { - const { queryByRole } = render( - - Action - - ); - expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); - }); - - test('ignores elements with accessible={undefined} and that are implicitely not accessible', () => { - const { queryByRole } = render( - - Action - - ); - expect(queryByRole('menu', { name: 'Action' })).toBeFalsy(); - }); -}); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 1b377ede0..81d6884f2 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -734,11 +734,31 @@ test('byRole queries support hidden option', () => { ); }); -test('does not take accessible prop into account', () => { - const { getByRole } = render( - - Action - - ); - expect(getByRole('button', { name: 'Action' })).toBeTruthy(); +describe('matches only accessible elements', () => { + test('matches elements with accessible={true}', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('menu', { name: 'Action' })).toBeTruthy(); + }); + + test('ignores elements with accessible={false}', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); + }); + + test('ignores elements with accessible={undefined} and that are implicitely not accessible', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('menu', { name: 'Action' })).toBeFalsy(); + }); }); diff --git a/src/queries/__tests__/text.breaking.test.tsx b/src/queries/__tests__/text.breaking.test.tsx deleted file mode 100644 index eb243edd1..000000000 --- a/src/queries/__tests__/text.breaking.test.tsx +++ /dev/null @@ -1,514 +0,0 @@ -import * as React from 'react'; -import { - View, - Text, - TouchableOpacity, - Image, - Button, - TextInput, -} from 'react-native'; -import { render, getDefaultNormalizer, within } from '../..'; -import { configureInternal } from '../../config'; - -beforeEach(() => { - configureInternal({ useBreakingChanges: true }); -}); - -test('byText matches simple text', () => { - const { getByText } = render(Hello World); - expect(getByText('Hello World').props.testID).toBe('text'); -}); - -test('byText matches inner nested text', () => { - const { getByText } = render( - - Hello World - - ); - expect(getByText('Hello World').props.testID).toBe('inner'); -}); - -test('byText matches accross multiple texts', () => { - const { getByText } = render( - - Hello World - - ); - expect(getByText('Hello World').props.testID).toBe('outer'); -}); - -type MyButtonProps = { - children: React.ReactNode; - onPress: () => void; -}; -const MyButton = ({ children, onPress }: MyButtonProps) => ( - - {children} - -); - -const Banana = () => { - const test = 0; - return ( - - Is the banana fresh? - not fresh - - Change freshness! - First Text - Second Text - {test} - - ); -}; - -type ChildrenProps = { children: React.ReactNode }; - -test('getByText, queryByText', () => { - const { getByText, queryByText } = render(); - const button = getByText(/change/i); - - expect(button.props.children).toBe('Change freshness!'); - - const sameButton = getByText('not fresh'); - - expect(sameButton.props.children).toBe('not fresh'); - expect(() => getByText('InExistent')).toThrow( - 'Unable to find an element with text: InExistent' - ); - - const zeroText = getByText('0'); - - expect(queryByText(/change/i)).toBe(button); - expect(queryByText('InExistent')).toBeNull(); - expect(() => queryByText(/fresh/)).toThrow( - 'Found multiple elements with text: /fresh/' - ); - expect(queryByText('0')).toBe(zeroText); -}); - -test('getByText, queryByText with children as Array', () => { - type BananaCounterProps = { numBananas: number }; - const BananaCounter = ({ numBananas }: BananaCounterProps) => ( - There are {numBananas} bananas in the bunch - ); - - const BananaStore = () => ( - - - - - - ); - - const { getByText } = render(); - - const threeBananaBunch = getByText('There are 3 bananas in the bunch'); - expect(threeBananaBunch.props.children).toEqual([ - 'There are ', - 3, - ' bananas in the bunch', - ]); -}); - -test('getAllByText, queryAllByText', () => { - const { getAllByText, queryAllByText } = render(); - const buttons = getAllByText(/fresh/i); - - expect(buttons).toHaveLength(3); - expect(() => getAllByText('InExistent')).toThrow( - 'Unable to find an element with text: InExistent' - ); - - expect(queryAllByText(/fresh/i)).toEqual(buttons); - expect(queryAllByText('InExistent')).toHaveLength(0); -}); - -test('findByText queries work asynchronously', async () => { - const options = { timeout: 10 }; // Short timeout so that this test runs quickly - const { rerender, findByText, findAllByText } = render(); - await expect(findByText('Some Text', {}, options)).rejects.toBeTruthy(); - await expect(findAllByText('Some Text', {}, options)).rejects.toBeTruthy(); - - setTimeout( - () => - rerender( - - Some Text - - ), - 20 - ); - - await expect(findByText('Some Text')).resolves.toBeTruthy(); - await expect(findAllByText('Some Text')).resolves.toHaveLength(1); -}, 20000); - -test('getByText works properly with custom text component', () => { - function BoldText({ children }: ChildrenProps) { - return {children}; - } - - expect( - render( - - Hello - - ).getByText('Hello') - ).toBeTruthy(); -}); - -test('getByText works properly with custom text container', () => { - function MyText({ children }: ChildrenProps) { - return {children}; - } - function BoldText({ children }: ChildrenProps) { - return {children}; - } - - expect( - render( - - Hello - - ).getByText('Hello') - ).toBeTruthy(); -}); - -test('queryByText nested in at start', () => { - expect( - render( - - - Hello - - ).queryByText('Hello') - ).toBeTruthy(); -}); - -test('queryByText nested in at end', () => { - expect( - render( - - Hello - - - ).queryByText('Hello') - ).toBeTruthy(); -}); - -test('queryByText nested in in middle', () => { - expect( - render( - - Hello - - World - - ).queryByText('HelloWorld') - ).toBeTruthy(); -}); - -test('queryByText not found', () => { - expect( - render( - - Hello - - - ).queryByText('SomethingElse') - ).toBeFalsy(); -}); - -test('*ByText matches text across multiple nested Text', () => { - const { getByText } = render( - - Hello{' '} - - World - !{true} - - - ); - - expect(getByText('Hello World!')).toBeTruthy(); -}); - -test('queryByText with nested Text components return the closest Text', () => { - const NestedTexts = () => ( - - My text - - ); - - const { queryByText } = render(); - expect(queryByText('My text', { exact: false })?.props.nativeID).toBe('2'); -}); - -test('queryByText with nested Text components each with text return the lowest one', () => { - const NestedTexts = () => ( - - bob - My text - - ); - - const { queryByText } = render(); - - expect(queryByText('My text', { exact: false })?.props.nativeID).toBe('2'); -}); - -test('queryByText nested deep in ', () => { - const CustomText = ({ children }: ChildrenProps) => { - return {children}; - }; - - expect( - render( - - Hello World! - - ).getByText('Hello World!') - ).toBeTruthy(); -}); - -test('queryByText with nested Text components: not-exact text match returns the most deeply nested common component', () => { - const { queryByText: queryByTextFirstCase } = render( - - bob - My - text - - ); - - const { queryByText: queryByTextSecondCase } = render( - - bob - My text for test - - ); - - expect(queryByTextFirstCase('My text')).toBe(null); - expect( - queryByTextSecondCase('My text', { exact: false })?.props.nativeID - ).toBe('2'); -}); - -test('queryAllByText does not match several times the same text', () => { - const allMatched = render( - - Start - This is a long text - - ).queryAllByText('long text', { exact: false }); - expect(allMatched.length).toBe(1); - expect(allMatched[0].props.nativeID).toBe('2'); -}); - -test('queryAllByText matches all the matching nodes', () => { - const allMatched = render( - - Start - This is a long text - This is another long text - - ).queryAllByText('long text', { exact: false }); - expect(allMatched.length).toBe(2); - expect(allMatched.map((node) => node.props.nativeID)).toEqual(['2', '3']); -}); - -describe('supports TextMatch options', () => { - test('getByText, getAllByText', () => { - const { getByText, getAllByText } = render( - - Text and details -