diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ae19a96e..b4e2fdbed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,22 +66,3 @@ jobs: uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - test-react-18: - needs: [install-cache-deps] - runs-on: ubuntu-latest - name: Test React 18 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js and deps - uses: ./.github/actions/setup-deps - - - name: Switch to React 18 - run: | - yarn remove react react-test-renderer react-native @react-native/babel-preset - yarn add -D react@18.3.1 react-test-renderer@18.3.1 react-native@0.77.0 @react-native/babel-preset@0.77.0 - - - name: Test - run: yarn test:ci diff --git a/jest-setup.ts b/jest-setup.ts index 2d6dd3c1d..f120a77e7 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -1,8 +1,5 @@ -import { resetToDefaults, configure } from './src/pure'; +import { resetToDefaults } from './src/pure'; beforeEach(() => { resetToDefaults(); - if (process.env.CONCURRENT_MODE === '0') { - configure({ concurrentRoot: false }); - } }); diff --git a/package.json b/package.json index bef89c3a4..688b6a72b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@testing-library/react-native", - "version": "13.2.0", + "version": "14.0.0-alpha.1", "description": "Simple and complete React Native testing utilities that encourage good testing practices.", "main": "build/index.js", "types": "build/index.d.ts", @@ -35,7 +35,7 @@ "build:ts": "tsc --build tsconfig.release.json", "build": "yarn clean && yarn build:js && yarn build:ts && yarn copy-flowtypes", "release": "release-it", - "release:rc": "release-it --preRelease=rc" + "release:alpha": "release-it --preRelease=alpha" }, "files": [ "build/", @@ -54,9 +54,9 @@ }, "peerDependencies": { "jest": ">=29.0.0", - "react": ">=18.2.0", - "react-native": ">=0.71", - "react-test-renderer": ">=18.2.0" + "react": ">=19.0.0", + "react-native": ">=0.77", + "universal-test-renderer": "0.6.0" }, "peerDependenciesMeta": { "jest": { @@ -90,16 +90,16 @@ "react": "19.0.0", "react-native": "0.78.0", "react-native-gesture-handler": "^2.23.1", - "react-test-renderer": "19.0.0", "release-it": "^18.0.0", "typescript": "^5.6.3", - "typescript-eslint": "^8.19.1" + "typescript-eslint": "^8.19.1", + "universal-test-renderer": "0.6.0" }, "publishConfig": { "registry": "https://registry.npmjs.org" }, "packageManager": "yarn@4.6.0", "engines": { - "node": ">=18" + "node": ">=20" } } diff --git a/src/__tests__/act.test.tsx b/src/__tests__/act.test.tsx index b398df774..278ad8f70 100644 --- a/src/__tests__/act.test.tsx +++ b/src/__tests__/act.test.tsx @@ -35,9 +35,9 @@ test('fireEvent should trigger useState', () => { render(); const counter = screen.getByText(/Total count/i); - expect(counter.props.children).toEqual('Total count: 0'); + expect(counter).toHaveTextContent('Total count: 0'); fireEvent.press(counter); - expect(counter.props.children).toEqual('Total count: 1'); + expect(counter).toHaveTextContent('Total count: 1'); }); test('should be able to not await act', () => { diff --git a/src/__tests__/auto-cleanup.test.tsx b/src/__tests__/auto-cleanup.test.tsx index 75453c93c..3c4c0fb05 100644 --- a/src/__tests__/auto-cleanup.test.tsx +++ b/src/__tests__/auto-cleanup.test.tsx @@ -27,14 +27,14 @@ afterEach(() => { // This just verifies that by importing RNTL in an environment which supports afterEach (like jest) // we'll get automatic cleanup between tests. -test('component is mounted, but not umounted before test ends', () => { +test('component is mounted, but not unmounted before test ends', () => { const fn = jest.fn(); render(); expect(isMounted).toEqual(true); expect(fn).not.toHaveBeenCalled(); }); -test('component is automatically umounted after first test ends', () => { +test('component is automatically unmounted after first test ends', () => { expect(isMounted).toEqual(false); }); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index dc454bea9..fa18b9be8 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -16,7 +16,6 @@ test('configure() overrides existing config values', () => { asyncUtilTimeout: 5000, defaultDebugOptions: { message: 'debug message' }, defaultIncludeHiddenElements: false, - concurrentRoot: true, }); }); diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index cdada565a..e51ff9df5 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -30,38 +30,12 @@ const WithoutEventComponent = (_props: WithoutEventComponentProps) => ( ); -type CustomEventComponentProps = { - onCustomEvent: () => void; -}; -const CustomEventComponent = ({ onCustomEvent }: CustomEventComponentProps) => ( - - Custom event component - -); - -type MyCustomButtonProps = { - handlePress: () => void; - text: string; -}; -const MyCustomButton = ({ handlePress, text }: MyCustomButtonProps) => ( - -); - -type CustomEventComponentWithCustomNameProps = { - handlePress: () => void; -}; -const CustomEventComponentWithCustomName = ({ - handlePress, -}: CustomEventComponentWithCustomNameProps) => ( - -); - describe('fireEvent', () => { test('should invoke specified event', () => { const onPressMock = jest.fn(); render(); - fireEvent(screen.getByText('Press me'), 'press'); + fireEvent.press(screen.getByText('Press me')); expect(onPressMock).toHaveBeenCalled(); }); @@ -71,7 +45,7 @@ describe('fireEvent', () => { const text = 'New press text'; render(); - fireEvent(screen.getByText(text), 'press'); + fireEvent.press(screen.getByText(text)); expect(onPressMock).toHaveBeenCalled(); }); @@ -84,26 +58,11 @@ describe('fireEvent', () => { fireEvent(screen.getByText('Without event'), 'press'); expect(onPressMock).not.toHaveBeenCalled(); }); - - test('should invoke event with custom name', () => { - const handlerMock = jest.fn(); - const EVENT_DATA = 'event data'; - - render( - - - , - ); - - fireEvent(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA); - - expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA); - }); }); test('fireEvent.press', () => { const onPressMock = jest.fn(); - const text = 'Fireevent press'; + const text = 'FireEvent press'; const eventData = { nativeEvent: { pageX: 20, @@ -114,7 +73,8 @@ test('fireEvent.press', () => { fireEvent.press(screen.getByText(text), eventData); - expect(onPressMock).toHaveBeenCalledWith(eventData); + expect(onPressMock).toHaveBeenCalledTimes(1); + expect(onPressMock.mock.calls[0][0].nativeEvent).toMatchObject(eventData.nativeEvent); }); test('fireEvent.scroll', () => { @@ -162,26 +122,6 @@ it('sets native state value for unmanaged text inputs', () => { expect(input).toHaveDisplayValue('abc'); }); -test('custom component with custom event name', () => { - const handlePress = jest.fn(); - - render(); - - fireEvent(screen.getByText('Custom component'), 'handlePress'); - - expect(handlePress).toHaveBeenCalled(); -}); - -test('event with multiple handler parameters', () => { - const handlePress = jest.fn(); - - render(); - - fireEvent(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2'); - - expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); -}); - test('should not fire on disabled TouchableOpacity', () => { const handlePress = jest.fn(); render( @@ -251,8 +191,7 @@ test('should fire inside View with pointerEvents="box-none"', () => { ); fireEvent.press(screen.getByText('Trigger')); - fireEvent(screen.getByText('Trigger'), 'onPress'); - expect(onPress).toHaveBeenCalledTimes(2); + expect(onPress).toHaveBeenCalledTimes(1); }); test('should fire inside View with pointerEvents="auto"', () => { @@ -266,8 +205,7 @@ test('should fire inside View with pointerEvents="auto"', () => { ); fireEvent.press(screen.getByText('Trigger')); - fireEvent(screen.getByText('Trigger'), 'onPress'); - expect(onPress).toHaveBeenCalledTimes(2); + expect(onPress).toHaveBeenCalledTimes(1); }); test('should not fire deeply inside View with pointerEvents="box-only"', () => { diff --git a/src/__tests__/react-native-animated.test.tsx b/src/__tests__/react-native-animated.test.tsx index 389bde268..923c2a823 100644 --- a/src/__tests__/react-native-animated.test.tsx +++ b/src/__tests__/react-native-animated.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import type { ViewStyle } from 'react-native'; -import { Animated } from 'react-native'; +import { Animated, Text } from 'react-native'; import { act, render, screen } from '..'; @@ -43,28 +43,28 @@ describe('AnimatedView', () => { jest.useRealTimers(); }); - it('should use native driver when useNativeDriver is true', () => { + it('should use native driver when useNativeDriver is true', async () => { render( - Test + Test , ); expect(screen.root).toHaveStyle({ opacity: 0 }); - act(() => jest.advanceTimersByTime(250)); + await act(() => jest.advanceTimersByTime(250)); // This stopped working in tests in RN 0.77 // expect(screen.root).toHaveStyle({ opacity: 0 }); }); - it('should not use native driver when useNativeDriver is false', () => { + it('should not use native driver when useNativeDriver is false', async () => { render( - Test + Test , ); expect(screen.root).toHaveStyle({ opacity: 0 }); - act(() => jest.advanceTimersByTime(250)); + await act(() => jest.advanceTimersByTime(250)); expect(screen.root).toHaveStyle({ opacity: 1 }); }); }); diff --git a/src/__tests__/render-debug.test.tsx b/src/__tests__/render-debug.test.tsx index 16418c19e..1b6320a27 100644 --- a/src/__tests__/render-debug.test.tsx +++ b/src/__tests__/render-debug.test.tsx @@ -11,15 +11,8 @@ const INPUT_CHEF = 'I inspected freshie'; const DEFAULT_INPUT_CHEF = 'What did you inspect?'; const DEFAULT_INPUT_CUSTOMER = 'What banana?'; -const ignoreWarnings = ['Using debug("message") is deprecated']; - beforeEach(() => { jest.spyOn(logger, 'info').mockImplementation(() => {}); - jest.spyOn(logger, 'warn').mockImplementation((message) => { - if (!ignoreWarnings.some((warning) => `${message}`.includes(warning))) { - logger.warn(message); - } - }); }); afterEach(() => { diff --git a/src/__tests__/render-hook.test.tsx b/src/__tests__/render-hook.test.tsx index 85151fdf2..b0f11a12c 100644 --- a/src/__tests__/render-hook.test.tsx +++ b/src/__tests__/render-hook.test.tsx @@ -1,6 +1,5 @@ import type { ReactNode } from 'react'; import React from 'react'; -import TestRenderer from 'react-test-renderer'; import { renderHook } from '../pure'; @@ -87,20 +86,3 @@ test('props type is inferred correctly when initial props is explicitly undefine expect(result.current).toBe(6); }); - -/** - * This test makes sure that calling renderHook does - * not try to detect host component names in any form. - * But since there are numerous methods that could trigger that - * we check the count of renders using React Test Renderers. - */ -test('does render only once', () => { - jest.spyOn(TestRenderer, 'create'); - - renderHook(() => { - const [state, setState] = React.useState(1); - return [state, setState]; - }); - - expect(TestRenderer.create).toHaveBeenCalledTimes(1); -}); diff --git a/src/__tests__/render-string-validation.test.tsx b/src/__tests__/render-string-validation.test.tsx index 0595c098a..2ff3cb163 100644 --- a/src/__tests__/render-string-validation.test.tsx +++ b/src/__tests__/render-string-validation.test.tsx @@ -7,8 +7,8 @@ import { fireEvent, render, screen } from '..'; const originalConsoleError = console.error; const VALIDATION_ERROR = - 'Invariant Violation: Text strings must be rendered within a component'; -const PROFILER_ERROR = 'The above error occurred in the component'; + 'Invariant Violation: Text strings must be rendered within a or component'; +const PROFILER_ERROR = 'The above error occurred in the component'; beforeEach(() => { // eslint-disable-next-line no-console @@ -25,19 +25,13 @@ afterEach(() => { }); test('should throw when rendering a string outside a text component', () => { - expect(() => - render(hello, { - unstable_validateStringsRenderedWithinText: true, - }), - ).toThrow( + expect(() => render(hello)).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "hello" string within a component.`, ); }); test('should throw an error when rerendering with text outside of Text component', () => { - render(, { - unstable_validateStringsRenderedWithinText: true, - }); + render(); expect(() => screen.rerender(hello)).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "hello" string within a component.`, @@ -59,9 +53,7 @@ const InvalidTextAfterPress = () => { }; test('should throw an error when strings are rendered outside Text', () => { - render(, { - unstable_validateStringsRenderedWithinText: true, - }); + render(); expect(() => fireEvent.press(screen.getByText('Show text'))).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "text rendered outside text component" string within a component.`, @@ -74,15 +66,10 @@ test('should not throw for texts nested in fragments', () => { <>hello , - { unstable_validateStringsRenderedWithinText: true }, ), ).not.toThrow(); }); -test('should not throw if option validateRenderedString is false', () => { - expect(() => render(hello)).not.toThrow(); -}); - test(`should throw when one of the children is a text and the parent is not a Text component`, () => { expect(() => render( @@ -90,7 +77,6 @@ test(`should throw when one of the children is a text and the parent is not a Te hello hello , - { unstable_validateStringsRenderedWithinText: true }, ), ).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "hello" string within a component.`, @@ -103,7 +89,6 @@ test(`should throw when a string is rendered within a fragment rendered outside <>hello , - { unstable_validateStringsRenderedWithinText: true }, ), ).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "hello" string within a component.`, @@ -111,9 +96,7 @@ test(`should throw when a string is rendered within a fragment rendered outside }); test('should throw if a number is rendered outside a text', () => { - expect(() => - render(0, { unstable_validateStringsRenderedWithinText: true }), - ).toThrow( + expect(() => render(0)).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "0" string within a component.`, ); }); @@ -126,7 +109,6 @@ test('should throw with components returning string value not rendered in Text', , - { unstable_validateStringsRenderedWithinText: true }, ), ).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "hello" string within a component.`, @@ -139,7 +121,6 @@ test('should not throw with components returning string value rendered in Text', , - { unstable_validateStringsRenderedWithinText: true }, ), ).not.toThrow(); }); @@ -150,7 +131,6 @@ test('should throw when rendering string in a View in a Text', () => { hello , - { unstable_validateStringsRenderedWithinText: true }, ), ).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "hello" string within a component.`, @@ -176,7 +156,7 @@ const UseEffectComponent = () => { }; test('should render immediate setState in useEffect properly', async () => { - render(, { unstable_validateStringsRenderedWithinText: true }); + render(); expect(await screen.findByText('Text is visible')).toBeTruthy(); }); @@ -196,9 +176,7 @@ const InvalidUseEffectComponent = () => { }; test('should throw properly for immediate setState in useEffect', () => { - expect(() => - render(, { unstable_validateStringsRenderedWithinText: true }), - ).toThrow( + expect(() => render()).toThrow( `${VALIDATION_ERROR}. Detected attempt to render "Text is visible" string within a component.`, ); }); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 6aa0769dd..ca8afff6c 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { Pressable, Text, TextInput, View } from 'react-native'; +import { CONTAINER_TYPE } from 'universal-test-renderer'; -import type { RenderAPI } from '..'; -import { fireEvent, render, screen } from '..'; +import { fireEvent, render, type RenderAPI, screen } from '..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; const PLACEHOLDER_CHEF = 'Who inspected freshness?'; @@ -74,42 +74,6 @@ class Banana extends React.Component { } } -test('UNSAFE_getAllByType, UNSAFE_queryAllByType', () => { - render(); - const [text, status, button] = screen.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(() => screen.UNSAFE_getAllByType(InExistent)).toThrow('No instances found'); - - expect(screen.UNSAFE_queryAllByType(Text)[1]).toBe(status); - expect(screen.UNSAFE_queryAllByType(InExistent)).toHaveLength(0); -}); - -test('UNSAFE_getByProps, UNSAFE_queryByProps', () => { - render(); - const primaryType = screen.UNSAFE_getByProps({ type: 'primary' }); - - expect(primaryType.props.children).toBe('Change freshness!'); - expect(() => screen.UNSAFE_getByProps({ type: 'inexistent' })).toThrow('No instances found'); - - expect(screen.UNSAFE_queryByProps({ type: 'primary' })).toBe(primaryType); - expect(screen.UNSAFE_queryByProps({ type: 'inexistent' })).toBeNull(); -}); - -test('UNSAFE_getAllByProp, UNSAFE_queryAllByProps', () => { - render(); - const primaryTypes = screen.UNSAFE_getAllByProps({ type: 'primary' }); - - expect(primaryTypes).toHaveLength(1); - expect(() => screen.UNSAFE_getAllByProps({ type: 'inexistent' })).toThrow('No instances found'); - - expect(screen.UNSAFE_queryAllByProps({ type: 'primary' })).toEqual(primaryTypes); - expect(screen.UNSAFE_queryAllByProps({ type: 'inexistent' })).toHaveLength(0); -}); - test('update', () => { const fn = jest.fn(); render(); @@ -200,26 +164,16 @@ test('returns host root', () => { render(); expect(screen.root).toBeDefined(); - expect(screen.root.type).toBe('View'); - expect(screen.root.props.testID).toBe('inner'); + expect(screen.root?.type).toBe('View'); + expect(screen.root?.props.testID).toBe('inner'); }); -test('returns composite UNSAFE_root', () => { +test('returns container', () => { render(); - expect(screen.UNSAFE_root).toBeDefined(); - expect(screen.UNSAFE_root.type).toBe(View); - expect(screen.UNSAFE_root.props.testID).toBe('inner'); -}); - -test('container displays deprecation', () => { - render(); - - expect(() => (screen as any).container).toThrowErrorMatchingInlineSnapshot(` - "'container' property has been renamed to 'UNSAFE_root'. - - Consider using 'root' property which returns root host element." - `); + expect(screen.container).toBeDefined(); + expect(screen.container.type).toBe(CONTAINER_TYPE); + expect(screen.container.props).toEqual({}); }); test('RenderAPI type', () => { @@ -233,13 +187,3 @@ test('returned output can be spread using rest operator', () => { const { rerender, ...rest } = render(); expect(rest).toBeTruthy(); }); - -test('supports legacy rendering', () => { - render(, { concurrentRoot: false }); - expect(screen.root).toBeOnTheScreen(); -}); - -test('supports concurrent rendering', () => { - render(, { concurrentRoot: true }); - expect(screen.root).toBeOnTheScreen(); -}); diff --git a/src/__tests__/screen.test.tsx b/src/__tests__/screen.test.tsx index de5d72c23..75abed20c 100644 --- a/src/__tests__/screen.test.tsx +++ b/src/__tests__/screen.test.tsx @@ -54,7 +54,7 @@ test('screen works with nested re-mounting rerender', () => { test('screen throws without render', () => { expect(() => screen.root).toThrow('`render` method has not been called'); - expect(() => screen.UNSAFE_root).toThrow('`render` method has not been called'); + expect(() => screen.container).toThrow('`render` method has not been called'); expect(() => screen.debug()).toThrow('`render` method has not been called'); expect(() => screen.getByText('Mt. Everest')).toThrow('`render` method has not been called'); }); diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 7568d2760..fae86a07d 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Pressable, Text, TouchableOpacity, View } from 'react-native'; -import { configure, fireEvent, render, screen, waitFor } from '..'; +import { act, configure, fireEvent, render, screen, waitFor } from '..'; class Banana extends React.Component { changeFresh = () => { @@ -46,7 +46,7 @@ test('waits for element until it stops throwing', async () => { const freshBananaText = await waitFor(() => screen.getByText('Fresh')); - expect(freshBananaText.props.children).toBe('Fresh'); + expect(freshBananaText).toHaveTextContent('Fresh'); }); test('waits for element until timeout is met', async () => { @@ -143,10 +143,12 @@ test.each([false, true])( fireEvent.press(screen.getByText('Change freshness!')); expect(screen.queryByText('Fresh')).toBeNull(); - jest.advanceTimersByTime(300); + await act(() => { + jest.advanceTimersByTime(300); + }); const freshBananaText = await waitFor(() => screen.getByText('Fresh')); - expect(freshBananaText.props.children).toBe('Fresh'); + expect(freshBananaText).toHaveTextContent('Fresh'); }, ); diff --git a/src/act.ts b/src/act.ts index 5aec2c319..21f38bb26 100644 --- a/src/act.ts +++ b/src/act.ts @@ -1,10 +1,8 @@ // This file and the act() implementation is sourced from react-testing-library // https://github.com/testing-library/react-testing-library/blob/3dcd8a9649e25054c0e650d95fca2317b7008576/types/index.d.ts import * as React from 'react'; -import { act as reactTestRendererAct } from 'react-test-renderer'; -const reactAct = typeof React.act === 'function' ? React.act : reactTestRendererAct; -type ReactAct = 0 extends 1 & typeof React.act ? typeof reactTestRendererAct : typeof React.act; +type ReactAct = typeof React.act; // See https://github.com/reactwg/react-18/discussions/102 for more context on global.IS_REACT_ACT_ENVIRONMENT declare global { @@ -46,11 +44,13 @@ function withGlobalActEnvironment(actImplementation: ReactAct) { // eslint-disable-next-line promise/always-return (returnValue) => { setIsReactActEnvironment(previousActEnvironment); - resolve(returnValue as never); + // @ts-expect-error too strict typing + resolve(returnValue); }, (error) => { setIsReactActEnvironment(previousActEnvironment); - reject(error as never); + // @ts-expect-error too strict typing + reject(error); }, ); }, @@ -68,8 +68,7 @@ function withGlobalActEnvironment(actImplementation: ReactAct) { }; } -// @ts-expect-error: typings get too complex -const act = withGlobalActEnvironment(reactAct) as ReactAct; +const act = withGlobalActEnvironment(React.act); export default act; -export { getIsReactActEnvironment, setIsReactActEnvironment as setReactActEnvironment }; +export { setIsReactActEnvironment as setReactActEnvironment, getIsReactActEnvironment }; diff --git a/src/config.ts b/src/config.ts index e861d0eb1..121e33bc4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,12 +13,6 @@ export type Config = { /** Default options for `debug` helper. */ defaultDebugOptions?: Partial; - - /** - * Set to `false` to disable concurrent rendering. - * Otherwise `render` will default to concurrent rendering. - */ - concurrentRoot: boolean; }; export type ConfigAliasOptions = { @@ -29,7 +23,6 @@ export type ConfigAliasOptions = { const defaultConfig: Config = { asyncUtilTimeout: 1000, defaultIncludeHiddenElements: false, - concurrentRoot: true, }; let config = { ...defaultConfig }; diff --git a/src/event-handler.ts b/src/event-handler.ts index 8f275c6b4..c8c6d12c3 100644 --- a/src/event-handler.ts +++ b/src/event-handler.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; export type EventHandlerOptions = { /** Include check for event handler named without adding `on*` prefix. */ @@ -6,7 +6,7 @@ export type EventHandlerOptions = { }; export function getEventHandler( - element: ReactTestInstance, + element: HostElement, eventName: string, options?: EventHandlerOptions, ) { diff --git a/src/fire-event.ts b/src/fire-event.ts index a843fad09..35bfb0da4 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -5,21 +5,22 @@ import type { TextProps, ViewProps, } from 'react-native'; -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import act from './act'; import { getEventHandler } from './event-handler'; -import { isElementMounted, isHostElement } from './helpers/component-tree'; +import { isElementMounted, isValidElement } from './helpers/component-tree'; import { isHostScrollView, isHostTextInput } from './helpers/host-component-names'; import { isPointerEventEnabled } from './helpers/pointer-events'; import { isEditableTextInput } from './helpers/text-input'; import { nativeState } from './native-state'; import type { Point, StringWithAutocomplete } from './types'; +import { EventBuilder } from './user-event/event-builder'; type EventHandler = (...args: unknown[]) => unknown; -export function isTouchResponder(element: ReactTestInstance) { - if (!isHostElement(element)) { +export function isTouchResponder(element: HostElement) { + if (!isValidElement(element)) { return false; } @@ -32,7 +33,15 @@ export function isTouchResponder(element: ReactTestInstance) { * Note: `fireEvent` is accepting both `press` and `onPress` for event names, * so we need cover both forms. */ -const eventsAffectedByPointerEventsProp = new Set(['press', 'onPress']); +const eventsAffectedByPointerEventsProp = new Set([ + 'press', + 'onPress', + 'responderGrant', + 'responderRelease', + 'longPress', + 'pressIn', + 'pressOut', +]); /** * List of `TextInput` events not affected by `editable` prop. @@ -50,9 +59,9 @@ const textInputEventsIgnoringEditableProp = new Set([ ]); export function isEventEnabled( - element: ReactTestInstance, + element: HostElement, eventName: string, - nearestTouchResponder?: ReactTestInstance, + nearestTouchResponder?: HostElement, ) { if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) { return ( @@ -75,14 +84,16 @@ export function isEventEnabled( } function findEventHandler( - element: ReactTestInstance, + element: HostElement, eventName: string, - nearestTouchResponder?: ReactTestInstance, + nearestTouchResponder?: HostElement, ): EventHandler | null { const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder; const handler = getEventHandler(element, eventName, { loose: true }); - if (handler && isEventEnabled(element, eventName, touchResponder)) return handler; + if (handler && isEventEnabled(element, eventName, touchResponder)) { + return handler; + } // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (element.parent === null || element.parent.parent === null) { @@ -105,7 +116,7 @@ type EventName = StringWithAutocomplete< | EventNameExtractor >; -function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) { +function fireEvent(element: HostElement, eventName: EventName, ...data: unknown[]) { if (!isElementMounted(element)) { return; } @@ -125,13 +136,41 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un return returnValue; } -fireEvent.press = (element: ReactTestInstance, ...data: unknown[]) => +fireEvent.press = (element: HostElement, ...data: unknown[]) => { + const nativeData = + data.length === 1 && + typeof data[0] === 'object' && + data[0] !== null && + 'nativeEvent' in data[0] && + typeof data[0].nativeEvent === 'object' + ? data[0].nativeEvent + : null; + + const responderGrantEvent = EventBuilder.Common.responderGrant(); + if (nativeData) { + responderGrantEvent.nativeEvent = { + ...responderGrantEvent.nativeEvent, + ...nativeData, + }; + } + fireEvent(element, 'responderGrant', responderGrantEvent); + fireEvent(element, 'press', ...data); -fireEvent.changeText = (element: ReactTestInstance, ...data: unknown[]) => + const responderReleaseEvent = EventBuilder.Common.responderRelease(); + if (nativeData) { + responderReleaseEvent.nativeEvent = { + ...responderReleaseEvent.nativeEvent, + ...nativeData, + }; + } + fireEvent(element, 'responderRelease', responderReleaseEvent); +}; + +fireEvent.changeText = (element: HostElement, ...data: unknown[]) => fireEvent(element, 'changeText', ...data); -fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) => +fireEvent.scroll = (element: HostElement, ...data: unknown[]) => fireEvent(element, 'scroll', ...data); export default fireEvent; @@ -144,7 +183,7 @@ const scrollEventNames = new Set([ 'momentumScrollEnd', ]); -function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, value: unknown) { +function setNativeStateIfNeeded(element: HostElement, eventName: string, value: unknown) { if (eventName === 'changeText' && typeof value === 'string' && isEditableTextInput(element)) { nativeState.valueForElement.set(element, value); } diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index c8a33036b..4bcf8cc33 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -1,18 +1,8 @@ import React from 'react'; -import { Text, TextInput, View } from 'react-native'; +import { View } from 'react-native'; import { render, screen } from '../..'; -import { - getHostChildren, - getHostParent, - getHostSelves, - getHostSiblings, - getUnsafeRootElement, -} from '../component-tree'; - -function ZeroHostChildren() { - return <>; -} +import { getContainerElement, getHostSiblings } from '../component-tree'; function MultipleHostChildren() { return ( @@ -24,155 +14,6 @@ function MultipleHostChildren() { ); } -describe('getHostParent()', () => { - it('returns host parent for host component', () => { - render( - - - - - - , - ); - - const hostParent = getHostParent(screen.getByTestId('subject')); - expect(hostParent).toBe(screen.getByTestId('parent')); - - const hostGrandparent = getHostParent(hostParent); - expect(hostGrandparent).toBe(screen.getByTestId('grandparent')); - - expect(getHostParent(hostGrandparent)).toBe(null); - }); - - it('returns host parent for null', () => { - expect(getHostParent(null)).toBe(null); - }); - - it('returns host parent for composite component', () => { - render( - - - - , - ); - - const compositeComponent = screen.UNSAFE_getByType(MultipleHostChildren); - const hostParent = getHostParent(compositeComponent); - expect(hostParent).toBe(screen.getByTestId('parent')); - }); -}); - -describe('getHostChildren()', () => { - it('returns host children for host component', () => { - render( - - - - Hello - - , - ); - - const hostSubject = screen.getByTestId('subject'); - expect(getHostChildren(hostSubject)).toEqual([]); - - const hostSibling = screen.getByTestId('sibling'); - expect(getHostChildren(hostSibling)).toEqual([]); - - const hostParent = screen.getByTestId('parent'); - expect(getHostChildren(hostParent)).toEqual([hostSubject, hostSibling]); - - const hostGrandparent = screen.getByTestId('grandparent'); - expect(getHostChildren(hostGrandparent)).toEqual([hostParent]); - }); - - it('returns host children for composite component', () => { - render( - - - - - , - ); - - expect(getHostChildren(screen.getByTestId('parent'))).toEqual([ - screen.getByTestId('child1'), - screen.getByTestId('child2'), - screen.getByTestId('child3'), - screen.getByTestId('subject'), - screen.getByTestId('sibling'), - ]); - }); -}); - -describe('getHostSelves()', () => { - it('returns passed element for host components', () => { - render( - - - - - - , - ); - - const hostSubject = screen.getByTestId('subject'); - expect(getHostSelves(hostSubject)).toEqual([hostSubject]); - - const hostSibling = screen.getByTestId('sibling'); - expect(getHostSelves(hostSibling)).toEqual([hostSibling]); - - const hostParent = screen.getByTestId('parent'); - expect(getHostSelves(hostParent)).toEqual([hostParent]); - - const hostGrandparent = screen.getByTestId('grandparent'); - expect(getHostSelves(hostGrandparent)).toEqual([hostGrandparent]); - }); - - test('returns single host element for React Native composite components', () => { - render( - - Text - - , - ); - - const compositeText = screen.getByText('Text'); - const hostText = screen.getByTestId('text'); - expect(getHostSelves(compositeText)).toEqual([hostText]); - - const compositeTextInputByValue = screen.getByDisplayValue('TextInputValue'); - const compositeTextInputByPlaceholder = screen.getByPlaceholderText('TextInputPlaceholder'); - - const hostTextInput = screen.getByTestId('textInput'); - expect(getHostSelves(compositeTextInputByValue)).toEqual([hostTextInput]); - expect(getHostSelves(compositeTextInputByPlaceholder)).toEqual([hostTextInput]); - }); - - test('returns host children for custom composite components', () => { - render( - - - - - , - ); - - const zeroCompositeComponent = screen.UNSAFE_getByType(ZeroHostChildren); - expect(getHostSelves(zeroCompositeComponent)).toEqual([]); - - const multipleCompositeComponent = screen.UNSAFE_getByType(MultipleHostChildren); - const hostChild1 = screen.getByTestId('child1'); - const hostChild2 = screen.getByTestId('child2'); - const hostChild3 = screen.getByTestId('child3'); - expect(getHostSelves(multipleCompositeComponent)).toEqual([hostChild1, hostChild2, hostChild3]); - }); -}); - describe('getHostSiblings()', () => { it('returns host siblings for host component', () => { render( @@ -195,31 +36,10 @@ describe('getHostSiblings()', () => { screen.getByTestId('child3'), ]); }); - - it('returns host siblings for composite component', () => { - render( - - - - - - - - , - ); - - const compositeComponent = screen.UNSAFE_getByType(MultipleHostChildren); - const hostSiblings = getHostSiblings(compositeComponent); - expect(hostSiblings).toEqual([ - screen.getByTestId('siblingBefore'), - screen.getByTestId('subject'), - screen.getByTestId('siblingAfter'), - ]); - }); }); -describe('getUnsafeRootElement()', () => { - it('returns UNSAFE_root for mounted view', () => { +describe('getRootElement()', () => { + it('returns container for mounted view', () => { render( @@ -227,6 +47,6 @@ describe('getUnsafeRootElement()', () => { ); const view = screen.getByTestId('view'); - expect(getUnsafeRootElement(view)).toEqual(screen.UNSAFE_root); + expect(getContainerElement(view)).toEqual(screen.container); }); }); diff --git a/src/helpers/__tests__/ensure-peer-deps.test.ts b/src/helpers/__tests__/ensure-peer-deps.test.ts deleted file mode 100644 index 354eab004..000000000 --- a/src/helpers/__tests__/ensure-peer-deps.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ - -// Mock the require calls -jest.mock('react/package.json', () => ({ version: '19.0.0' })); -jest.mock('react-test-renderer/package.json', () => ({ version: '19.0.0' })); - -describe('ensurePeerDeps', () => { - const originalEnv = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...originalEnv }; - delete process.env.RNTL_SKIP_DEPS_CHECK; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - it('should not throw when versions match', () => { - expect(() => require('../ensure-peer-deps')).not.toThrow(); - }); - - it('should throw when react-test-renderer is missing', () => { - jest.mock('react-test-renderer/package.json', () => { - throw new Error('Module not found'); - }); - - expect(() => require('../ensure-peer-deps')).toThrow( - 'Missing dev dependency "react-test-renderer@19.0.0"', - ); - }); - - it('should throw when react-test-renderer version mismatches', () => { - jest.mock('react-test-renderer/package.json', () => ({ version: '18.2.0' })); - - expect(() => require('../ensure-peer-deps')).toThrow( - 'Incorrect version of "react-test-renderer" detected. Expected "19.0.0", but found "18.2.0"', - ); - }); - - it('should skip dependency check when RNTL_SKIP_DEPS_CHECK is set', () => { - process.env.RNTL_SKIP_DEPS_CHECK = '1'; - jest.mock('react-test-renderer/package.json', () => { - throw new Error('Module not found'); - }); - - expect(() => require('../ensure-peer-deps')).not.toThrow(); - }); -}); diff --git a/src/helpers/__tests__/format-element.test.tsx b/src/helpers/__tests__/format-element.test.tsx index b27bde7a6..8c09c25c6 100644 --- a/src/helpers/__tests__/format-element.test.tsx +++ b/src/helpers/__tests__/format-element.test.tsx @@ -12,6 +12,9 @@ test('formatElement', () => { , ); + expect(formatElement(null)).toMatchInlineSnapshot(`"(null)"`); + expect(formatElement('Hello World')).toMatchInlineSnapshot(`"Hello World"`); + expect(formatElement(screen.getByTestId('view'), { mapProps: null })).toMatchInlineSnapshot(` "; + cache?: WeakMap; }; export const accessibilityStateKeys: (keyof AccessibilityState)[] = [ @@ -23,14 +23,14 @@ export const accessibilityStateKeys: (keyof AccessibilityState)[] = [ export const accessibilityValueKeys: (keyof AccessibilityValue)[] = ['min', 'max', 'now', 'text']; export function isHiddenFromAccessibility( - element: ReactTestInstance | null, + element: HostElement | null, { cache }: IsInaccessibleOptions = {}, ): boolean { if (element == null) { return true; } - let current: ReactTestInstance | null = element; + let current: HostElement | null = element; while (current) { let isCurrentSubtreeInaccessible = cache?.get(current); @@ -52,7 +52,7 @@ export function isHiddenFromAccessibility( /** RTL-compatibility alias for `isHiddenFromAccessibility` */ export const isInaccessible = isHiddenFromAccessibility; -function isSubtreeInaccessible(element: ReactTestInstance): boolean { +function isSubtreeInaccessible(element: HostElement): boolean { // Null props can happen for React.Fragments if (element.props == null) { return false; @@ -89,7 +89,7 @@ function isSubtreeInaccessible(element: ReactTestInstance): boolean { return false; } -export function isAccessibilityElement(element: ReactTestInstance | null): boolean { +export function isAccessibilityElement(element: HostElement | null): boolean { if (element == null) { return false; } @@ -119,7 +119,7 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole * @param element * @returns */ -export function getRole(element: ReactTestInstance): Role | AccessibilityRole { +export function getRole(element: HostElement): Role | AccessibilityRole { const explicitRole = element.props.role ?? element.props.accessibilityRole; if (explicitRole) { return normalizeRole(explicitRole); @@ -150,18 +150,20 @@ export function normalizeRole(role: string): Role | AccessibilityRole { return role as Role | AccessibilityRole; } -export function computeAriaModal(element: ReactTestInstance): boolean | undefined { +export function computeAriaModal(element: HostElement): boolean | undefined { return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal; } -export function computeAriaLabel(element: ReactTestInstance): string | undefined { +export function computeAriaLabel(element: HostElement): string | undefined { const labelElementId = element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy; if (labelElementId) { - const rootElement = getUnsafeRootElement(element); + const rootElement = getContainerElement(element); const labelElement = findAll( rootElement, - (node) => isHostElement(node) && node.props.nativeID === labelElementId, - { includeHiddenElements: true }, + (node) => isValidElement(node) && node.props.nativeID === labelElementId, + { + includeHiddenElements: true, + }, ); if (labelElement.length > 0) { return getTextContent(labelElement[0]); @@ -182,12 +184,12 @@ export function computeAriaLabel(element: ReactTestInstance): string | undefined } // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#busy-state -export function computeAriaBusy({ props }: ReactTestInstance): boolean { +export function computeAriaBusy({ props }: HostElement): boolean { return props['aria-busy'] ?? props.accessibilityState?.busy ?? false; } // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#checked-state -export function computeAriaChecked(element: ReactTestInstance): AccessibilityState['checked'] { +export function computeAriaChecked(element: HostElement): AccessibilityState['checked'] { const { props } = element; if (isHostSwitch(element)) { @@ -203,7 +205,7 @@ export function computeAriaChecked(element: ReactTestInstance): AccessibilitySta } // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#disabled-state -export function computeAriaDisabled(element: ReactTestInstance): boolean { +export function computeAriaDisabled(element: HostElement): boolean { if (isHostTextInput(element) && !isEditableTextInput(element)) { return true; } @@ -213,16 +215,16 @@ export function computeAriaDisabled(element: ReactTestInstance): boolean { } // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#expanded-state -export function computeAriaExpanded({ props }: ReactTestInstance): boolean | undefined { +export function computeAriaExpanded({ props }: HostElement): boolean | undefined { return props['aria-expanded'] ?? props.accessibilityState?.expanded; } // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#selected-state -export function computeAriaSelected({ props }: ReactTestInstance): boolean { +export function computeAriaSelected({ props }: HostElement): boolean { return props['aria-selected'] ?? props.accessibilityState?.selected ?? false; } -export function computeAriaValue(element: ReactTestInstance): AccessibilityValue { +export function computeAriaValue(element: HostElement): AccessibilityValue { const { accessibilityValue, 'aria-valuemax': ariaValueMax, @@ -239,7 +241,7 @@ export function computeAriaValue(element: ReactTestInstance): AccessibilityValue }; } -export function computeAccessibleName(element: ReactTestInstance): string | undefined { +export function computeAccessibleName(element: HostElement): string | undefined { return computeAriaLabel(element) ?? getTextContent(element); } diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 9b2c99afd..e6a93a6ed 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -1,102 +1,44 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; +import { CONTAINER_TYPE } from 'universal-test-renderer'; import { screen } from '../screen'; -/** - * ReactTestInstance referring to host element. - */ -export type HostTestInstance = ReactTestInstance & { type: string }; /** * Checks if the given element is a host element. * @param element The element to check. */ -export function isHostElement(element?: ReactTestInstance | null): element is HostTestInstance { - return typeof element?.type === 'string'; +export function isValidElement(element?: HostElement | null): element is HostElement { + return typeof element?.type === 'string' && element.type !== CONTAINER_TYPE; } -export function isElementMounted(element: ReactTestInstance) { - return getUnsafeRootElement(element) === screen.UNSAFE_root; +export function isElementMounted(element: HostElement) { + return getContainerElement(element) === screen.container; } /** - * Returns first host ancestor for given element. + * Returns the unsafe root element of the tree (probably composite). + * * @param element The element start traversing from. + * @returns The root element of the tree (host or composite). */ -export function getHostParent(element: ReactTestInstance | null): HostTestInstance | null { - if (element == null) { - return null; - } - - let current = element.parent; - while (current) { - if (isHostElement(current)) { - return current; - } - +export function getContainerElement(element: HostElement) { + let current: HostElement | null = element; + while (current?.parent) { current = current.parent; } - return null; -} - -/** - * Returns host children for given element. - * @param element The element start traversing from. - */ -export function getHostChildren(element: ReactTestInstance | null): HostTestInstance[] { - if (element == null) { - return []; - } - - const hostChildren: HostTestInstance[] = []; - - element.children.forEach((child) => { - if (typeof child !== 'object') { - return; - } - - if (isHostElement(child)) { - hostChildren.push(child); - } else { - hostChildren.push(...getHostChildren(child)); - } - }); - - return hostChildren; -} - -/** - * Return the array of host elements that represent the passed element. - * - * @param element The element start traversing from. - * @returns If the passed element is a host element, it will return an array containing only that element, - * if the passed element is a composite element, it will return an array containing its host children (zero, one or many). - */ -export function getHostSelves(element: ReactTestInstance | null): HostTestInstance[] { - return isHostElement(element) ? [element] : getHostChildren(element); + return current; } /** * Returns host siblings for given element. * @param element The element start traversing from. */ -export function getHostSiblings(element: ReactTestInstance | null): HostTestInstance[] { - const hostParent = getHostParent(element); - const hostSelves = getHostSelves(element); - return getHostChildren(hostParent).filter((sibling) => !hostSelves.includes(sibling)); -} - -/** - * Returns the unsafe root element of the tree (probably composite). - * - * @param element The element start traversing from. - * @returns The root element of the tree (host or composite). - */ -export function getUnsafeRootElement(element: ReactTestInstance) { - let current = element; - while (current.parent) { - current = current.parent; - } - - return current; +export function getHostSiblings(element: HostElement | null): HostElement[] { + const hostParent = element?.parent ?? null; + return ( + hostParent?.children.filter( + (sibling): sibling is HostElement => typeof sibling === 'object' && sibling !== element, + ) ?? [] + ); } diff --git a/src/helpers/debug.ts b/src/helpers/debug.ts index 4ec242f61..80ec53df2 100644 --- a/src/helpers/debug.ts +++ b/src/helpers/debug.ts @@ -1,4 +1,4 @@ -import type { ReactTestRendererJSON } from 'react-test-renderer'; +import type { JsonNode } from 'universal-test-renderer'; import type { FormatElementOptions } from './format-element'; import { formatJson } from './format-element'; @@ -11,10 +11,10 @@ export type DebugOptions = { /** * Log pretty-printed deep test component instance */ -export function debug( - instance: ReactTestRendererJSON | ReactTestRendererJSON[], - { message, ...formatOptions }: DebugOptions = {}, -) { +export function debug(instance: JsonNode | JsonNode[], options?: DebugOptions) { + const message = options?.message; + const formatOptions = { mapProps: options?.mapProps }; + if (message) { logger.info(`${message}\n\n`, formatJson(instance, formatOptions)); } else { diff --git a/src/helpers/ensure-peer-deps.ts b/src/helpers/ensure-peer-deps.ts deleted file mode 100644 index b06507bfc..000000000 --- a/src/helpers/ensure-peer-deps.ts +++ /dev/null @@ -1,37 +0,0 @@ -function ensurePeerDeps() { - const reactVersion = getPackageVersion('react'); - ensurePackage('react-test-renderer', reactVersion); -} - -function ensurePackage(name: string, expectedVersion: string) { - const actualVersion = getPackageVersion(name); - if (!actualVersion) { - const error = new Error( - `Missing dev dependency "${name}@${expectedVersion}".\n\nFix it by running:\nnpm install -D ${name}@${expectedVersion}`, - ); - Error.captureStackTrace(error, ensurePeerDeps); - throw error; - } - - if (expectedVersion !== actualVersion) { - const error = new Error( - `Incorrect version of "${name}" detected. Expected "${expectedVersion}", but found "${actualVersion}".\n\nFix it by running:\nnpm install -D ${name}@${expectedVersion}`, - ); - Error.captureStackTrace(error, ensurePeerDeps); - throw error; - } -} - -function getPackageVersion(name: string) { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const packageJson = require(`${name}/package.json`); - return packageJson.version; - } catch { - return null; - } -} - -if (!process.env.RNTL_SKIP_DEPS_CHECK) { - ensurePeerDeps(); -} diff --git a/src/helpers/find-all.ts b/src/helpers/find-all.ts index 4b476dfdb..1968b16c5 100644 --- a/src/helpers/find-all.ts +++ b/src/helpers/find-all.ts @@ -1,9 +1,8 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import { getConfig } from '../config'; import { isHiddenFromAccessibility } from './accessibility'; -import type { HostTestInstance } from './component-tree'; -import { isHostElement } from './component-tree'; +import { isValidElement } from './component-tree'; interface FindAllOptions { /** Match elements hidden from accessibility */ @@ -17,10 +16,10 @@ interface FindAllOptions { } export function findAll( - root: ReactTestInstance, - predicate: (element: ReactTestInstance) => boolean, + root: HostElement, + predicate: (element: HostElement) => boolean, options?: FindAllOptions, -): HostTestInstance[] { +): HostElement[] { const results = findAllInternal(root, predicate, options); const includeHiddenElements = @@ -30,35 +29,36 @@ export function findAll( return results; } - const cache = new WeakMap(); + const cache = new WeakMap(); return results.filter((element) => !isHiddenFromAccessibility(element, { cache })); } // Extracted from React Test Renderer // src: https://github.com/facebook/react/blob/8e2bde6f2751aa6335f3cef488c05c3ea08e074a/packages/react-test-renderer/src/ReactTestRenderer.js#L402 function findAllInternal( - root: ReactTestInstance, - predicate: (element: ReactTestInstance) => boolean, + node: HostElement, + predicate: (element: HostElement) => boolean, options?: FindAllOptions, -): HostTestInstance[] { - const results: HostTestInstance[] = []; + indent: string = '', +): HostElement[] { + const results: HostElement[] = []; // Match descendants first but do not add them to results yet. - const matchingDescendants: HostTestInstance[] = []; - root.children.forEach((child) => { + const matchingDescendants: HostElement[] = []; + node.children.forEach((child) => { if (typeof child === 'string') { return; } - matchingDescendants.push(...findAllInternal(child, predicate, options)); + matchingDescendants.push(...findAllInternal(child, predicate, options, indent + ' ')); }); if ( // When matchDeepestOnly = true: add current element only if no descendants match (!options?.matchDeepestOnly || matchingDescendants.length === 0) && - isHostElement(root) && - predicate(root) + isValidElement(node) && + predicate(node) ) { - results.push(root); + results.push(node); } // Add matching descendants after element to preserve original tree walk order. diff --git a/src/helpers/format-element.ts b/src/helpers/format-element.ts index 295636db2..a2007432b 100644 --- a/src/helpers/format-element.ts +++ b/src/helpers/format-element.ts @@ -1,6 +1,6 @@ -import type { ReactTestInstance, ReactTestRendererJSON } from 'react-test-renderer'; import type { NewPlugin } from 'pretty-format'; import prettyFormat, { plugins } from 'pretty-format'; +import type { HostNode, JsonNode } from 'universal-test-renderer'; import type { MapPropsFunction } from './map-props'; import { defaultMapProps } from './map-props'; @@ -22,15 +22,18 @@ export type FormatElementOptions = { * @param element Element to format. */ export function formatElement( - element: ReactTestInstance | null, + element: HostNode | null, { compact, highlight = true, mapProps = defaultMapProps }: FormatElementOptions = {}, ) { if (element == null) { return '(null)'; } - const { children, ...props } = element.props; - const childrenToDisplay = typeof children === 'string' ? [children] : undefined; + if (typeof element === 'string') { + return element; + } + + const childrenToDisplay = element.children.filter((child) => typeof child === 'string'); return prettyFormat( { @@ -38,12 +41,12 @@ export function formatElement( // a ReactTestRendererJSON instance, so it is formatted as JSX. $$typeof: Symbol.for('react.test.json'), type: `${element.type}`, - props: mapProps ? mapProps(props) : props, + props: mapProps ? mapProps(element.props) : element.props, children: childrenToDisplay, }, // See: https://www.npmjs.com/package/pretty-format#usage-with-options { - plugins: [plugins.ReactTestComponent, plugins.ReactElement], + plugins: [plugins.ReactTestComponent], printFunctionName: false, printBasicPrototype: false, highlight: highlight, @@ -52,7 +55,7 @@ export function formatElement( ); } -export function formatElementList(elements: ReactTestInstance[], options?: FormatElementOptions) { +export function formatElementList(elements: HostNode[], options?: FormatElementOptions) { if (elements.length === 0) { return '(no elements)'; } @@ -61,7 +64,7 @@ export function formatElementList(elements: ReactTestInstance[], options?: Forma } export function formatJson( - json: ReactTestRendererJSON | ReactTestRendererJSON[], + json: JsonNode | JsonNode[], { compact, highlight = true, mapProps = defaultMapProps }: FormatElementOptions = {}, ) { return prettyFormat(json, { diff --git a/src/helpers/host-component-names.ts b/src/helpers/host-component-names.ts index 45e019bc8..6536088ad 100644 --- a/src/helpers/host-component-names.ts +++ b/src/helpers/host-component-names.ts @@ -1,8 +1,8 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; -import type { HostTestInstance } from './component-tree'; +import { isValidElement } from './component-tree'; -const HOST_TEXT_NAMES = ['Text', 'RCTText']; +export const HOST_TEXT_NAMES = ['Text', 'RCTText']; const HOST_TEXT_INPUT_NAMES = ['TextInput']; const HOST_IMAGE_NAMES = ['Image']; const HOST_SWITCH_NAMES = ['RCTSwitch']; @@ -13,46 +13,46 @@ const HOST_MODAL_NAMES = ['Modal']; * Checks if the given element is a host Text element. * @param element The element to check. */ -export function isHostText(element: ReactTestInstance): element is HostTestInstance { - return typeof element?.type === 'string' && HOST_TEXT_NAMES.includes(element.type); +export function isHostText(element: HostElement | null) { + return isValidElement(element) && HOST_TEXT_NAMES.includes(element.type); } /** * Checks if the given element is a host TextInput element. * @param element The element to check. */ -export function isHostTextInput(element: ReactTestInstance): element is HostTestInstance { - return typeof element?.type === 'string' && HOST_TEXT_INPUT_NAMES.includes(element.type); +export function isHostTextInput(element: HostElement | null) { + return isValidElement(element) && HOST_TEXT_INPUT_NAMES.includes(element.type); } /** * Checks if the given element is a host Image element. * @param element The element to check. */ -export function isHostImage(element: ReactTestInstance): element is HostTestInstance { - return typeof element?.type === 'string' && HOST_IMAGE_NAMES.includes(element.type); +export function isHostImage(element: HostElement | null) { + return isValidElement(element) && HOST_IMAGE_NAMES.includes(element.type); } /** * Checks if the given element is a host Switch element. * @param element The element to check. */ -export function isHostSwitch(element: ReactTestInstance): element is HostTestInstance { - return typeof element?.type === 'string' && HOST_SWITCH_NAMES.includes(element.type); +export function isHostSwitch(element: HostElement | null) { + return isValidElement(element) && HOST_SWITCH_NAMES.includes(element.type); } /** * Checks if the given element is a host ScrollView element. * @param element The element to check. */ -export function isHostScrollView(element: ReactTestInstance): element is HostTestInstance { - return typeof element?.type === 'string' && HOST_SCROLL_VIEW_NAMES.includes(element.type); +export function isHostScrollView(element: HostElement | null) { + return isValidElement(element) && HOST_SCROLL_VIEW_NAMES.includes(element.type); } /** * Checks if the given element is a host Modal element. * @param element The element to check. */ -export function isHostModal(element: ReactTestInstance): element is HostTestInstance { - return typeof element?.type === 'string' && HOST_MODAL_NAMES.includes(element.type); +export function isHostModal(element: HostElement | null) { + return isValidElement(element) && HOST_MODAL_NAMES.includes(element.type); } diff --git a/src/helpers/matchers/match-accessibility-state.ts b/src/helpers/matchers/match-accessibility-state.ts index 0aabf216b..9cf1be21d 100644 --- a/src/helpers/matchers/match-accessibility-state.ts +++ b/src/helpers/matchers/match-accessibility-state.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaBusy, @@ -20,10 +20,7 @@ export interface AccessibilityStateMatcher { expanded?: boolean; } -export function matchAccessibilityState( - node: ReactTestInstance, - matcher: AccessibilityStateMatcher, -) { +export function matchAccessibilityState(node: HostElement, matcher: AccessibilityStateMatcher) { if (matcher.busy !== undefined && matcher.busy !== computeAriaBusy(node)) { return false; } diff --git a/src/helpers/matchers/match-accessibility-value.ts b/src/helpers/matchers/match-accessibility-value.ts index 6fe281d32..9e332f49f 100644 --- a/src/helpers/matchers/match-accessibility-value.ts +++ b/src/helpers/matchers/match-accessibility-value.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import type { TextMatch } from '../../matches'; import { computeAriaValue } from '../accessibility'; @@ -12,7 +12,7 @@ export interface AccessibilityValueMatcher { } export function matchAccessibilityValue( - node: ReactTestInstance, + node: HostElement, matcher: AccessibilityValueMatcher, ): boolean { const value = computeAriaValue(node); diff --git a/src/helpers/matchers/match-label-text.ts b/src/helpers/matchers/match-label-text.ts index ce1fef4c0..b30197892 100644 --- a/src/helpers/matchers/match-label-text.ts +++ b/src/helpers/matchers/match-label-text.ts @@ -1,11 +1,11 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import type { TextMatch, TextMatchOptions } from '../../matches'; import { matches } from '../../matches'; import { computeAriaLabel } from '../accessibility'; export function matchAccessibilityLabel( - element: ReactTestInstance, + element: HostElement, expectedLabel: TextMatch, options?: TextMatchOptions, ) { diff --git a/src/helpers/matchers/match-text-content.ts b/src/helpers/matchers/match-text-content.ts index dd5e7d90e..b193f6d25 100644 --- a/src/helpers/matchers/match-text-content.ts +++ b/src/helpers/matchers/match-text-content.ts @@ -1,4 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import type { TextMatch, TextMatchOptions } from '../../matches'; import { matches } from '../../matches'; @@ -12,7 +12,7 @@ import { getTextContent } from '../text-content'; * @returns - Whether the node's text content matches the given string or regex. */ export function matchTextContent( - node: ReactTestInstance, + node: HostElement, text: TextMatch, options: TextMatchOptions = {}, ) { diff --git a/src/helpers/pointer-events.ts b/src/helpers/pointer-events.ts index 2e72ff8a0..e2ce189db 100644 --- a/src/helpers/pointer-events.ts +++ b/src/helpers/pointer-events.ts @@ -1,6 +1,4 @@ -import type { ReactTestInstance } from 'react-test-renderer'; - -import { getHostParent } from './component-tree'; +import type { HostElement } from 'universal-test-renderer'; /** * pointerEvents controls whether the View can be the target of touch events. @@ -9,7 +7,7 @@ import { getHostParent } from './component-tree'; * 'box-none': The View is never the target of touch events but its subviews can be * 'box-only': The view can be the target of touch events but its subviews cannot be * see the official react native doc https://reactnative.dev/docs/view#pointerevents */ -export const isPointerEventEnabled = (element: ReactTestInstance, isParent?: boolean): boolean => { +export const isPointerEventEnabled = (element: HostElement, isParent?: boolean): boolean => { const parentCondition = isParent ? element?.props.pointerEvents === 'box-only' : element?.props.pointerEvents === 'box-none'; @@ -18,7 +16,7 @@ export const isPointerEventEnabled = (element: ReactTestInstance, isParent?: boo return false; } - const hostParent = getHostParent(element); + const hostParent = element.parent; if (!hostParent) return true; return isPointerEventEnabled(hostParent, true); diff --git a/src/helpers/string-validation.ts b/src/helpers/string-validation.ts deleted file mode 100644 index 17864c8e1..000000000 --- a/src/helpers/string-validation.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { ReactTestRendererNode } from 'react-test-renderer'; - -export const validateStringsRenderedWithinText = ( - rendererJSON: ReactTestRendererNode | Array | null, -) => { - if (!rendererJSON) return; - - if (Array.isArray(rendererJSON)) { - rendererJSON.forEach(validateStringsRenderedWithinTextForNode); - return; - } - - return validateStringsRenderedWithinTextForNode(rendererJSON); -}; - -const validateStringsRenderedWithinTextForNode = (node: ReactTestRendererNode) => { - if (typeof node === 'string') { - return; - } - - if (node.type !== 'Text') { - node.children?.forEach((child) => { - if (typeof child === 'string') { - throw new Error( - `Invariant Violation: Text strings must be rendered within a component. Detected attempt to render "${child}" string within a <${node.type}> component.`, - ); - } - }); - } - - if (node.children) { - node.children.forEach(validateStringsRenderedWithinTextForNode); - } -}; diff --git a/src/helpers/text-content.ts b/src/helpers/text-content.ts index 126dca44f..208160d35 100644 --- a/src/helpers/text-content.ts +++ b/src/helpers/text-content.ts @@ -1,6 +1,6 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; -export function getTextContent(element: ReactTestInstance | string | null): string { +export function getTextContent(element: HostElement | string | null): string { if (!element) { return ''; } diff --git a/src/helpers/text-input.ts b/src/helpers/text-input.ts index 682043992..29fa000b3 100644 --- a/src/helpers/text-input.ts +++ b/src/helpers/text-input.ts @@ -1,13 +1,13 @@ -import type { ReactTestInstance } from 'react-test-renderer'; +import type { HostElement } from 'universal-test-renderer'; import { nativeState } from '../native-state'; import { isHostTextInput } from './host-component-names'; -export function isEditableTextInput(element: ReactTestInstance) { +export function isEditableTextInput(element: HostElement) { return isHostTextInput(element) && element.props.editable !== false; } -export function getTextInputValue(element: ReactTestInstance) { +export function getTextInputValue(element: HostElement) { if (!isHostTextInput(element)) { throw new Error(`Element is not a "TextInput", but it has type "${element.type}".`); } diff --git a/src/helpers/wrap-async.ts b/src/helpers/wrap-async.ts index a80d86156..8f8d710c4 100644 --- a/src/helpers/wrap-async.ts +++ b/src/helpers/wrap-async.ts @@ -5,6 +5,7 @@ import { flushMicroTasks } from '../flush-micro-tasks'; /** * Run given async callback with temporarily disabled `act` environment and flushes microtasks queue. + * See: https://github.com/testing-library/react-testing-library/blob/3dcd8a9649e25054c0e650d95fca2317b7008576/src/pure.js#L37 * * @param callback Async callback to run * @returns Result of the callback diff --git a/src/index.ts b/src/index.ts index 426042f94..39711dfe0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import './helpers/ensure-peer-deps'; import './matchers/extend-expect'; +export { HostElement } from 'universal-test-renderer'; import { getIsReactActEnvironment, setReactActEnvironment } from './act'; import { flushMicroTasks } from './flush-micro-tasks'; import { cleanup } from './pure'; diff --git a/src/matchers/__tests__/to-be-empty-element.test.tsx b/src/matchers/__tests__/to-be-empty-element.test.tsx index f38047db3..f5ea32207 100644 --- a/src/matchers/__tests__/to-be-empty-element.test.tsx +++ b/src/matchers/__tests__/to-be-empty-element.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View } from 'react-native'; +import { Text, View } from 'react-native'; import { render, screen } from '../..'; @@ -13,6 +13,7 @@ test('toBeEmptyElement() base case', () => { render( + Hello , ); @@ -33,7 +34,21 @@ test('toBeEmptyElement() base case', () => { Received: " + /> + + Hello + " + `); + + const text = screen.getByTestId('text'); + expect(text).not.toBeEmptyElement(); + expect(() => expect(text).toBeEmptyElement()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeEmptyElement() + + Received: + Hello" `); }); diff --git a/src/matchers/__tests__/to-have-accessible-name.test.tsx b/src/matchers/__tests__/to-have-accessible-name.test.tsx index 0337f2ba3..1f433d857 100644 --- a/src/matchers/__tests__/to-have-accessible-name.test.tsx +++ b/src/matchers/__tests__/to-have-accessible-name.test.tsx @@ -119,14 +119,14 @@ test('toHaveAccessibleName() handles a view without name when called without exp }); it('toHaveAccessibleName() rejects non-host element', () => { - const nonElement = 'This is not a ReactTestInstance'; + const nonElement = 'This is not a HostElement'; expect(() => expect(nonElement).toHaveAccessibleName()).toThrowErrorMatchingInlineSnapshot(` "expect(received).toHaveAccessibleName() received value must be a host element. Received has type: string - Received has value: "This is not a ReactTestInstance"" + Received has value: "This is not a HostElement"" `); expect(() => expect(nonElement).not.toHaveAccessibleName()).toThrowErrorMatchingInlineSnapshot(` @@ -134,6 +134,6 @@ it('toHaveAccessibleName() rejects non-host element', () => { received value must be a host element. Received has type: string - Received has value: "This is not a ReactTestInstance"" + Received has value: "This is not a HostElement"" `); }); diff --git a/src/matchers/__tests__/to-have-text-content.test.tsx b/src/matchers/__tests__/to-have-text-content.test.tsx index edb0724cb..ab9029bce 100644 --- a/src/matchers/__tests__/to-have-text-content.test.tsx +++ b/src/matchers/__tests__/to-have-text-content.test.tsx @@ -6,7 +6,8 @@ import { render, screen } from '../..'; test('toHaveTextContent() example test', () => { render( - Hello World + Hello + World , ); diff --git a/src/matchers/__tests__/utils.test.tsx b/src/matchers/__tests__/utils.test.tsx index 7c95138da..def0c84c8 100644 --- a/src/matchers/__tests__/utils.test.tsx +++ b/src/matchers/__tests__/utils.test.tsx @@ -17,15 +17,6 @@ test('checkHostElement allows host element', () => { }).not.toThrow(); }); -test('checkHostElement allows rejects composite element', () => { - render(); - - expect(() => { - // @ts-expect-error: intentionally passing wrong element shape - checkHostElement(screen.UNSAFE_root, fakeMatcher, {}); - }).toThrow(/value must be a host element./); -}); - test('checkHostElement allows rejects null element', () => { expect(() => { // @ts-expect-error: intentionally passing wrong element shape diff --git a/src/matchers/to-be-busy.ts b/src/matchers/to-be-busy.ts index 6af30d9bc..969c99c6c 100644 --- a/src/matchers/to-be-busy.ts +++ b/src/matchers/to-be-busy.ts @@ -1,12 +1,12 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaBusy } from '../helpers/accessibility'; import { formatElement } from '../helpers/format-element'; import { checkHostElement } from './utils'; -export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeBusy(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeBusy, this); return { diff --git a/src/matchers/to-be-checked.ts b/src/matchers/to-be-checked.ts index 2a5dbacd1..0dcdb1d06 100644 --- a/src/matchers/to-be-checked.ts +++ b/src/matchers/to-be-checked.ts @@ -1,6 +1,6 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaChecked, @@ -13,7 +13,7 @@ import { formatElement } from '../helpers/format-element'; import { isHostSwitch } from '../helpers/host-component-names'; import { checkHostElement } from './utils'; -export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeChecked(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeChecked, this); if (!isHostSwitch(element) && !isSupportedAccessibilityElement(element)) { @@ -37,7 +37,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc }; } -function isSupportedAccessibilityElement(element: ReactTestInstance) { +function isSupportedAccessibilityElement(element: HostElement) { if (!isAccessibilityElement(element)) { return false; } diff --git a/src/matchers/to-be-disabled.ts b/src/matchers/to-be-disabled.ts index 96b5dadab..3640a2938 100644 --- a/src/matchers/to-be-disabled.ts +++ b/src/matchers/to-be-disabled.ts @@ -1,13 +1,12 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaDisabled } from '../helpers/accessibility'; -import { getHostParent } from '../helpers/component-tree'; import { formatElement } from '../helpers/format-element'; import { checkHostElement } from './utils'; -export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeDisabled(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeDisabled, this); const isDisabled = computeAriaDisabled(element) || isAncestorDisabled(element); @@ -26,7 +25,7 @@ export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstan }; } -export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeEnabled(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeEnabled, this); const isEnabled = !computeAriaDisabled(element) && !isAncestorDisabled(element); @@ -45,8 +44,8 @@ export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstanc }; } -function isAncestorDisabled(element: ReactTestInstance): boolean { - const parent = getHostParent(element); +function isAncestorDisabled(element: HostElement): boolean { + const parent = element.parent; if (parent == null) { return false; } diff --git a/src/matchers/to-be-empty-element.ts b/src/matchers/to-be-empty-element.ts index 31c1d9e08..2466a46f9 100644 --- a/src/matchers/to-be-empty-element.ts +++ b/src/matchers/to-be-empty-element.ts @@ -1,18 +1,17 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; -import { getHostChildren } from '../helpers/component-tree'; import { formatElementList } from '../helpers/format-element'; import { checkHostElement } from './utils'; -export function toBeEmptyElement(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeEmptyElement(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeEmptyElement, this); - const hostChildren = getHostChildren(element); + const hostChildren = element?.children; return { - pass: hostChildren.length === 0, + pass: hostChildren?.length === 0, message: () => { return [ matcherHint(`${this.isNot ? '.not' : ''}.toBeEmptyElement`, 'element', ''), diff --git a/src/matchers/to-be-expanded.ts b/src/matchers/to-be-expanded.ts index 4fd6a656e..632e872f4 100644 --- a/src/matchers/to-be-expanded.ts +++ b/src/matchers/to-be-expanded.ts @@ -1,12 +1,12 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaExpanded } from '../helpers/accessibility'; import { formatElement } from '../helpers/format-element'; import { checkHostElement } from './utils'; -export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeExpanded(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeExpanded, this); return { @@ -23,7 +23,7 @@ export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstan }; } -export function toBeCollapsed(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeCollapsed(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeCollapsed, this); return { diff --git a/src/matchers/to-be-on-the-screen.ts b/src/matchers/to-be-on-the-screen.ts index cbdbdf378..76c0682cf 100644 --- a/src/matchers/to-be-on-the-screen.ts +++ b/src/matchers/to-be-on-the-screen.ts @@ -1,18 +1,18 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; -import { getUnsafeRootElement } from '../helpers/component-tree'; +import { getContainerElement } from '../helpers/component-tree'; import { formatElement } from '../helpers/format-element'; import { screen } from '../screen'; import { checkHostElement } from './utils'; -export function toBeOnTheScreen(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeOnTheScreen(this: jest.MatcherContext, element: HostElement) { if (element !== null || !this.isNot) { checkHostElement(element, toBeOnTheScreen, this); } - const pass = element === null ? false : screen.UNSAFE_root === getUnsafeRootElement(element); + const pass = element === null ? false : screen.container === getContainerElement(element); const errorFound = () => { return `expected element tree not to contain element, but found\n${redent( diff --git a/src/matchers/to-be-partially-checked.ts b/src/matchers/to-be-partially-checked.ts index 1224de1aa..166faa703 100644 --- a/src/matchers/to-be-partially-checked.ts +++ b/src/matchers/to-be-partially-checked.ts @@ -1,13 +1,13 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility'; import { ErrorWithStack } from '../helpers/errors'; import { formatElement } from '../helpers/format-element'; import { checkHostElement } from './utils'; -export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBePartiallyChecked(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBePartiallyChecked, this); if (!hasValidAccessibilityRole(element)) { @@ -31,7 +31,7 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe }; } -function hasValidAccessibilityRole(element: ReactTestInstance) { +function hasValidAccessibilityRole(element: HostElement) { const role = getRole(element); return isAccessibilityElement(element) && role === 'checkbox'; } diff --git a/src/matchers/to-be-selected.ts b/src/matchers/to-be-selected.ts index f33fe8449..834d642e2 100644 --- a/src/matchers/to-be-selected.ts +++ b/src/matchers/to-be-selected.ts @@ -1,12 +1,12 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaSelected } from '../helpers/accessibility'; import { formatElement } from '../helpers/format-element'; import { checkHostElement } from './utils'; -export function toBeSelected(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeSelected(this: jest.MatcherContext, element: HostElement) { checkHostElement(element, toBeSelected, this); return { diff --git a/src/matchers/to-be-visible.ts b/src/matchers/to-be-visible.ts index d21b112e9..ab9ba6630 100644 --- a/src/matchers/to-be-visible.ts +++ b/src/matchers/to-be-visible.ts @@ -1,15 +1,14 @@ import { StyleSheet } from 'react-native'; -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; import { isHiddenFromAccessibility } from '../helpers/accessibility'; -import { getHostParent } from '../helpers/component-tree'; import { formatElement } from '../helpers/format-element'; import { isHostModal } from '../helpers/host-component-names'; import { checkHostElement } from './utils'; -export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstance) { +export function toBeVisible(this: jest.MatcherContext, element: HostElement) { if (element !== null || !this.isNot) { checkHostElement(element, toBeVisible, this); } @@ -29,11 +28,11 @@ export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstanc } function isElementVisible( - element: ReactTestInstance, - accessibilityCache?: WeakMap, + element: HostElement, + accessibilityCache?: WeakMap, ): boolean { // Use cache to speed up repeated searches by `isHiddenFromAccessibility`. - const cache = accessibilityCache ?? new WeakMap(); + const cache = accessibilityCache ?? new WeakMap(); if (isHiddenFromAccessibility(element, { cache })) { return false; } @@ -48,7 +47,7 @@ function isElementVisible( return false; } - const hostParent = getHostParent(element); + const hostParent = element.parent; if (hostParent === null) { return true; } @@ -56,7 +55,7 @@ function isElementVisible( return isElementVisible(hostParent, cache); } -function isHiddenForStyles(element: ReactTestInstance) { +function isHiddenForStyles(element: HostElement) { const flatStyle = StyleSheet.flatten(element.props.style); return flatStyle?.display === 'none' || flatStyle?.opacity === 0; } diff --git a/src/matchers/to-contain-element.ts b/src/matchers/to-contain-element.ts index c891cf7a3..663d3000d 100644 --- a/src/matchers/to-contain-element.ts +++ b/src/matchers/to-contain-element.ts @@ -1,14 +1,15 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; import redent from 'redent'; +import type { HostElement } from 'universal-test-renderer'; +import { findAll } from '../helpers/find-all'; import { formatElement } from '../helpers/format-element'; import { checkHostElement } from './utils'; export function toContainElement( this: jest.MatcherContext, - container: ReactTestInstance, - element: ReactTestInstance | null, + container: HostElement, + element: HostElement | null, ) { checkHostElement(container, toContainElement, this); @@ -16,9 +17,9 @@ export function toContainElement( checkHostElement(element, toContainElement, this); } - let matches: ReactTestInstance[] = []; + let matches: HostElement[] = []; if (element) { - matches = container.findAll((node) => node === element); + matches = findAll(container, (node) => node === element); } return { diff --git a/src/matchers/to-have-accessibility-value.ts b/src/matchers/to-have-accessibility-value.ts index 6c5ec423b..eff8b4edf 100644 --- a/src/matchers/to-have-accessibility-value.ts +++ b/src/matchers/to-have-accessibility-value.ts @@ -1,5 +1,5 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, stringify } from 'jest-matcher-utils'; +import type { HostElement } from 'universal-test-renderer'; import { computeAriaValue } from '../helpers/accessibility'; import type { AccessibilityValueMatcher } from '../helpers/matchers/match-accessibility-value'; @@ -9,7 +9,7 @@ import { checkHostElement, formatMessage } from './utils'; export function toHaveAccessibilityValue( this: jest.MatcherContext, - element: ReactTestInstance, + element: HostElement, expectedValue: AccessibilityValueMatcher, ) { checkHostElement(element, toHaveAccessibilityValue, this); diff --git a/src/matchers/to-have-accessible-name.ts b/src/matchers/to-have-accessible-name.ts index 6cdf9b07a..71e09f190 100644 --- a/src/matchers/to-have-accessible-name.ts +++ b/src/matchers/to-have-accessible-name.ts @@ -1,5 +1,5 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; +import type { HostElement } from 'universal-test-renderer'; import { computeAccessibleName } from '../helpers/accessibility'; import type { TextMatch, TextMatchOptions } from '../matches'; @@ -8,7 +8,7 @@ import { checkHostElement, formatMessage } from './utils'; export function toHaveAccessibleName( this: jest.MatcherContext, - element: ReactTestInstance, + element: HostElement, expectedName?: TextMatch, options?: TextMatchOptions, ) { diff --git a/src/matchers/to-have-display-value.ts b/src/matchers/to-have-display-value.ts index d7284b3e5..e8a3b4496 100644 --- a/src/matchers/to-have-display-value.ts +++ b/src/matchers/to-have-display-value.ts @@ -1,5 +1,5 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; +import type { HostElement } from 'universal-test-renderer'; import { ErrorWithStack } from '../helpers/errors'; import { isHostTextInput } from '../helpers/host-component-names'; @@ -10,7 +10,7 @@ import { checkHostElement, formatMessage } from './utils'; export function toHaveDisplayValue( this: jest.MatcherContext, - element: ReactTestInstance, + element: HostElement, expectedValue: TextMatch, options?: TextMatchOptions, ) { diff --git a/src/matchers/to-have-prop.ts b/src/matchers/to-have-prop.ts index ce0b6204b..c72ef6cc8 100644 --- a/src/matchers/to-have-prop.ts +++ b/src/matchers/to-have-prop.ts @@ -1,11 +1,11 @@ -import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, printExpected, stringify } from 'jest-matcher-utils'; +import type { HostElement } from 'universal-test-renderer'; import { checkHostElement, formatMessage } from './utils'; export function toHaveProp( this: jest.MatcherContext, - element: ReactTestInstance, + element: HostElement, name: string, expectedValue: unknown, ) { diff --git a/src/matchers/to-have-style.ts b/src/matchers/to-have-style.ts index 9cc1c96c1..9ca880a45 100644 --- a/src/matchers/to-have-style.ts +++ b/src/matchers/to-have-style.ts @@ -1,7 +1,7 @@ import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native'; import { StyleSheet } from 'react-native'; -import type { ReactTestInstance } from 'react-test-renderer'; import { diff, matcherHint } from 'jest-matcher-utils'; +import type { HostElement } from 'universal-test-renderer'; import { checkHostElement, formatMessage } from './utils'; @@ -11,7 +11,7 @@ type StyleLike = Record; export function toHaveStyle( this: jest.MatcherContext, - element: ReactTestInstance, + element: HostElement, style: StyleProp