diff --git a/package.json b/package.json index 9e4b84bff..dbbb2dbce 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "typecheck": "tsc", "lint": "eslint src --cache", "validate": "yarn prettier && yarn lint && yarn typecheck && yarn test", + "validate:write": "yarn prettier:write && yarn lint --fix && yarn typecheck && yarn test -u", "build:js": "babel src --out-dir build --extensions \".js,.ts,.jsx,.tsx\" --source-maps --ignore \"**/__tests__/**\"", "build:ts": "tsc --build tsconfig.release.json", "build": "yarn clean && yarn build:js && yarn build:ts", diff --git a/src/__tests__/__snapshots__/render.test.tsx.snap b/src/__tests__/__snapshots__/unsafe-render-sync.test.tsx.snap similarity index 100% rename from src/__tests__/__snapshots__/render.test.tsx.snap rename to src/__tests__/__snapshots__/unsafe-render-sync.test.tsx.snap diff --git a/src/__tests__/act.test.tsx b/src/__tests__/act.test.tsx index 036b80a14..c54e35c38 100644 --- a/src/__tests__/act.test.tsx +++ b/src/__tests__/act.test.tsx @@ -16,23 +16,23 @@ const Counter = () => { return setCount(count + 1)}>{text}; }; -test('render should trigger useEffect', () => { +test('render should trigger useEffect', async () => { const effectCallback = jest.fn(); - render(); + await render(); expect(effectCallback).toHaveBeenCalledTimes(1); }); -test('rerender should trigger useEffect', () => { +test('rerender should trigger useEffect', async () => { const effectCallback = jest.fn(); - render(); - screen.rerender(); + await render(); + await screen.rerender(); expect(effectCallback).toHaveBeenCalledTimes(2); }); test('fireEvent should trigger useState', async () => { - render(); + await render(); const counter = screen.getByText(/Total count/i); expect(counter.props.children).toEqual('Total count: 0'); diff --git a/src/__tests__/auto-cleanup-skip.test.tsx b/src/__tests__/auto-cleanup-skip.test.tsx index d6d51c50f..1bf6c419a 100644 --- a/src/__tests__/auto-cleanup-skip.test.tsx +++ b/src/__tests__/auto-cleanup-skip.test.tsx @@ -29,9 +29,9 @@ class Test extends React.Component<{ onUnmount?(): void }> { // This just verifies that by importing RNTL in pure mode in an environment which supports // afterEach (like jest) we won't get automatic cleanup between tests. -test('component is mounted, but not umounted before test ends', () => { +test('component is mounted, but not umounted before test ends', async () => { const fn = jest.fn(); - render(); + await render(); expect(fn).not.toHaveBeenCalled(); }); diff --git a/src/__tests__/auto-cleanup.test.tsx b/src/__tests__/auto-cleanup.test.tsx index 75453c93c..a4a3a6202 100644 --- a/src/__tests__/auto-cleanup.test.tsx +++ b/src/__tests__/auto-cleanup.test.tsx @@ -27,9 +27,9 @@ 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 umounted before test ends', async () => { const fn = jest.fn(); - render(); + await render(); expect(isMounted).toEqual(true); expect(fn).not.toHaveBeenCalled(); }); @@ -38,14 +38,14 @@ test('component is automatically umounted after first test ends', () => { expect(isMounted).toEqual(false); }); -test('does not time out with legacy fake timers', () => { +test('does not time out with legacy fake timers', async () => { jest.useFakeTimers({ legacyFakeTimers: true }); - render(); + await render(); expect(isMounted).toEqual(true); }); -test('does not time out with fake timers', () => { +test('does not time out with fake timers', async () => { jest.useFakeTimers(); - render(); + await render(); expect(isMounted).toEqual(true); }); diff --git a/src/__tests__/cleanup.test.tsx b/src/__tests__/cleanup.test.tsx index e09f3ce9b..f6036d028 100644 --- a/src/__tests__/cleanup.test.tsx +++ b/src/__tests__/cleanup.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { View } from 'react-native'; -import { cleanupAsync, render, renderAsync } from '../pure'; +import { cleanup, render, unsafe_renderSync } from '../pure'; class Test extends React.Component<{ onUnmount: () => void }> { componentWillUnmount() { @@ -17,21 +17,21 @@ class Test extends React.Component<{ onUnmount: () => void }> { test('cleanup after render', async () => { const fn = jest.fn(); - render(); - render(); + await render(); + await render(); expect(fn).not.toHaveBeenCalled(); - await cleanupAsync(); + await cleanup(); expect(fn).toHaveBeenCalledTimes(2); }); -test('cleanup after renderAsync', async () => { +test('cleanup after unsafe_renderSync', async () => { const fn = jest.fn(); - await renderAsync(); - await renderAsync(); + unsafe_renderSync(); + unsafe_renderSync(); expect(fn).not.toHaveBeenCalled(); - await cleanupAsync(); + await cleanup(); expect(fn).toHaveBeenCalledTimes(2); }); diff --git a/src/__tests__/event-handler.test.tsx b/src/__tests__/event-handler.test.tsx index 2c7f5ac43..0b200d7ca 100644 --- a/src/__tests__/event-handler.test.tsx +++ b/src/__tests__/event-handler.test.tsx @@ -4,11 +4,11 @@ import { Text, View } from 'react-native'; import { render, screen } from '..'; import { getEventHandlerFromProps } from '../event-handler'; -test('getEventHandler strict mode', () => { +test('getEventHandler strict mode', async () => { const onPress = jest.fn(); const testOnlyOnPress = jest.fn(); - render( + await render( {/* @ts-expect-error Intentionally passing such props */} @@ -31,11 +31,11 @@ test('getEventHandler strict mode', () => { expect(getEventHandlerFromProps(both.props, 'onPress')).toBe(undefined); }); -test('getEventHandler loose mode', () => { +test('getEventHandler loose mode', async () => { const onPress = jest.fn(); const testOnlyOnPress = jest.fn(); - render( + await render( {/* @ts-expect-error Intentionally passing such props */} diff --git a/src/__tests__/fire-event-textInput.test.tsx b/src/__tests__/fire-event-textInput.test.tsx index 9e9f2ccb9..7223b1b21 100644 --- a/src/__tests__/fire-event-textInput.test.tsx +++ b/src/__tests__/fire-event-textInput.test.tsx @@ -20,7 +20,7 @@ test('should fire only non-touch-related events on non-editable TextInput', asyn const onSubmitEditing = jest.fn(); const onLayout = jest.fn(); - render( + await render( { test('should invoke specified event', async () => { const onPressMock = jest.fn(); - render(); + await render(); await fireEvent(screen.getByText('Press me'), 'press'); @@ -62,7 +62,7 @@ describe('fireEvent', () => { test('should invoke specified event on parent element', async () => { const onPressMock = jest.fn(); const text = 'New press text'; - render(); + await render(); await fireEvent(screen.getByText(text), 'press'); expect(onPressMock).toHaveBeenCalled(); @@ -72,7 +72,7 @@ describe('fireEvent', () => { const handlerMock = jest.fn(); const EVENT_DATA = 'event data'; - render( + await render( , @@ -93,7 +93,7 @@ test('fireEvent.press', async () => { pageY: 30, }, }; - render(); + await render(); await fireEvent.press(screen.getByText(text), eventData); @@ -110,7 +110,7 @@ test('fireEvent.scroll', async () => { }, }; - render( + await render( XD , @@ -124,7 +124,7 @@ test('fireEvent.scroll', async () => { test('fireEvent.changeText', async () => { const onChangeTextMock = jest.fn(); - render( + await render( , @@ -136,7 +136,7 @@ test('fireEvent.changeText', async () => { }); it('sets native state value for unmanaged text inputs', async () => { - render(); + await render(); const input = screen.getByTestId('input'); expect(input).toHaveDisplayValue(''); @@ -148,7 +148,7 @@ it('sets native state value for unmanaged text inputs', async () => { test('custom component with custom event name', async () => { const handlePress = jest.fn(); - render(); + await render(); await fireEvent(screen.getByText('Custom component'), 'handlePress'); @@ -158,7 +158,7 @@ test('custom component with custom event name', async () => { test('event with multiple handler parameters', async () => { const handlePress = jest.fn(); - render(); + await render(); await fireEvent(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2'); @@ -167,7 +167,7 @@ test('event with multiple handler parameters', async () => { test('should not fire on disabled TouchableOpacity', async () => { const handlePress = jest.fn(); - render( + await render( Trigger @@ -181,7 +181,7 @@ test('should not fire on disabled TouchableOpacity', async () => { test('should not fire on disabled Pressable', async () => { const handlePress = jest.fn(); - render( + await render( Trigger @@ -195,7 +195,7 @@ test('should not fire on disabled Pressable', async () => { test('should not fire inside View with pointerEvents="none"', async () => { const onPress = jest.fn(); - render( + await render( Trigger @@ -210,7 +210,7 @@ test('should not fire inside View with pointerEvents="none"', async () => { test('should not fire inside View with pointerEvents="box-only"', async () => { const onPress = jest.fn(); - render( + await render( Trigger @@ -225,7 +225,7 @@ test('should not fire inside View with pointerEvents="box-only"', async () => { test('should fire inside View with pointerEvents="box-none"', async () => { const onPress = jest.fn(); - render( + await render( Trigger @@ -240,7 +240,7 @@ test('should fire inside View with pointerEvents="box-none"', async () => { test('should fire inside View with pointerEvents="auto"', async () => { const onPress = jest.fn(); - render( + await render( Trigger @@ -255,7 +255,7 @@ test('should fire inside View with pointerEvents="auto"', async () => { test('should not fire deeply inside View with pointerEvents="box-only"', async () => { const onPress = jest.fn(); - render( + await render( @@ -272,7 +272,7 @@ test('should not fire deeply inside View with pointerEvents="box-only"', async ( test('should fire non-pointer events inside View with pointerEvents="box-none"', async () => { const onTouchStart = jest.fn(); - render(); + await render(); await fireEvent(screen.getByTestId('view'), 'touchStart'); expect(onTouchStart).toHaveBeenCalled(); @@ -280,7 +280,7 @@ test('should fire non-pointer events inside View with pointerEvents="box-none"', test('should fire non-touch events inside View with pointerEvents="box-none"', async () => { const onLayout = jest.fn(); - render(); + await render(); await fireEvent(screen.getByTestId('view'), 'layout'); expect(onLayout).toHaveBeenCalled(); @@ -290,7 +290,7 @@ test('should fire non-touch events inside View with pointerEvents="box-none"', a // the 'press' event on host View rendered by pressable. test('should fire on Pressable with pointerEvents="box-only', async () => { const onPress = jest.fn(); - render(); + await render(); await fireEvent.press(screen.getByTestId('pressable')); expect(onPress).toHaveBeenCalled(); @@ -299,7 +299,7 @@ test('should fire on Pressable with pointerEvents="box-only', async () => { test('should pass event up on disabled TouchableOpacity', async () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); - render( + await render( Inner Trigger @@ -315,7 +315,7 @@ test('should pass event up on disabled TouchableOpacity', async () => { test('should pass event up on disabled Pressable', async () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); - render( + await render( Inner Trigger @@ -342,7 +342,7 @@ const TestComponent = ({ onPress }: TestComponentProps) => { test('is not fooled by non-native disabled prop', async () => { const handlePress = jest.fn(); - render(); + await render(); await fireEvent.press(screen.getByText('Trigger Test')); expect(handlePress).toHaveBeenCalledTimes(1); @@ -366,7 +366,7 @@ function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableCo test('is not fooled by non-responder wrapping host elements', async () => { const handlePress = jest.fn(); - render( + await render( , @@ -394,7 +394,7 @@ function TestDraggableComponent({ onDrag }: TestDraggableComponentProps) { test('has only onMove', async () => { const handleDrag = jest.fn(); - render(); + await render(); await fireEvent(screen.getByText('Trigger'), 'responderMove', { touchHistory: { mostRecentTimeStamp: '2', touchBank: [] }, @@ -407,7 +407,7 @@ test('has only onMove', async () => { describe('native events', () => { test('triggers onScrollBeginDrag', async () => { const onScrollBeginDragSpy = jest.fn(); - render(); + await render(); await fireEvent(screen.getByTestId('test-id'), 'onScrollBeginDrag'); expect(onScrollBeginDragSpy).toHaveBeenCalled(); @@ -415,7 +415,7 @@ describe('native events', () => { test('triggers onScrollEndDrag', async () => { const onScrollEndDragSpy = jest.fn(); - render(); + await render(); await fireEvent(screen.getByTestId('test-id'), 'onScrollEndDrag'); expect(onScrollEndDragSpy).toHaveBeenCalled(); @@ -423,7 +423,7 @@ describe('native events', () => { test('triggers onMomentumScrollBegin', async () => { const onMomentumScrollBeginSpy = jest.fn(); - render(); + await render(); await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollBegin'); expect(onMomentumScrollBeginSpy).toHaveBeenCalled(); @@ -431,7 +431,7 @@ describe('native events', () => { test('triggers onMomentumScrollEnd', async () => { const onMomentumScrollEndSpy = jest.fn(); - render(); + await render(); await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollEnd'); expect(onMomentumScrollEndSpy).toHaveBeenCalled(); @@ -472,7 +472,7 @@ describe('React.Suspense integration', () => { test('should handle events after Suspense resolves', async () => { const onPressMock = jest.fn(); - render( + await render( , @@ -483,8 +483,8 @@ describe('React.Suspense integration', () => { // Resolve the promise resolveMockPromise('loaded'); - await waitFor(() => { - screen.rerender( + await waitFor(async () => { + await screen.rerender( , @@ -512,7 +512,7 @@ describe('React.Suspense integration', () => { ); } - render( + await render( }> , @@ -547,7 +547,7 @@ describe('React.Suspense integration', () => { ); } - const { rerender } = render( + const { rerender } = await render( Outer Loading...}> Inner Loading...}> @@ -566,8 +566,8 @@ describe('React.Suspense integration', () => { // Resolve inner component resolveMockPromise('inner-loaded'); - await waitFor(() => { - rerender( + await waitFor(async () => { + await rerender( Outer Loading...}> Inner Loading...}> @@ -611,7 +611,7 @@ describe('React.Suspense integration', () => { ); } - render( + await render( Loading data...}> @@ -628,7 +628,7 @@ describe('React.Suspense integration', () => { expect(onPressMock).toHaveBeenCalled(); // Rerender - now DataComponent should suspend - screen.rerender( + await screen.rerender( Loading data...}> @@ -644,14 +644,14 @@ describe('React.Suspense integration', () => { test('should handle unmounted elements gracefully in async mode', async () => { const onPress = jest.fn(); - render( + await render( Test , ); const element = screen.getByText('Test'); - screen.unmount(); + await screen.unmount(); // Firing async event on unmounted element should not crash await fireEvent.press(element); diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 3e89dd84b..3fbf5fdf2 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -11,39 +11,39 @@ import { isHostTextInput, } from '../helpers/host-component-names'; -test('detects host Text component', () => { - render(Hello); +test('detects host Text component', async () => { + await render(Hello); expect(isHostText(screen.root)).toBe(true); }); // Some users might use the raw RCTText component directly for performance reasons. // See: https://blog.theodo.com/2023/10/native-views-rn-performance/ -test('detects raw RCTText component', () => { - render(React.createElement('RCTText', { testID: 'text' }, 'Hello')); +test('detects raw RCTText component', async () => { + await render(React.createElement('RCTText', { testID: 'text' }, 'Hello')); expect(isHostText(screen.root)).toBe(true); }); -test('detects host TextInput component', () => { - render(); +test('detects host TextInput component', async () => { + await render(); expect(isHostTextInput(screen.root)).toBe(true); }); -test('detects host Image component', () => { - render(); +test('detects host Image component', async () => { + await render(); expect(isHostImage(screen.root)).toBe(true); }); -test('detects host Switch component', () => { - render(); +test('detects host Switch component', async () => { + await render(); expect(isHostSwitch(screen.root)).toBe(true); }); -test('detects host ScrollView component', () => { - render(); +test('detects host ScrollView component', async () => { + await render(); expect(isHostScrollView(screen.root)).toBe(true); }); -test('detects host Modal component', () => { - render(); +test('detects host Modal component', async () => { + await render(); expect(isHostModal(screen.root)).toBe(true); }); diff --git a/src/__tests__/host-text-nesting.test.tsx b/src/__tests__/host-text-nesting.test.tsx index 6e5b11915..c15c480d4 100644 --- a/src/__tests__/host-text-nesting.test.tsx +++ b/src/__tests__/host-text-nesting.test.tsx @@ -4,19 +4,19 @@ import { Pressable, Text, View } from 'react-native'; import { render, screen } from '../pure'; describe('nested text handling', () => { - test('basic', () => { - render(Hello); + test('basic', async () => { + await render(Hello); expect(screen.getByText('Hello')).toBeTruthy(); }); - test('role with direct text children', () => { - render(About); + test('role with direct text children', async () => { + await render(About); expect(screen.getByRole('header', { name: 'About' })).toBeTruthy(); }); - test('nested text with child with role', () => { - render( + test('nested text with child with role', async () => { + await render( About @@ -27,8 +27,8 @@ describe('nested text handling', () => { expect(screen.getByRole('header', { name: 'About' }).props.testID).toBe('child'); }); - test('pressable within View, with text child', () => { - render( + test('pressable within View, with text child', async () => { + await render( Save @@ -39,8 +39,8 @@ describe('nested text handling', () => { expect(screen.getByRole('button', { name: 'Save' }).props.testID).toBe('pressable'); }); - test('pressable within View, with text child within view', () => { - render( + test('pressable within View, with text child within view', async () => { + await render( @@ -53,8 +53,8 @@ describe('nested text handling', () => { expect(screen.getByRole('button', { name: 'Save' }).props.testID).toBe('pressable'); }); - test('Text within pressable', () => { - render( + test('Text within pressable', async () => { + await render( Save , @@ -63,8 +63,8 @@ describe('nested text handling', () => { expect(screen.getByText('Save').props.testID).toBe('text'); }); - test('Text within view within pressable', () => { - render( + test('Text within view within pressable', async () => { + await render( Save diff --git a/src/__tests__/questionsBoard.test.tsx b/src/__tests__/questionsBoard.test.tsx index c8be45eed..1eb97dcbb 100644 --- a/src/__tests__/questionsBoard.test.tsx +++ b/src/__tests__/questionsBoard.test.tsx @@ -44,7 +44,7 @@ test('form submits two answers', async () => { const onSubmit = jest.fn(); const user = userEvent.setup(); - render(); + await render(); const answerInputs = screen.getAllByLabelText('answer input'); await user.type(answerInputs[0], 'a1'); diff --git a/src/__tests__/react-native-animated.test.tsx b/src/__tests__/react-native-animated.test.tsx index 8d617fedd..87f3f582b 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 '..'; @@ -44,9 +44,9 @@ describe('AnimatedView', () => { }); it('should use native driver when useNativeDriver is true', async () => { - render( + await render( - Test + Test , ); expect(screen.root).toHaveStyle({ opacity: 0 }); @@ -57,9 +57,9 @@ describe('AnimatedView', () => { }); it('should not use native driver when useNativeDriver is false', async () => { - render( + await render( - Test + Test , ); expect(screen.root).toHaveStyle({ opacity: 0 }); diff --git a/src/__tests__/react-native-api.test.tsx b/src/__tests__/react-native-api.test.tsx index b4926ccc9..76c2f0c03 100644 --- a/src/__tests__/react-native-api.test.tsx +++ b/src/__tests__/react-native-api.test.tsx @@ -10,8 +10,8 @@ import { getReactNativeVersion } from '../test-utils/version'; * changed in a way that may impact our code like queries or event handling. */ -test('React Native API assumption: renders a single host element', () => { - render(); +test('React Native API assumption: renders a single host element', async () => { + await render(); expect(screen).toMatchInlineSnapshot(` renders a single host element', () => `); }); -test('React Native API assumption: renders a single host element', () => { - render(Hello); +test('React Native API assumption: renders a single host element', async () => { + await render(Hello); expect(screen).toMatchInlineSnapshot(` renders a single host element', () => `); }); -test('React Native API assumption: nested renders a single host element', () => { - render( +test('React Native API assumption: nested renders a single host element', async () => { + await render( Before Hello @@ -66,8 +66,8 @@ test('React Native API assumption: nested renders a single host element', `); }); -test('React Native API assumption: renders a single host element', () => { - render( +test('React Native API assumption: renders a single host element', async () => { + await render( renders a single host element', ( `); }); -test('React Native API assumption: with nested Text renders single host element', () => { - render( +test('React Native API assumption: with nested Text renders single host element', async () => { + await render( Hello , @@ -105,8 +105,8 @@ test('React Native API assumption: with nested Text renders single h `); }); -test('React Native API assumption: renders a single host element', () => { - render(); +test('React Native API assumption: renders a single host element', async () => { + await render(); expect( mapJsonProps(screen.toJSON(), { @@ -125,8 +125,10 @@ test('React Native API assumption: renders a single host element', () = `); }); -test('React Native API assumption: renders a single host element', () => { - render(Alt text); +test('React Native API assumption: renders a single host element', async () => { + await render( + Alt text, + ); expect(screen).toMatchInlineSnapshot(` renders a single host element', () => `); }); -test('React Native API assumption: renders a single host element', () => { - render( +test('React Native API assumption: renders a single host element', async () => { + await render( , @@ -161,8 +163,8 @@ test('React Native API assumption: renders a single host element', `); }); -test('React Native API assumption: renders a single host element', () => { - render( +test('React Native API assumption: renders a single host element', async () => { + await render( {item}} />, ); @@ -215,8 +217,8 @@ test('React Native API assumption: renders a single host `); }); -test('React Native API assumption: renders a single host element', () => { - render( +test('React Native API assumption: renders a single host element', async () => { + await render( Modal Content , @@ -250,8 +252,8 @@ test('React Native API assumption: renders a single host element', () => } }); -test('React Native API assumption: aria-* props render directly on host View', () => { - render( +test('React Native API assumption: aria-* props render directly on host View', async () => { + await render( { - render( +test('React Native API assumption: aria-* props render directly on host Text', async () => { + await render( { - render( +test('React Native API assumption: aria-* props render directly on host TextInput', async () => { + await render( { const onPressOut = jest.fn(); const onLongPress = jest.fn(); - render( + await render( { const { events, logEvent } = createEventLogger(); const user = userEvent.setup(); - render( + await render( { - 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() { - return ( - - Is the banana fresh? - {this.state.fresh ? 'fresh' : 'not fresh'} - - ); - } -} - -test('renderAsync renders component asynchronously', async () => { - await renderAsync(); - expect(screen.getByTestId('test')).toBeOnTheScreen(); -}); - -test('renderAsync with wrapper option', async () => { - const WrapperComponent = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - await renderAsync(, { - wrapper: WrapperComponent, - }); - - expect(screen.getByTestId('wrapper')).toBeTruthy(); - expect(screen.getByTestId('inner')).toBeTruthy(); -}); - -test('rerender function throws error when used with renderAsync', async () => { - await renderAsync(); - - expect(() => screen.rerender()).toThrowErrorMatchingInlineSnapshot( - `""rerender(...)" is not supported when using "renderAsync" use "await rerenderAsync(...)" instead"`, - ); -}); - -test('rerenderAsync function updates component asynchronously', async () => { - const fn = jest.fn(); - await renderAsync(); - expect(fn).toHaveBeenCalledTimes(0); - - await screen.rerenderAsync(); - expect(fn).toHaveBeenCalledTimes(1); -}); - -test('unmount function throws error when used with renderAsync', async () => { - await renderAsync(); - - expect(() => screen.unmount()).toThrowErrorMatchingInlineSnapshot( - `""unmount()" is not supported when using "renderAsync" use "await unmountAsync()" instead"`, - ); -}); - -test('unmountAsync function unmounts component asynchronously', async () => { - const fn = jest.fn(); - await renderAsync(); - - await screen.unmountAsync(); - expect(fn).toHaveBeenCalled(); -}); diff --git a/src/__tests__/render-debug.test.tsx b/src/__tests__/render-debug.test.tsx index 544c2e45a..21b37deb3 100644 --- a/src/__tests__/render-debug.test.tsx +++ b/src/__tests__/render-debug.test.tsx @@ -90,8 +90,8 @@ class Banana extends React.Component { } } -test('debug', () => { - render(); +test('debug', async () => { + await render(); screen.debug(); screen.debug({ message: 'another custom message' }); @@ -104,7 +104,7 @@ test('debug', () => { }); test('debug changing component', async () => { - render(); + await render(); await fireEvent.press(screen.getByRole('button', { name: 'Change freshness!' })); screen.debug({ mapProps: null }); @@ -113,16 +113,16 @@ test('debug changing component', async () => { expect(mockCalls[0][0]).toMatchSnapshot('bananaFresh button message should now be "fresh"'); }); -test('debug with only children prop', () => { - render(); +test('debug with only children prop', async () => { + await render(); screen.debug({ mapProps: () => ({}) }); const mockCalls = jest.mocked(logger.info).mock.calls; expect(mockCalls[0][0]).toMatchSnapshot(); }); -test('debug with only prop whose value is bananaChef', () => { - render(); +test('debug with only prop whose value is bananaChef', async () => { + await render(); screen.debug({ mapProps: (props) => { const filterProps: Record = {}; @@ -139,10 +139,10 @@ test('debug with only prop whose value is bananaChef', () => { expect(mockCalls[0][0]).toMatchSnapshot(); }); -test('debug should use debugOptions from config when no option is specified', () => { +test('debug should use debugOptions from config when no option is specified', async () => { configure({ defaultDebugOptions: { mapProps: () => ({}) } }); - render( + await render( hello , @@ -153,17 +153,17 @@ test('debug should use debugOptions from config when no option is specified', () expect(mockCalls[0][0]).toMatchSnapshot(); }); -test('filtering out props through mapProps option should not modify component', () => { - render(); +test('filtering out props through mapProps option should not modify component', async () => { + await render(); screen.debug({ mapProps: () => ({}) }); expect(screen.getByTestId('viewTestID')).toBeTruthy(); }); -test('debug should use given options over config debugOptions', () => { +test('debug should use given options over config debugOptions', async () => { configure({ defaultDebugOptions: { mapProps: () => ({}) } }); - render( + await render( hello , diff --git a/src/__tests__/render-hook.test.tsx b/src/__tests__/render-hook.test.tsx index 782fec0d1..ca1f3f828 100644 --- a/src/__tests__/render-hook.test.tsx +++ b/src/__tests__/render-hook.test.tsx @@ -203,10 +203,10 @@ test('handles hook with suspense', async () => { }); class ErrorBoundary extends React.Component< - { children: React.ReactNode; fallback: string }, + { children: React.ReactNode; fallback: React.ReactNode }, { hasError: boolean } > { - constructor(props: { children: React.ReactNode; fallback: string }) { + constructor(props: { children: React.ReactNode; fallback: React.ReactNode }) { super(props); this.state = { hasError: false }; } @@ -233,7 +233,7 @@ test('handles hook suspense with error boundary', async () => { const { result } = await renderHook(useSuspendingHook, { initialProps: promise, wrapper: ({ children }) => ( - + Error Fallback}> Loading...}>{children} ), diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index ae5c8d350..97cd6c732 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -1,25 +1,7 @@ import * as React from 'react'; -import { Pressable, Text, TextInput, View } from 'react-native'; +import { Text, View } from 'react-native'; -import type { RenderAPI } from '..'; -import { fireEvent, render, screen } from '..'; - -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} - - ); - } -} +import { render, screen } from '..'; class Banana extends React.Component { state = { @@ -45,169 +27,46 @@ class Banana extends React.Component { }; render() { - const test = 0; return ( Is the banana fresh? {this.state.fresh ? 'fresh' : 'not fresh'} - - - - - - Change freshness! - - First Text - Second Text - {test} ); } } -test('supports basic rendering', () => { - render(); - expect(screen.root).toBeOnTheScreen(); -}); - -test('rerender', async () => { - const fn = jest.fn(); - render(); - expect(fn).toHaveBeenCalledTimes(0); - - await fireEvent.press(screen.getByText('Change freshness!')); - expect(fn).toHaveBeenCalledTimes(1); - - screen.rerender(); - expect(fn).toHaveBeenCalledTimes(2); -}); - -test('unmount', () => { - const fn = jest.fn(); - render(); - screen.unmount(); - expect(fn).toHaveBeenCalled(); -}); - -test('unmount should handle cleanup functions', () => { - const cleanup = jest.fn(); - const Component = () => { - React.useEffect(() => cleanup); - return null; - }; - - render(); - - screen.unmount(); - - expect(cleanup).toHaveBeenCalledTimes(1); -}); - -test('toJSON renders host output', () => { - render(press me); - expect(screen).toMatchSnapshot(); +test('render renders component asynchronously', async () => { + await render(); + expect(screen.getByTestId('test')).toBeOnTheScreen(); }); -test('renders options.wrapper around node', () => { - type WrapperComponentProps = { children: React.ReactNode }; - const WrapperComponent = ({ children }: WrapperComponentProps) => ( +test('render with wrapper option', async () => { + const WrapperComponent = ({ children }: { children: React.ReactNode }) => ( {children} ); - render(, { + await render(, { wrapper: WrapperComponent, }); expect(screen.getByTestId('wrapper')).toBeTruthy(); - expect(screen).toMatchInlineSnapshot(` - - - - `); + expect(screen.getByTestId('inner')).toBeTruthy(); }); -test('renders options.wrapper around updated node', () => { - type WrapperComponentProps = { children: React.ReactNode }; - const WrapperComponent = ({ children }: WrapperComponentProps) => ( - {children} - ); - - render(, { - wrapper: WrapperComponent, - }); - - screen.rerender(); - - expect(screen.getByTestId('wrapper')).toBeTruthy(); - expect(screen).toMatchInlineSnapshot(` - - - - `); -}); - -test('returns host root', () => { - render(); - - expect(screen.root).toBeDefined(); - expect(screen.root?.type).toBe('View'); - expect(screen.root?.props.testID).toBe('inner'); -}); - -test('RenderAPI type', () => { - render() as RenderAPI; - expect(true).toBeTruthy(); -}); - -test('returned output can be spread using rest operator', () => { - // Next line should not throw - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { rerender, ...rest } = render(); - expect(rest).toBeTruthy(); -}); - -test('rerenderAsync updates the component asynchronously', async () => { +test('rerender function updates component asynchronously', async () => { const fn = jest.fn(); - const result = render(); - - await result.rerenderAsync(); - - expect(fn).toHaveBeenCalledTimes(1); -}); - -test('updateAsync is an alias for rerenderAsync', async () => { - const fn = jest.fn(); - const result = render(); - - await result.updateAsync(); + await render(); + expect(fn).toHaveBeenCalledTimes(0); + await screen.rerender(); expect(fn).toHaveBeenCalledTimes(1); }); -test('unmountAsync unmounts the component asynchronously', async () => { +test('unmount function unmounts component asynchronously', async () => { const fn = jest.fn(); - const result = render(); - - await result.unmountAsync(); + await render(); + await screen.unmount(); expect(fn).toHaveBeenCalled(); }); diff --git a/src/__tests__/screen.test.tsx b/src/__tests__/screen.test.tsx index d41bd4e29..59b95bf4b 100644 --- a/src/__tests__/screen.test.tsx +++ b/src/__tests__/screen.test.tsx @@ -3,8 +3,8 @@ import { Text, View } from 'react-native'; import { render, screen } from '..'; -test('screen has the same queries as render result', () => { - const result = render(Mt. Everest); +test('screen has the same queries as render result', async () => { + const result = await render(Mt. Everest); expect(screen).toBe(result); expect(screen.getByText('Mt. Everest')).toBeTruthy(); @@ -13,10 +13,10 @@ test('screen has the same queries as render result', () => { expect(screen.queryAllByText('Mt. Everest')).toHaveLength(1); }); -test('screen holds last render result', () => { - render(Mt. Everest); - render(Mt. Blanc); - const finalResult = render(Śnieżka); +test('screen holds last render result', async () => { + await render(Mt. Everest); + await render(Mt. Blanc); + const finalResult = await render(Śnieżka); expect(screen).toBe(finalResult); expect(screen.getByText('Śnieżka')).toBeTruthy(); @@ -24,24 +24,24 @@ test('screen holds last render result', () => { expect(screen.queryByText('Mt. Blanc')).toBeFalsy(); }); -test('screen works with updating rerender', () => { - const result = render(Mt. Everest); +test('screen works with updating rerender', async () => { + const result = await render(Mt. Everest); expect(screen).toBe(result); - screen.rerender(Śnieżka); + await screen.rerender(Śnieżka); expect(screen).toBe(result); expect(screen.getByText('Śnieżka')).toBeTruthy(); }); -test('screen works with nested re-mounting rerender', () => { - const result = render( +test('screen works with nested re-mounting rerender', async () => { + const result = await render( Mt. Everest , ); expect(screen).toBe(result); - screen.rerender( + await screen.rerender( Śnieżka @@ -53,8 +53,8 @@ test('screen works with nested re-mounting rerender', () => { }); test('screen throws without render', () => { - expect(() => screen.container).toThrow('`render` method has not been called'); - expect(() => screen.root).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'); + expect(() => screen.container).toThrow('`render` function has not been called'); + expect(() => screen.root).toThrow('`render` function has not been called'); + expect(() => screen.debug()).toThrow('`render` function has not been called'); + expect(() => screen.getByText('Mt. Everest')).toThrow('`render` function has not been called'); }); diff --git a/src/__tests__/suspense-fake-timers.test.tsx b/src/__tests__/suspense-fake-timers.test.tsx index efb09a8ec..495971337 100644 --- a/src/__tests__/suspense-fake-timers.test.tsx +++ b/src/__tests__/suspense-fake-timers.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Text, View } from 'react-native'; -import { act, renderAsync, screen } from '..'; +import { act, render, screen } from '..'; import { excludeConsoleMessage } from '../test-utils/console'; jest.useFakeTimers(); @@ -26,7 +26,7 @@ test('resolves manually-controlled promise', async () => { resolvePromise = resolve; }); - await renderAsync( + await render( Loading...}> @@ -50,7 +50,7 @@ test('resolves timer-controlled promise', async () => { setTimeout(() => resolve(null), 100); }); - await renderAsync( + await render( Loading...}> @@ -96,7 +96,7 @@ test('handles promise rejection with error boundary', async () => { rejectPromise = reject; }); - await renderAsync( + await render( Error occurred}> Loading...}> @@ -126,7 +126,7 @@ test('handles multiple suspending components', async () => { resolvePromise2 = resolve; }); - await renderAsync( + await render( Loading...}> @@ -163,7 +163,7 @@ test('handles multiple suspense boundaries independently', async () => { resolvePromise2 = resolve; }); - await renderAsync( + await render( First Loading...}> diff --git a/src/__tests__/suspense.test.tsx b/src/__tests__/suspense.test.tsx index c84e93b12..eded69052 100644 --- a/src/__tests__/suspense.test.tsx +++ b/src/__tests__/suspense.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Text, View } from 'react-native'; -import { act, renderAsync, screen } from '..'; +import { act, render, screen } from '..'; import { excludeConsoleMessage } from '../test-utils/console'; // eslint-disable-next-line no-console @@ -24,7 +24,7 @@ test('resolves manually-controlled promise', async () => { resolvePromise = resolve; }); - await renderAsync( + await render( Loading...}> @@ -48,7 +48,7 @@ test('resolves timer-controlled promise', async () => { setTimeout(() => resolve(null), 100); }); - await renderAsync( + await render( Loading...}> @@ -93,7 +93,7 @@ test('handles promise rejection with error boundary', async () => { rejectPromise = reject; }); - await renderAsync( + await render( Error occurred}> Loading...}> @@ -123,7 +123,7 @@ test('handles multiple suspending components', async () => { resolvePromise2 = resolve; }); - await renderAsync( + await render( Loading...}> @@ -160,7 +160,7 @@ test('handles multiple suspense boundaries independently', async () => { resolvePromise2 = resolve; }); - await renderAsync( + await render( First Loading...}> diff --git a/src/__tests__/timers.test.ts b/src/__tests__/timers.test.ts index acb9ec4c5..2c3a40a75 100644 --- a/src/__tests__/timers.test.ts +++ b/src/__tests__/timers.test.ts @@ -1,4 +1,4 @@ -import waitFor from '../wait-for'; +import { waitFor } from '../wait-for'; describe.each([false, true])('fake timers tests (legacyFakeTimers = %s)', (legacyFakeTimers) => { beforeEach(() => { diff --git a/src/__tests__/deprecated/fire-event-sync.test.tsx b/src/__tests__/unsafe-fire-event-sync.test.tsx similarity index 67% rename from src/__tests__/deprecated/fire-event-sync.test.tsx rename to src/__tests__/unsafe-fire-event-sync.test.tsx index d87f09c7f..c1fae505b 100644 --- a/src/__tests__/deprecated/fire-event-sync.test.tsx +++ b/src/__tests__/unsafe-fire-event-sync.test.tsx @@ -9,7 +9,7 @@ import { View, } from 'react-native'; -import { deprecated_fireEventSync, render, screen } from '../..'; +import { screen, unsafe_fireEventSync, unsafe_renderSync } from '..'; type OnPressComponentProps = { onPress: () => void; @@ -36,6 +36,7 @@ type MyCustomButtonProps = { handlePress: () => void; text: string; }; + const MyCustomButton = ({ handlePress, text }: MyCustomButtonProps) => ( ); @@ -43,18 +44,19 @@ const MyCustomButton = ({ handlePress, text }: MyCustomButtonProps) => ( type CustomEventComponentWithCustomNameProps = { handlePress: () => void; }; + const CustomEventComponentWithCustomName = ({ handlePress, }: CustomEventComponentWithCustomNameProps) => ( ); -describe('deprecated_fireEventSync', () => { +describe('unsafe_fireEventSync', () => { test('should invoke specified event', () => { const onPressMock = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByText('Press me'), 'press'); + unsafe_fireEventSync(screen.getByText('Press me'), 'press'); expect(onPressMock).toHaveBeenCalled(); }); @@ -62,9 +64,9 @@ describe('deprecated_fireEventSync', () => { test('should invoke specified event on parent element', () => { const onPressMock = jest.fn(); const text = 'New press text'; - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByText(text), 'press'); + unsafe_fireEventSync(screen.getByText(text), 'press'); expect(onPressMock).toHaveBeenCalled(); }); @@ -72,19 +74,19 @@ describe('deprecated_fireEventSync', () => { const handlerMock = jest.fn(); const EVENT_DATA = 'event data'; - render( + unsafe_renderSync( , ); - deprecated_fireEventSync(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA); + unsafe_fireEventSync(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA); expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA); }); }); -test('deprecated_fireEventSync.press', () => { +test('unsafe_fireEventSync.press', () => { const onPressMock = jest.fn(); const text = 'Fireevent press'; const eventData = { @@ -93,14 +95,14 @@ test('deprecated_fireEventSync.press', () => { pageY: 30, }, }; - render(); + unsafe_renderSync(); - deprecated_fireEventSync.press(screen.getByText(text), eventData); + unsafe_fireEventSync.press(screen.getByText(text), eventData); expect(onPressMock).toHaveBeenCalledWith(eventData); }); -test('deprecated_fireEventSync.scroll', () => { +test('unsafe_fireEventSync.scroll', () => { const onScrollMock = jest.fn(); const eventData = { nativeEvent: { @@ -110,47 +112,47 @@ test('deprecated_fireEventSync.scroll', () => { }, }; - render( + unsafe_renderSync( XD , ); - deprecated_fireEventSync.scroll(screen.getByText('XD'), eventData); + unsafe_fireEventSync.scroll(screen.getByText('XD'), eventData); expect(onScrollMock).toHaveBeenCalledWith(eventData); }); -test('deprecated_fireEventSync.changeText', () => { +test('unsafe_fireEventSync.changeText', () => { const onChangeTextMock = jest.fn(); - render( + unsafe_renderSync( , ); const input = screen.getByPlaceholderText('Customer placeholder'); - deprecated_fireEventSync.changeText(input, 'content'); + unsafe_fireEventSync.changeText(input, 'content'); expect(onChangeTextMock).toHaveBeenCalledWith('content'); }); it('sets native state value for unmanaged text inputs', () => { - render(); + unsafe_renderSync(); const input = screen.getByTestId('input'); expect(input).toHaveDisplayValue(''); - deprecated_fireEventSync.changeText(input, 'abc'); + unsafe_fireEventSync.changeText(input, 'abc'); expect(input).toHaveDisplayValue('abc'); }); test('custom component with custom event name', () => { const handlePress = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByText('Custom component'), 'handlePress'); + unsafe_fireEventSync(screen.getByText('Custom component'), 'handlePress'); expect(handlePress).toHaveBeenCalled(); }); @@ -158,16 +160,16 @@ test('custom component with custom event name', () => { test('event with multiple handler parameters', () => { const handlePress = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2'); + unsafe_fireEventSync(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2'); expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); }); test('should not fire on disabled TouchableOpacity', () => { const handlePress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -175,13 +177,13 @@ test('should not fire on disabled TouchableOpacity', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync.press(screen.getByText('Trigger')); expect(handlePress).not.toHaveBeenCalled(); }); test('should not fire on disabled Pressable', () => { const handlePress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -189,13 +191,13 @@ test('should not fire on disabled Pressable', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync.press(screen.getByText('Trigger')); expect(handlePress).not.toHaveBeenCalled(); }); test('should not fire inside View with pointerEvents="none" in props', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -203,14 +205,14 @@ test('should not fire inside View with pointerEvents="none" in props', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should not fire inside View with pointerEvents="none" in styles', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -218,14 +220,14 @@ test('should not fire inside View with pointerEvents="none" in styles', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should not fire inside View with pointerEvents="none" in styles array', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -233,14 +235,14 @@ test('should not fire inside View with pointerEvents="none" in styles array', () , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should not fire inside View with pointerEvents="box-only" in props', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -248,14 +250,14 @@ test('should not fire inside View with pointerEvents="box-only" in props', () => , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should not fire inside View with pointerEvents="box-only" in styles', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -263,14 +265,14 @@ test('should not fire inside View with pointerEvents="box-only" in styles', () = , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should fire inside View with pointerEvents="box-none" in props', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -278,14 +280,14 @@ test('should fire inside View with pointerEvents="box-none" in props', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).toHaveBeenCalledTimes(2); }); test('should fire inside View with pointerEvents="box-none" in styles', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -293,14 +295,14 @@ test('should fire inside View with pointerEvents="box-none" in styles', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).toHaveBeenCalledTimes(2); }); test('should fire inside View with pointerEvents="auto" in props', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -308,14 +310,14 @@ test('should fire inside View with pointerEvents="auto" in props', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).toHaveBeenCalledTimes(2); }); test('should fire inside View with pointerEvents="auto" in styles', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( Trigger @@ -323,14 +325,14 @@ test('should fire inside View with pointerEvents="auto" in styles', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).toHaveBeenCalledTimes(2); }); test('should not fire deeply inside View with pointerEvents="box-only" in props', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( @@ -340,14 +342,14 @@ test('should not fire deeply inside View with pointerEvents="box-only" in props' , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should not fire deeply inside View with pointerEvents="box-only" in styles', () => { const onPress = jest.fn(); - render( + unsafe_renderSync( @@ -357,40 +359,44 @@ test('should not fire deeply inside View with pointerEvents="box-only" in styles , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); - deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress'); + unsafe_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync(screen.getByText('Trigger'), 'onPress'); expect(onPress).not.toHaveBeenCalled(); }); test('should fire non-pointer events inside View with pointerEvents="box-none" in props', () => { const onTouchStart = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByTestId('view'), 'touchStart'); + unsafe_fireEventSync(screen.getByTestId('view'), 'touchStart'); expect(onTouchStart).toHaveBeenCalled(); }); test('should fire non-pointer events inside View with pointerEvents="box-none" in styles', () => { const onTouchStart = jest.fn(); - render(); + unsafe_renderSync( + , + ); - deprecated_fireEventSync(screen.getByTestId('view'), 'touchStart'); + unsafe_fireEventSync(screen.getByTestId('view'), 'touchStart'); expect(onTouchStart).toHaveBeenCalled(); }); test('should fire non-touch events inside View with pointerEvents="box-none" in props', () => { const onLayout = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByTestId('view'), 'layout'); + unsafe_fireEventSync(screen.getByTestId('view'), 'layout'); expect(onLayout).toHaveBeenCalled(); }); test('should fire non-touch events inside View with pointerEvents="box-none" in styles', () => { const onLayout = jest.fn(); - render(); + unsafe_renderSync( + , + ); - deprecated_fireEventSync(screen.getByTestId('view'), 'layout'); + unsafe_fireEventSync(screen.getByTestId('view'), 'layout'); expect(onLayout).toHaveBeenCalled(); }); @@ -398,24 +404,26 @@ test('should fire non-touch events inside View with pointerEvents="box-none" in // the 'press' event on host View rendered by pressable. test('should fire on Pressable with pointerEvents="box-only" in props', () => { const onPress = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync.press(screen.getByTestId('pressable')); + unsafe_fireEventSync.press(screen.getByTestId('pressable')); expect(onPress).toHaveBeenCalled(); }); test('should fire on Pressable with pointerEvents="box-only" in styles', () => { const onPress = jest.fn(); - render(); + unsafe_renderSync( + , + ); - deprecated_fireEventSync.press(screen.getByTestId('pressable')); + unsafe_fireEventSync.press(screen.getByTestId('pressable')); expect(onPress).toHaveBeenCalled(); }); test('should pass event up on disabled TouchableOpacity', () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); - render( + unsafe_renderSync( Inner Trigger @@ -423,7 +431,7 @@ test('should pass event up on disabled TouchableOpacity', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Inner Trigger')); + unsafe_fireEventSync.press(screen.getByText('Inner Trigger')); expect(handleInnerPress).not.toHaveBeenCalled(); expect(handleOuterPress).toHaveBeenCalledTimes(1); }); @@ -431,7 +439,7 @@ test('should pass event up on disabled TouchableOpacity', () => { test('should pass event up on disabled Pressable', () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); - render( + unsafe_renderSync( Inner Trigger @@ -439,7 +447,7 @@ test('should pass event up on disabled Pressable', () => { , ); - deprecated_fireEventSync.press(screen.getByText('Inner Trigger')); + unsafe_fireEventSync.press(screen.getByText('Inner Trigger')); expect(handleInnerPress).not.toHaveBeenCalled(); expect(handleOuterPress).toHaveBeenCalledTimes(1); }); @@ -458,9 +466,9 @@ const TestComponent = ({ onPress }: TestComponentProps) => { test('is not fooled by non-native disabled prop', () => { const handlePress = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync.press(screen.getByText('Trigger Test')); + unsafe_fireEventSync.press(screen.getByText('Trigger Test')); expect(handlePress).toHaveBeenCalledTimes(1); }); @@ -482,13 +490,13 @@ function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableCo test('is not fooled by non-responder wrapping host elements', () => { const handlePress = jest.fn(); - render( + unsafe_renderSync( , ); - deprecated_fireEventSync.press(screen.getByText('Trigger')); + unsafe_fireEventSync.press(screen.getByText('Trigger')); expect(handlePress).not.toHaveBeenCalled(); }); @@ -510,62 +518,64 @@ function TestDraggableComponent({ onDrag }: TestDraggableComponentProps) { test('has only onMove', () => { const handleDrag = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByText('Trigger'), 'responderMove', { + unsafe_fireEventSync(screen.getByText('Trigger'), 'responderMove', { touchHistory: { mostRecentTimeStamp: '2', touchBank: [] }, }); expect(handleDrag).toHaveBeenCalled(); }); -// Those events ideally should be triggered through `deprecated_fireEventSync.scroll`, but they are handled at the +// Those events ideally should be triggered through `unsafe_fireEventSync.scroll`, but they are handled at the // native level, so we need to support manually triggering them describe('native events', () => { test('triggers onScrollBeginDrag', () => { const onScrollBeginDragSpy = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByTestId('test-id'), 'onScrollBeginDrag'); + unsafe_fireEventSync(screen.getByTestId('test-id'), 'onScrollBeginDrag'); expect(onScrollBeginDragSpy).toHaveBeenCalled(); }); test('triggers onScrollEndDrag', () => { const onScrollEndDragSpy = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByTestId('test-id'), 'onScrollEndDrag'); + unsafe_fireEventSync(screen.getByTestId('test-id'), 'onScrollEndDrag'); expect(onScrollEndDragSpy).toHaveBeenCalled(); }); test('triggers onMomentumScrollBegin', () => { const onMomentumScrollBeginSpy = jest.fn(); - render(); + unsafe_renderSync( + , + ); - deprecated_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollBegin'); + unsafe_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollBegin'); expect(onMomentumScrollBeginSpy).toHaveBeenCalled(); }); test('triggers onMomentumScrollEnd', () => { const onMomentumScrollEndSpy = jest.fn(); - render(); + unsafe_renderSync(); - deprecated_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollEnd'); + unsafe_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollEnd'); expect(onMomentumScrollEndSpy).toHaveBeenCalled(); }); }); test('should handle unmounted elements gracefully', () => { const onPress = jest.fn(); - render( + const { unmount } = unsafe_renderSync( Test , ); const element = screen.getByText('Test'); - screen.unmount(); + unmount(); // Firing event on unmounted element should not crash - deprecated_fireEventSync.press(element); + unsafe_fireEventSync.press(element); expect(onPress).not.toHaveBeenCalled(); }); diff --git a/src/__tests__/deprecated/render-hook-sync.test.tsx b/src/__tests__/unsafe-render-hook-sync.test.tsx similarity index 82% rename from src/__tests__/deprecated/render-hook-sync.test.tsx rename to src/__tests__/unsafe-render-hook-sync.test.tsx index 53a5e2ca8..fd95f491d 100644 --- a/src/__tests__/deprecated/render-hook-sync.test.tsx +++ b/src/__tests__/unsafe-render-hook-sync.test.tsx @@ -1,10 +1,10 @@ import type { ReactNode } from 'react'; import * as React from 'react'; -import { deprecated_renderHookSync } from '../../pure'; +import { unsafe_renderHookSync } from '../pure'; test('renders hook and returns committed result', () => { - const { result } = deprecated_renderHookSync(() => { + const { result } = unsafe_renderHookSync(() => { const [state, setState] = React.useState(1); React.useEffect(() => { @@ -22,7 +22,7 @@ test('works with wrapper option', () => { function Wrapper({ children }: { children: ReactNode }) { return {children}; } - const { result } = deprecated_renderHookSync( + const { result } = unsafe_renderHookSync( () => { return React.useContext(Context); }, @@ -35,7 +35,7 @@ test('works with wrapper option', () => { }); test('works with initialProps option', () => { - const { result } = deprecated_renderHookSync( + const { result } = unsafe_renderHookSync( (props: { branch: 'left' | 'right' }) => { const [left, setLeft] = React.useState('left'); const [right, setRight] = React.useState('right'); @@ -61,12 +61,12 @@ test('works without initialProps option', () => { return { count, setCount }; } - const { result } = deprecated_renderHookSync(useTestHook); + const { result } = unsafe_renderHookSync(useTestHook); expect(result.current.count).toBe(0); }); test('rerender updates hook with new props', () => { - const { result, rerender } = deprecated_renderHookSync( + const { result, rerender } = unsafe_renderHookSync( (props: { branch: 'left' | 'right' }) => { const [left, setLeft] = React.useState('left'); const [right, setRight] = React.useState('right'); @@ -102,7 +102,7 @@ test('unmount triggers cleanup effects', () => { return 'test'; } - const { unmount } = deprecated_renderHookSync(useTestHook); + const { unmount } = unsafe_renderHookSync(useTestHook); expect(cleanupCalled).toBe(false); unmount(); @@ -114,7 +114,7 @@ function useMyHook(param: T) { } test('props type is inferred correctly when initial props is defined', () => { - const { result, rerender } = deprecated_renderHookSync((num: number) => useMyHook(num), { + const { result, rerender } = unsafe_renderHookSync((num: number) => useMyHook(num), { initialProps: 5, }); expect(result.current.param).toBe(5); @@ -124,12 +124,9 @@ test('props type is inferred correctly when initial props is defined', () => { }); test('props type is inferred correctly when initial props is explicitly undefined', () => { - const { result, rerender } = deprecated_renderHookSync( - (num: number | undefined) => useMyHook(num), - { - initialProps: undefined, - }, - ); + const { result, rerender } = unsafe_renderHookSync((num: number | undefined) => useMyHook(num), { + initialProps: undefined, + }); expect(result.current.param).toBeUndefined(); diff --git a/src/__tests__/unsafe-render-sync.test.tsx b/src/__tests__/unsafe-render-sync.test.tsx new file mode 100644 index 000000000..38596dd94 --- /dev/null +++ b/src/__tests__/unsafe-render-sync.test.tsx @@ -0,0 +1,185 @@ +import * as React from 'react'; +import { Pressable, Text, TextInput, View } from 'react-native'; + +import { screen, unsafe_fireEventSync, unsafe_renderSync } from '..'; + +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('supports basic rendering', () => { + unsafe_renderSync(); + expect(screen.root).toBeOnTheScreen(); +}); + +test('rerender', () => { + const fn = jest.fn(); + const { rerender } = unsafe_renderSync(); + expect(fn).toHaveBeenCalledTimes(0); + + unsafe_fireEventSync.press(screen.getByText('Change freshness!')); + expect(fn).toHaveBeenCalledTimes(1); + + rerender(); + expect(fn).toHaveBeenCalledTimes(2); +}); + +test('unmount', () => { + const fn = jest.fn(); + const { unmount } = unsafe_renderSync(); + unmount(); + expect(fn).toHaveBeenCalled(); +}); + +test('unmount should handle cleanup functions', () => { + const cleanup = jest.fn(); + const Component = () => { + React.useEffect(() => cleanup); + return null; + }; + + const { unmount } = unsafe_renderSync(); + + unmount(); + + expect(cleanup).toHaveBeenCalledTimes(1); +}); + +test('toJSON renders host output', () => { + unsafe_renderSync(press me); + expect(screen).toMatchSnapshot(); +}); + +test('renders options.wrapper around node', () => { + type WrapperComponentProps = { children: React.ReactNode }; + const WrapperComponent = ({ children }: WrapperComponentProps) => ( + {children} + ); + + unsafe_renderSync(, { + wrapper: WrapperComponent, + }); + + expect(screen.getByTestId('wrapper')).toBeTruthy(); + expect(screen).toMatchInlineSnapshot(` + + + + `); +}); + +test('renders options.wrapper around updated node', () => { + type WrapperComponentProps = { children: React.ReactNode }; + const WrapperComponent = ({ children }: WrapperComponentProps) => ( + {children} + ); + + unsafe_renderSync(, { + wrapper: WrapperComponent, + }); + + void screen.rerender(); + + expect(screen.getByTestId('wrapper')).toBeTruthy(); + expect(screen).toMatchInlineSnapshot(` + + + + `); +}); + +test('returns host root', () => { + unsafe_renderSync(); + + expect(screen.root).toBeDefined(); + expect(screen.root?.type).toBe('View'); + expect(screen.root?.props.testID).toBe('inner'); +}); + +test('RenderAPI type', () => { + // This test verifies that unsafe_renderSync returns a compatible type + // Note: unsafe_renderSync has different method signatures (sync vs async) + const result = unsafe_renderSync(); + expect(result).toBeTruthy(); +}); + +test('returned output can be spread using rest operator', () => { + // Next line should not throw + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { rerender, ...rest } = unsafe_renderSync(); + expect(rest).toBeTruthy(); +}); diff --git a/src/__tests__/wait-for-element-to-be-removed.test.tsx b/src/__tests__/wait-for-element-to-be-removed.test.tsx index 0dda4ae87..b40ab679b 100644 --- a/src/__tests__/wait-for-element-to-be-removed.test.tsx +++ b/src/__tests__/wait-for-element-to-be-removed.test.tsx @@ -30,7 +30,7 @@ afterEach(() => { }); test('waits when using getBy query', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Remove Element')); const element = screen.getByText('Observed Element'); @@ -42,7 +42,7 @@ test('waits when using getBy query', async () => { }); test('waits when using getAllBy query', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Remove Element')); const elements = screen.getAllByText('Observed Element'); @@ -54,7 +54,7 @@ test('waits when using getAllBy query', async () => { }); test('waits when using queryBy query', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Remove Element')); const element = screen.getByText('Observed Element'); @@ -66,7 +66,7 @@ test('waits when using queryBy query', async () => { }); test('waits when using queryAllBy query', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Remove Element')); const elements = screen.getAllByText('Observed Element'); @@ -78,7 +78,7 @@ test('waits when using queryAllBy query', async () => { }); test('checks if elements exist at start', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Remove Element')); expect(screen.queryByText('Observed Element')).toBeNull(); @@ -91,7 +91,7 @@ test('checks if elements exist at start', async () => { }); test('waits until timeout', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Remove Element')); expect(screen.getByText('Observed Element')).toBeTruthy(); diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 4efd0253c..b9235b3e9 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -38,7 +38,7 @@ afterEach(() => { }); test('waits for element until it stops throwing', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Change freshness!')); @@ -50,7 +50,7 @@ test('waits for element until it stops throwing', async () => { }); test('waits for element until timeout is met', async () => { - render(); + await render(); await fireEvent.press(screen.getByText('Change freshness!')); @@ -63,7 +63,7 @@ test('waits for element until timeout is met', async () => { test('waitFor defaults to asyncWaitTimeout config option', async () => { configure({ asyncUtilTimeout: 100 }); - render(); + await render(); await fireEvent.press(screen.getByText('Change freshness!')); await expect(waitFor(() => screen.getByText('Fresh'))).rejects.toThrow(); @@ -75,7 +75,7 @@ test('waitFor defaults to asyncWaitTimeout config option', async () => { test('waitFor timeout option takes precendence over `asyncWaitTimeout` config option', async () => { configure({ asyncUtilTimeout: 2000 }); - render(); + await render(); await fireEvent.press(screen.getByText('Change freshness!')); await expect(waitFor(() => screen.getByText('Fresh'), { timeout: 100 })).rejects.toThrow(); @@ -125,7 +125,7 @@ const Comp = ({ onPress }: { onPress: () => void }) => { test('waits for async event with fireEvent', async () => { const spy = jest.fn(); - render(); + await render(); await fireEvent.press(screen.getByText('Trigger')); @@ -138,7 +138,7 @@ test.each([false, true])( 'waits for element until it stops throwing using fake timers (legacyFakeTimers = %s)', async (legacyFakeTimers) => { jest.useFakeTimers({ legacyFakeTimers }); - render(); + await render(); await fireEvent.press(screen.getByText('Change freshness!')); expect(screen.queryByText('Fresh')).toBeNull(); @@ -294,7 +294,7 @@ test.each([ } const onPress = jest.fn(); - render(); + await render(); // Required: this `waitFor` will succeed on first check, because the "root" view is there // since the initial mount. diff --git a/src/__tests__/within.test.tsx b/src/__tests__/within.test.tsx index 105aa081d..bd6889d99 100644 --- a/src/__tests__/within.test.tsx +++ b/src/__tests__/within.test.tsx @@ -4,7 +4,7 @@ import { Text, TextInput, View } from 'react-native'; import { getQueriesForElement, render, within } from '..'; test('within() exposes basic queries', async () => { - const rootQueries = render( + const rootQueries = await render( Same Text @@ -41,7 +41,7 @@ test('within() exposes basic queries', async () => { }); test('within() exposes a11y queries', async () => { - const rootQueries = render( + const rootQueries = await render( void; -type CleanUpFunctionAsync = () => Promise; +type CleanUpFunction = () => Promise | void; -const cleanupQueue = new Set(); +const cleanupQueue = new Set(); -export default function cleanup() { - clearRenderResult(); - - cleanupQueue.forEach((fn) => fn()); - cleanupQueue.clear(); -} - -export async function cleanupAsync() { +export async function cleanup() { clearRenderResult(); for (const fn of cleanupQueue) { @@ -22,6 +14,6 @@ export async function cleanupAsync() { cleanupQueue.clear(); } -export function addToCleanupQueue(fn: CleanUpFunction | CleanUpFunctionAsync) { +export function addToCleanupQueue(fn: CleanUpFunction) { cleanupQueue.add(fn); } diff --git a/src/fire-event.ts b/src/fire-event.ts index 17e1fb119..1fe38367c 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -157,7 +157,7 @@ fireEvent.scroll = async (element: HostElement, ...data: unknown[]) => await fireEvent(element, 'scroll', ...data); /** @deprecated - Use async `fireEvent` instead. */ -function deprecated_fireEventSync(element: HostElement, eventName: EventName, ...data: unknown[]) { +function unsafe_fireEventSync(element: HostElement, eventName: EventName, ...data: unknown[]) { if (!isElementMounted(element)) { return; } @@ -178,18 +178,18 @@ function deprecated_fireEventSync(element: HostElement, eventName: EventName, .. } /** @deprecated - Use async `fireEvent.press` instead. */ -deprecated_fireEventSync.press = (element: HostElement, ...data: unknown[]) => - deprecated_fireEventSync(element, 'press', ...data); +unsafe_fireEventSync.press = (element: HostElement, ...data: unknown[]) => + unsafe_fireEventSync(element, 'press', ...data); /** @deprecated - Use async `fireEvent.changeText` instead. */ -deprecated_fireEventSync.changeText = (element: HostElement, ...data: unknown[]) => - deprecated_fireEventSync(element, 'changeText', ...data); +unsafe_fireEventSync.changeText = (element: HostElement, ...data: unknown[]) => + unsafe_fireEventSync(element, 'changeText', ...data); /** @deprecated - Use async `fireEvent.scroll` instead. */ -deprecated_fireEventSync.scroll = (element: HostElement, ...data: unknown[]) => - deprecated_fireEventSync(element, 'scroll', ...data); +unsafe_fireEventSync.scroll = (element: HostElement, ...data: unknown[]) => + unsafe_fireEventSync(element, 'scroll', ...data); -export { fireEvent, deprecated_fireEventSync }; +export { fireEvent, unsafe_fireEventSync }; const scrollEventNames = new Set([ 'scroll', diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index dd277c596..64331884d 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -5,10 +5,10 @@ import { isHiddenFromAccessibility, isInaccessible, render, screen } from '../.. import { computeAriaDisabled, computeAriaLabel, isAccessibilityElement } from '../accessibility'; describe('isHiddenFromAccessibility', () => { - test('returns false for accessible elements', () => { + test('returns false for accessible elements', async () => { expect( isHiddenFromAccessibility( - render().getByTestId('subject', { + (await render()).getByTestId('subject', { includeHiddenElements: true, }), ), @@ -16,7 +16,7 @@ describe('isHiddenFromAccessibility', () => { expect( isHiddenFromAccessibility( - render(Hello).getByTestId('subject', { + (await render(Hello)).getByTestId('subject', { includeHiddenElements: true, }), ), @@ -24,7 +24,7 @@ describe('isHiddenFromAccessibility', () => { expect( isHiddenFromAccessibility( - render().getByTestId('subject', { + (await render()).getByTestId('subject', { includeHiddenElements: true, }), ), @@ -35,8 +35,8 @@ describe('isHiddenFromAccessibility', () => { expect(isHiddenFromAccessibility(null)).toBe(true); }); - test('detects elements with aria-hidden prop', () => { - render(); + test('detects elements with aria-hidden prop', async () => { + await render(); expect( isHiddenFromAccessibility( screen.getByTestId('subject', { @@ -46,8 +46,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects nested elements with aria-hidden prop', () => { - render( + test('detects nested elements with aria-hidden prop', async () => { + await render( , @@ -61,8 +61,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects elements with accessibilityElementsHidden prop', () => { - render(); + test('detects elements with accessibilityElementsHidden prop', async () => { + await render(); expect( isHiddenFromAccessibility( screen.getByTestId('subject', { @@ -72,8 +72,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects nested elements with accessibilityElementsHidden prop', () => { - render( + test('detects nested elements with accessibilityElementsHidden prop', async () => { + await render( , @@ -87,8 +87,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects deeply nested elements with accessibilityElementsHidden prop', () => { - render( + test('detects deeply nested elements with accessibilityElementsHidden prop', async () => { + await render( @@ -106,8 +106,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects elements with importantForAccessibility="no-hide-descendants" prop', () => { - render(); + test('detects elements with importantForAccessibility="no-hide-descendants" prop', async () => { + await render(); expect( isHiddenFromAccessibility( screen.getByTestId('subject', { @@ -117,8 +117,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects nested elements with importantForAccessibility="no-hide-descendants" prop', () => { - render( + test('detects nested elements with importantForAccessibility="no-hide-descendants" prop', async () => { + await render( , @@ -132,8 +132,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects elements with display=none', () => { - render(); + test('detects elements with display=none', async () => { + await render(); expect( isHiddenFromAccessibility( screen.getByTestId('subject', { @@ -143,8 +143,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects nested elements with display=none', () => { - render( + test('detects nested elements with display=none', async () => { + await render( , @@ -158,8 +158,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects deeply nested elements with display=none', () => { - render( + test('detects deeply nested elements with display=none', async () => { + await render( @@ -177,8 +177,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects elements with display=none with complex style', () => { - render( + test('detects elements with display=none with complex style', async () => { + await render( { ).toBe(true); }); - test('is not trigged by opacity = 0', () => { - render(); + test('is not trigged by opacity = 0', async () => { + await render(); expect( isHiddenFromAccessibility( screen.getByTestId('subject', { @@ -204,8 +204,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(false); }); - test('detects siblings of element with accessibilityViewIsModal prop', () => { - render( + test('detects siblings of element with accessibilityViewIsModal prop', async () => { + await render( @@ -220,8 +220,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects deeply nested siblings of element with accessibilityViewIsModal prop', () => { - render( + test('detects deeply nested siblings of element with accessibilityViewIsModal prop', async () => { + await render( @@ -236,8 +236,8 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('detects siblings of element with "aria-modal" prop', () => { - render( + test('detects siblings of element with "aria-modal" prop', async () => { + await render( @@ -248,13 +248,13 @@ describe('isHiddenFromAccessibility', () => { ).toBe(true); }); - test('is not triggered for element with accessibilityViewIsModal prop', () => { - render(); + test('is not triggered for element with accessibilityViewIsModal prop', async () => { + await render(); expect(isHiddenFromAccessibility(screen.getByTestId('subject'))).toBe(false); }); - test('is not triggered for child of element with accessibilityViewIsModal prop', () => { - render( + test('is not triggered for child of element with accessibilityViewIsModal prop', async () => { + await render( , @@ -262,8 +262,8 @@ describe('isHiddenFromAccessibility', () => { expect(isHiddenFromAccessibility(screen.getByTestId('subject'))).toBe(false); }); - test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => { - render( + test('is not triggered for descendent of element with accessibilityViewIsModal prop', async () => { + await render( @@ -279,15 +279,15 @@ describe('isHiddenFromAccessibility', () => { expect(isInaccessible).toBe(isHiddenFromAccessibility); }); - test('is not triggered for element with "aria-modal" prop', () => { - render(); + test('is not triggered for element with "aria-modal" prop', async () => { + await render(); expect(isHiddenFromAccessibility(screen.getByTestId('subject'))).toBe(false); }); }); describe('isAccessibilityElement', () => { - test('matches View component properly', () => { - render( + test('matches View component properly', async () => { + await render( @@ -299,8 +299,8 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy(); }); - test('matches TextInput component properly', () => { - render( + test('matches TextInput component properly', async () => { + await render( @@ -312,8 +312,8 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy(); }); - test('matches Text component properly', () => { - render( + test('matches Text component properly', async () => { + await render( Default @@ -329,8 +329,8 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy(); }); - test('matches Switch component properly', () => { - render( + test('matches Switch component properly', async () => { + await render( @@ -342,8 +342,8 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy(); }); - test('matches Pressable component properly', () => { - render( + test('matches Pressable component properly', async () => { + await render( @@ -355,8 +355,8 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy(); }); - test('matches TouchableOpacity component properly', () => { - render( + test('matches TouchableOpacity component properly', async () => { + await render( @@ -374,8 +374,8 @@ describe('isAccessibilityElement', () => { }); describe('computeAriaLabel', () => { - test('supports basic usage', () => { - render( + test('supports basic usage', async () => { + await render( @@ -395,8 +395,8 @@ describe('computeAriaLabel', () => { expect(computeAriaLabel(screen.getByTestId('text-content'))).toBeUndefined(); }); - test('label priority', () => { - render( + test('label priority', async () => { + await render( @@ -410,8 +410,8 @@ describe('computeAriaLabel', () => { }); describe('computeAriaDisabled', () => { - test('supports basic usage', () => { - render( + test('supports basic usage', async () => { + await render( @@ -428,8 +428,8 @@ describe('computeAriaDisabled', () => { expect(computeAriaDisabled(screen.getByTestId('disabled-false-by-state'))).toBe(false); }); - test('supports TextInput', () => { - render( + test('supports TextInput', async () => { + await render( @@ -442,8 +442,8 @@ describe('computeAriaDisabled', () => { expect(computeAriaDisabled(screen.getByTestId('editable-false'))).toBe(true); }); - test('supports Button', () => { - render( + test('supports Button', async () => { + await render( Default Button @@ -462,8 +462,8 @@ describe('computeAriaDisabled', () => { expect(computeAriaDisabled(screen.getByTestId('disabled-false'))).toBe(false); }); - test('supports Text', () => { - render( + test('supports Text', async () => { + await render( Default Text Disabled Text diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index 8ec9715b2..18b8d2580 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -15,8 +15,8 @@ function MultipleHostChildren() { } describe('getHostSiblings()', () => { - it('returns host siblings for host component', () => { - render( + it('returns host siblings for host component', async () => { + await render( @@ -39,8 +39,8 @@ describe('getHostSiblings()', () => { }); describe('getContainerElement()', () => { - it('returns container for mounted view', () => { - render( + it('returns container for mounted view', async () => { + await render( , diff --git a/src/helpers/__tests__/format-element.test.tsx b/src/helpers/__tests__/format-element.test.tsx index b27bde7a6..ffe8eaddc 100644 --- a/src/helpers/__tests__/format-element.test.tsx +++ b/src/helpers/__tests__/format-element.test.tsx @@ -4,8 +4,8 @@ import { Text, View } from 'react-native'; import { render, screen } from '../..'; import { formatElement } from '../format-element'; -test('formatElement', () => { - render( +test('formatElement', async () => { + await render( Hello diff --git a/src/helpers/__tests__/include-hidden-elements.test.tsx b/src/helpers/__tests__/include-hidden-elements.test.tsx index cffa8d234..03c9ddd3a 100644 --- a/src/helpers/__tests__/include-hidden-elements.test.tsx +++ b/src/helpers/__tests__/include-hidden-elements.test.tsx @@ -3,36 +3,36 @@ import { View } from 'react-native'; import { configure, render, screen } from '../..'; -test('includeHiddenElements query option takes priority over hidden option and global config', () => { +test('includeHiddenElements query option takes priority over hidden option and global config', async () => { configure({ defaultHidden: true, defaultIncludeHiddenElements: true }); - render(); + await render(); expect(screen.queryByTestId('view', { includeHiddenElements: false, hidden: true })).toBeFalsy(); }); -test('hidden option takes priority over global config when includeHiddenElements is not defined', () => { +test('hidden option takes priority over global config when includeHiddenElements is not defined', async () => { configure({ defaultHidden: true, defaultIncludeHiddenElements: true }); - render(); + await render(); expect(screen.queryByTestId('view', { hidden: false })).toBeFalsy(); }); -test('global config defaultIncludeElements option takes priority over defaultHidden when set at the same time', () => { +test('global config defaultIncludeElements option takes priority over defaultHidden when set at the same time', async () => { configure({ defaultHidden: false, defaultIncludeHiddenElements: true }); - render(); + await render(); expect(screen.getByTestId('view')).toBeTruthy(); }); -test('defaultHidden takes priority when it was set last', () => { +test('defaultHidden takes priority when it was set last', async () => { // also simulates the case when defaultIncludeHiddenElements is true by default in the config configure({ defaultIncludeHiddenElements: true }); configure({ defaultHidden: false }); - render(); + await render(); expect(screen.queryByTestId('view')).toBeFalsy(); }); -test('defaultIncludeHiddenElements takes priority when it was set last', () => { +test('defaultIncludeHiddenElements takes priority when it was set last', async () => { // also simulates the case when defaultHidden is true by default in the config configure({ defaultHidden: true }); configure({ defaultIncludeHiddenElements: false }); - render(); + await render(); expect(screen.queryByTestId('view')).toBeFalsy(); }); diff --git a/src/helpers/__tests__/text-content.test.tsx b/src/helpers/__tests__/text-content.test.tsx index 42f4d3f1e..0c773f699 100644 --- a/src/helpers/__tests__/text-content.test.tsx +++ b/src/helpers/__tests__/text-content.test.tsx @@ -4,8 +4,8 @@ import { Text } from 'react-native'; import { render, screen } from '../..'; import { getTextContent } from '../text-content'; -test('getTextContent with simple content', () => { - render(Hello world); +test('getTextContent with simple content', async () => { + await render(Hello world); expect(getTextContent(screen.root)).toBe('Hello world'); }); @@ -13,8 +13,8 @@ test('getTextContent with null element', () => { expect(getTextContent(null)).toBe(''); }); -test('getTextContent with single nested content', () => { - render( +test('getTextContent with single nested content', async () => { + await render( Hello world , @@ -22,8 +22,8 @@ test('getTextContent with single nested content', () => { expect(getTextContent(screen.root)).toBe('Hello world'); }); -test('getTextContent with multiple nested content', () => { - render( +test('getTextContent with multiple nested content', async () => { + await render( Hello world , @@ -31,8 +31,8 @@ test('getTextContent with multiple nested content', () => { expect(getTextContent(screen.root)).toBe('Hello world'); }); -test('getTextContent with multiple number content', () => { - render( +test('getTextContent with multiple number content', async () => { + await render( Hello world {100} , @@ -40,8 +40,8 @@ test('getTextContent with multiple number content', () => { expect(getTextContent(screen.root)).toBe('Hello world 100'); }); -test('getTextContent with multiple boolean content', () => { - render( +test('getTextContent with multiple boolean content', async () => { + await render( Hello{false} {true}world , diff --git a/src/helpers/__tests__/text-input.test.tsx b/src/helpers/__tests__/text-input.test.tsx index 60534dbb8..b3a6f72cf 100644 --- a/src/helpers/__tests__/text-input.test.tsx +++ b/src/helpers/__tests__/text-input.test.tsx @@ -4,8 +4,8 @@ import { TextInput, View } from 'react-native'; import { render, screen } from '../..'; import { getTextInputValue, isEditableTextInput } from '../text-input'; -test('getTextInputValue basic test', () => { - render( +test('getTextInputValue basic test', async () => { + await render( @@ -22,8 +22,8 @@ test('getTextInputValue basic test', () => { ); }); -test('isEditableTextInput basic test', () => { - render( +test('isEditableTextInput basic test', async () => { + await render( diff --git a/src/helpers/host-component-names.ts b/src/helpers/host-component-names.ts index 7eea2e9c5..39dc31925 100644 --- a/src/helpers/host-component-names.ts +++ b/src/helpers/host-component-names.ts @@ -1,6 +1,6 @@ import type { HostElement } from 'universal-test-renderer'; -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']; diff --git a/src/index.ts b/src/index.ts index 6a2e4ec4e..01a8f8ee9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import './matchers/extend-expect'; import { getIsReactActEnvironment, setReactActEnvironment } from './act'; import { flushMicroTasks } from './flush-micro-tasks'; -import { cleanupAsync } from './pure'; +import { cleanup } from './pure'; if (!process?.env?.RNTL_SKIP_AUTO_CLEANUP) { // If we're running in a test runner that supports afterEach @@ -13,7 +13,7 @@ if (!process?.env?.RNTL_SKIP_AUTO_CLEANUP) { if (typeof afterEach === 'function') { afterEach(async () => { await flushMicroTasks(); - await cleanupAsync(); + await cleanup(); }); } diff --git a/src/matchers/__tests__/to-be-busy.test.tsx b/src/matchers/__tests__/to-be-busy.test.tsx index e6684c242..cdd00bc7d 100644 --- a/src/matchers/__tests__/to-be-busy.test.tsx +++ b/src/matchers/__tests__/to-be-busy.test.tsx @@ -3,8 +3,8 @@ import { View } from 'react-native'; import { render, screen } from '../..'; -test('toBeBusy() basic case', () => { - render( +test('toBeBusy() basic case', async () => { + await render( <> @@ -21,8 +21,8 @@ test('toBeBusy() basic case', () => { expect(screen.getByTestId('default')).not.toBeBusy(); }); -test('toBeBusy() error messages', () => { - render( +test('toBeBusy() error messages', async () => { + await render( <> diff --git a/src/matchers/__tests__/to-be-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx index 2b0b3f22a..4020fa450 100644 --- a/src/matchers/__tests__/to-be-checked.test.tsx +++ b/src/matchers/__tests__/to-be-checked.test.tsx @@ -3,8 +3,8 @@ import { type AccessibilityRole, Switch, View } from 'react-native'; import { render, screen } from '../..'; -function renderViewsWithRole(role: AccessibilityRole) { - render( +async function renderViewsWithRole(role: AccessibilityRole) { + await render( <> { - render( +test('toBeCheck() with Switch', async () => { + await render( <> @@ -78,8 +78,8 @@ test('toBeCheck() with Switch', () => { `); }); -test('toBeCheck() with "checkbox" role', () => { - renderViewsWithRole('checkbox'); +test('toBeCheck() with "checkbox" role', async () => { + await renderViewsWithRole('checkbox'); const checked = screen.getByTestId('checkbox-checked'); const unchecked = screen.getByTestId('checkbox-unchecked'); @@ -148,8 +148,8 @@ test('toBeCheck() with "checkbox" role', () => { `); }); -test('toBeCheck() with "radio" role', () => { - renderViewsWithRole('radio'); +test('toBeCheck() with "radio" role', async () => { + await renderViewsWithRole('radio'); const checked = screen.getByTestId('radio-checked'); const unchecked = screen.getByTestId('radio-unchecked'); @@ -201,8 +201,8 @@ test('toBeCheck() with "radio" role', () => { `); }); -test('toBeCheck() with "switch" role', () => { - renderViewsWithRole('switch'); +test('toBeCheck() with "switch" role', async () => { + await renderViewsWithRole('switch'); const checked = screen.getByTestId('switch-checked'); const unchecked = screen.getByTestId('switch-unchecked'); @@ -254,8 +254,8 @@ test('toBeCheck() with "switch" role', () => { `); }); -test('throws error for invalid role', () => { - renderViewsWithRole('adjustable'); +test('throws error for invalid role', async () => { + await renderViewsWithRole('adjustable'); const checked = screen.getByTestId('adjustable-checked'); const unchecked = screen.getByTestId('adjustable-unchecked'); @@ -268,8 +268,8 @@ test('throws error for invalid role', () => { ); }); -test('throws error for non-accessibility element', () => { - render(); +test('throws error for non-accessibility element', async () => { + await render(); const view = screen.getByTestId('test'); expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot( diff --git a/src/matchers/__tests__/to-be-disabled.test.tsx b/src/matchers/__tests__/to-be-disabled.test.tsx index 9a3a6dedf..da7916665 100644 --- a/src/matchers/__tests__/to-be-disabled.test.tsx +++ b/src/matchers/__tests__/to-be-disabled.test.tsx @@ -13,8 +13,8 @@ import { import { render, screen } from '../..'; -test('toBeDisabled()/toBeEnabled() supports basic case', () => { - render( +test('toBeDisabled()/toBeEnabled() supports basic case', async () => { + await render( @@ -87,8 +87,8 @@ test('toBeDisabled()/toBeEnabled() supports basic case', () => { `); }); -test('toBeDisabled()/toBeEnabled() supports Pressable with "disabled" prop', () => { - render( +test('toBeDisabled()/toBeEnabled() supports Pressable with "disabled" prop', async () => { + await render( Button , @@ -157,28 +157,31 @@ test.each([ ['TouchableHighlight', TouchableHighlight], ['TouchableWithoutFeedback', TouchableWithoutFeedback], ['TouchableNativeFeedback', TouchableNativeFeedback], -] as const)('toBeDisabled()/toBeEnabled() supports %s with "disabled" prop', (_, Component) => { - render( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - JSX element type 'Component' does not have any construct or call signatures. - - Button - , - ); +] as const)( + 'toBeDisabled()/toBeEnabled() supports %s with "disabled" prop', + async (_, Component) => { + await render( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - JSX element type 'Component' does not have any construct or call signatures. + + Button + , + ); - const touchable = screen.getByTestId('subject'); - expect(touchable).toBeDisabled(); - expect(touchable).not.toBeEnabled(); + const touchable = screen.getByTestId('subject'); + expect(touchable).toBeDisabled(); + expect(touchable).not.toBeEnabled(); - const title = screen.getByText('Button'); - expect(title).toBeDisabled(); - expect(title).not.toBeEnabled(); + const title = screen.getByText('Button'); + expect(title).toBeDisabled(); + expect(title).not.toBeEnabled(); - expect(() => expect(touchable).toBeEnabled()).toThrow(); - expect(() => expect(touchable).not.toBeDisabled()).toThrow(); - expect(() => expect(title).toBeEnabled()).toThrow(); - expect(() => expect(title).not.toBeDisabled()).toThrow(); -}); + expect(() => expect(touchable).toBeEnabled()).toThrow(); + expect(() => expect(touchable).not.toBeDisabled()).toThrow(); + expect(() => expect(title).toBeEnabled()).toThrow(); + expect(() => expect(title).not.toBeDisabled()).toThrow(); + }, +); test.each([ ['View', View], @@ -190,8 +193,8 @@ test.each([ ['TouchableNativeFeedback', TouchableNativeFeedback], ] as const)( 'toBeDisabled()/toBeEnabled() supports %s with "aria-disabled" prop', - (_, Component) => { - render( + async (_, Component) => { + await render( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - JSX element type 'Component' does not have any construct or call signatures. @@ -218,8 +221,8 @@ test.each([ ['TouchableNativeFeedback', TouchableNativeFeedback], ] as const)( 'toBeDisabled()/toBeEnabled() supports %s with "accessibilityState.disabled" prop', - (_, Component) => { - render( + async (_, Component) => { + await render( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - JSX element type 'Component' does not have any construct or call signatures. @@ -235,8 +238,8 @@ test.each([ }, ); -test('toBeDisabled()/toBeEnabled() supports "editable" prop on TextInput', () => { - render( +test('toBeDisabled()/toBeEnabled() supports "editable" prop on TextInput', async () => { + await render( @@ -253,8 +256,8 @@ test('toBeDisabled()/toBeEnabled() supports "editable" prop on TextInput', () => expect(screen.getByTestId('disabled')).not.toBeEnabled(); }); -test('toBeDisabled()/toBeEnabled() supports "disabled" prop on Button', () => { - render( +test('toBeDisabled()/toBeEnabled() supports "disabled" prop on Button', async () => { + await render(