diff --git a/codemods/v14-async-functions/tests/fixtures/skip-variants/expected.tsx b/codemods/v14-async-functions/tests/fixtures/skip-variants/expected.tsx index d5f15efa2..96c73ea28 100644 --- a/codemods/v14-async-functions/tests/fixtures/skip-variants/expected.tsx +++ b/codemods/v14-async-functions/tests/fixtures/skip-variants/expected.tsx @@ -2,8 +2,6 @@ import { render, act, renderHook, - unsafe_act, - unsafe_renderHookSync, } from '@testing-library/react-native'; test('skips unsafe variants', async () => { @@ -13,13 +11,7 @@ test('skips unsafe variants', async () => { // Should be transformed }); - unsafe_act(() => { - // Should NOT be transformed - }); - const { result } = await renderHook(() => ({ value: 42 })); - unsafe_renderHookSync(() => ({ value: 43 })); - await render(); }); diff --git a/codemods/v14-async-functions/tests/fixtures/skip-variants/input.tsx b/codemods/v14-async-functions/tests/fixtures/skip-variants/input.tsx index d44c68977..897274e7b 100644 --- a/codemods/v14-async-functions/tests/fixtures/skip-variants/input.tsx +++ b/codemods/v14-async-functions/tests/fixtures/skip-variants/input.tsx @@ -2,8 +2,6 @@ import { render, act, renderHook, - unsafe_act, - unsafe_renderHookSync, renderAsync, } from '@testing-library/react-native'; @@ -14,13 +12,7 @@ test('skips unsafe variants', async () => { // Should be transformed }); - unsafe_act(() => { - // Should NOT be transformed - }); - const { result } = renderHook(() => ({ value: 42 })); - unsafe_renderHookSync(() => ({ value: 43 })); - await renderAsync(); }); diff --git a/src/__tests__/__snapshots__/unsafe-render-sync.test.tsx.snap b/src/__tests__/__snapshots__/unsafe-render-sync.test.tsx.snap deleted file mode 100644 index 5a844c697..000000000 --- a/src/__tests__/__snapshots__/unsafe-render-sync.test.tsx.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`toJSON renders host output 1`] = ` - - - press me - - -`; diff --git a/src/__tests__/cleanup.test.tsx b/src/__tests__/cleanup.test.tsx index f6036d028..13f57b91c 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 { cleanup, render, unsafe_renderSync } from '../pure'; +import { cleanup, render } from '../pure'; class Test extends React.Component<{ onUnmount: () => void }> { componentWillUnmount() { @@ -24,14 +24,3 @@ test('cleanup after render', async () => { await cleanup(); expect(fn).toHaveBeenCalledTimes(2); }); - -test('cleanup after unsafe_renderSync', async () => { - const fn = jest.fn(); - - unsafe_renderSync(); - unsafe_renderSync(); - expect(fn).not.toHaveBeenCalled(); - - await cleanup(); - expect(fn).toHaveBeenCalledTimes(2); -}); diff --git a/src/__tests__/unsafe-fire-event-sync.test.tsx b/src/__tests__/unsafe-fire-event-sync.test.tsx deleted file mode 100644 index 7a696ad2a..000000000 --- a/src/__tests__/unsafe-fire-event-sync.test.tsx +++ /dev/null @@ -1,581 +0,0 @@ -import * as React from 'react'; -import { - PanResponder, - Pressable, - ScrollView, - Text, - TextInput, - TouchableOpacity, - View, -} from 'react-native'; - -import { screen, unsafe_fireEventSync, unsafe_renderSync } from '..'; - -type OnPressComponentProps = { - onPress: () => void; - text: string; -}; -const OnPressComponent = ({ onPress, text }: OnPressComponentProps) => ( - - - {text} - - -); - -type CustomEventComponentProps = { - onCustomEvent: () => void; -}; -const CustomEventComponent = ({ onCustomEvent }: CustomEventComponentProps) => ( - - Custom event component - -); - -type MyCustomButtonProps = { - handlePress: () => void; - text: string; -}; - -const MyCustomButton = ({ handlePress, text }: MyCustomButtonProps) => ( - -); - -type CustomEventComponentWithCustomNameProps = { - handlePress: () => void; -}; - -const CustomEventComponentWithCustomName = ({ - handlePress, -}: CustomEventComponentWithCustomNameProps) => ( - -); - -describe('unsafe_fireEventSync', () => { - test('should invoke specified event', () => { - const onPressMock = jest.fn(); - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByText('Press me'), 'press'); - - expect(onPressMock).toHaveBeenCalled(); - }); - - test('should invoke specified event on parent element', () => { - const onPressMock = jest.fn(); - const text = 'New press text'; - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByText(text), 'press'); - expect(onPressMock).toHaveBeenCalled(); - }); - - test('should invoke event with custom name', () => { - const handlerMock = jest.fn(); - const EVENT_DATA = 'event data'; - - unsafe_renderSync( - - - , - ); - - unsafe_fireEventSync(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA); - - expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA); - }); -}); - -test('unsafe_fireEventSync.press', () => { - const onPressMock = jest.fn(); - const text = 'Fireevent press'; - const eventData = { - nativeEvent: { - pageX: 20, - pageY: 30, - }, - }; - unsafe_renderSync(); - - unsafe_fireEventSync.press(screen.getByText(text), eventData); - - expect(onPressMock).toHaveBeenCalledWith(eventData); -}); - -test('unsafe_fireEventSync.scroll', () => { - const onScrollMock = jest.fn(); - const eventData = { - nativeEvent: { - contentOffset: { - y: 200, - }, - }, - }; - - unsafe_renderSync( - - XD - , - ); - - unsafe_fireEventSync.scroll(screen.getByText('XD'), eventData); - - expect(onScrollMock).toHaveBeenCalledWith(eventData); -}); - -test('unsafe_fireEventSync.changeText', () => { - const onChangeTextMock = jest.fn(); - - unsafe_renderSync( - - - , - ); - - const input = screen.getByPlaceholderText('Customer placeholder'); - unsafe_fireEventSync.changeText(input, 'content'); - expect(onChangeTextMock).toHaveBeenCalledWith('content'); -}); - -it('sets native state value for unmanaged text inputs', () => { - unsafe_renderSync(); - - const input = screen.getByTestId('input'); - expect(input).toHaveDisplayValue(''); - - unsafe_fireEventSync.changeText(input, 'abc'); - expect(input).toHaveDisplayValue('abc'); -}); - -test('custom component with custom event name', () => { - const handlePress = jest.fn(); - - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByText('Custom component'), 'handlePress'); - - expect(handlePress).toHaveBeenCalled(); -}); - -test('event with multiple handler parameters', () => { - const handlePress = jest.fn(); - - unsafe_renderSync(); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - unsafe_fireEventSync.press(screen.getByText('Trigger')); - expect(handlePress).not.toHaveBeenCalled(); -}); - -test('should not fire on disabled Pressable', () => { - const handlePress = jest.fn(); - unsafe_renderSync( - - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - Trigger - - , - ); - - 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(); - unsafe_renderSync( - - - - Trigger - - - , - ); - - 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(); - unsafe_renderSync( - - - - Trigger - - - , - ); - - 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(); - unsafe_renderSync(); - - 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(); - unsafe_renderSync( - , - ); - - 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(); - unsafe_renderSync(); - - 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(); - unsafe_renderSync( - , - ); - - unsafe_fireEventSync(screen.getByTestId('view'), 'layout'); - expect(onLayout).toHaveBeenCalled(); -}); - -// This test if pointerEvents="box-only" on composite `Pressable` is blocking -// the 'press' event on host View rendered by pressable. -test('should fire on Pressable with pointerEvents="box-only" in props', () => { - const onPress = jest.fn(); - unsafe_renderSync(); - - unsafe_fireEventSync.press(screen.getByTestId('pressable')); - expect(onPress).toHaveBeenCalled(); -}); - -test('should fire on Pressable with pointerEvents="box-only" in styles', () => { - const onPress = jest.fn(); - unsafe_renderSync( - , - ); - - 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(); - unsafe_renderSync( - - - Inner Trigger - - , - ); - - unsafe_fireEventSync.press(screen.getByText('Inner Trigger')); - expect(handleInnerPress).not.toHaveBeenCalled(); - expect(handleOuterPress).toHaveBeenCalledTimes(1); -}); - -test('should pass event up on disabled Pressable', () => { - const handleInnerPress = jest.fn(); - const handleOuterPress = jest.fn(); - unsafe_renderSync( - - - Inner Trigger - - , - ); - - unsafe_fireEventSync.press(screen.getByText('Inner Trigger')); - expect(handleInnerPress).not.toHaveBeenCalled(); - expect(handleOuterPress).toHaveBeenCalledTimes(1); -}); - -type TestComponentProps = { - onPress: () => void; - disabled?: boolean; -}; -const TestComponent = ({ onPress }: TestComponentProps) => { - return ( - - Trigger Test - - ); -}; - -test('is not fooled by non-native disabled prop', () => { - const handlePress = jest.fn(); - unsafe_renderSync(); - - unsafe_fireEventSync.press(screen.getByText('Trigger Test')); - expect(handlePress).toHaveBeenCalledTimes(1); -}); - -type TestChildTouchableComponentProps = { - onPress: () => void; - someProp: boolean; -}; - -function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableComponentProps) { - return ( - - - Trigger - - - ); -} - -test('is not fooled by non-responder wrapping host elements', () => { - const handlePress = jest.fn(); - - unsafe_renderSync( - - - , - ); - - unsafe_fireEventSync.press(screen.getByText('Trigger')); - expect(handlePress).not.toHaveBeenCalled(); -}); - -type TestDraggableComponentProps = { onDrag: () => void }; - -function TestDraggableComponent({ onDrag }: TestDraggableComponentProps) { - const responderHandlers = PanResponder.create({ - onMoveShouldSetPanResponder: (_evt, _gestureState) => true, - onPanResponderMove: onDrag, - }).panHandlers; - - return ( - - Trigger - - ); -} - -test('has only onMove', () => { - const handleDrag = jest.fn(); - - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByText('Trigger'), 'responderMove', { - touchHistory: { mostRecentTimeStamp: '2', touchBank: [] }, - }); - expect(handleDrag).toHaveBeenCalled(); -}); - -// 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(); - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByTestId('test-id'), 'onScrollBeginDrag'); - expect(onScrollBeginDragSpy).toHaveBeenCalled(); - }); - - test('triggers onScrollEndDrag', () => { - const onScrollEndDragSpy = jest.fn(); - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByTestId('test-id'), 'onScrollEndDrag'); - expect(onScrollEndDragSpy).toHaveBeenCalled(); - }); - - test('triggers onMomentumScrollBegin', () => { - const onMomentumScrollBeginSpy = jest.fn(); - unsafe_renderSync( - , - ); - - unsafe_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollBegin'); - expect(onMomentumScrollBeginSpy).toHaveBeenCalled(); - }); - - test('triggers onMomentumScrollEnd', () => { - const onMomentumScrollEndSpy = jest.fn(); - unsafe_renderSync(); - - unsafe_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollEnd'); - expect(onMomentumScrollEndSpy).toHaveBeenCalled(); - }); -}); - -test('should handle unmounted elements gracefully', () => { - const onPress = jest.fn(); - const { rerender } = unsafe_renderSync( - - Test - , - ); - - const element = screen.getByText('Test'); - rerender(); - - // Firing event on unmounted element should not crash - unsafe_fireEventSync.press(element); - expect(onPress).not.toHaveBeenCalled(); -}); diff --git a/src/__tests__/unsafe-render-hook-sync.test.tsx b/src/__tests__/unsafe-render-hook-sync.test.tsx deleted file mode 100644 index fd95f491d..000000000 --- a/src/__tests__/unsafe-render-hook-sync.test.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { ReactNode } from 'react'; -import * as React from 'react'; - -import { unsafe_renderHookSync } from '../pure'; - -test('renders hook and returns committed result', () => { - const { result } = unsafe_renderHookSync(() => { - const [state, setState] = React.useState(1); - - React.useEffect(() => { - setState(2); - }, []); - - return [state, setState]; - }); - - expect(result.current).toEqual([2, expect.any(Function)]); -}); - -test('works with wrapper option', () => { - const Context = React.createContext('default'); - function Wrapper({ children }: { children: ReactNode }) { - return {children}; - } - const { result } = unsafe_renderHookSync( - () => { - return React.useContext(Context); - }, - { - wrapper: Wrapper, - }, - ); - - expect(result.current).toEqual('provided'); -}); - -test('works with initialProps option', () => { - const { result } = unsafe_renderHookSync( - (props: { branch: 'left' | 'right' }) => { - const [left, setLeft] = React.useState('left'); - const [right, setRight] = React.useState('right'); - - switch (props.branch) { - case 'left': - return [left, setLeft]; - case 'right': - return [right, setRight]; - default: - throw new Error('No Props passed. This is a bug in the implementation'); - } - }, - { initialProps: { branch: 'left' } }, - ); - - expect(result.current).toEqual(['left', expect.any(Function)]); -}); - -test('works without initialProps option', () => { - function useTestHook() { - const [count, setCount] = React.useState(0); - return { count, setCount }; - } - - const { result } = unsafe_renderHookSync(useTestHook); - expect(result.current.count).toBe(0); -}); - -test('rerender updates hook with new props', () => { - const { result, rerender } = unsafe_renderHookSync( - (props: { branch: 'left' | 'right' }) => { - const [left, setLeft] = React.useState('left'); - const [right, setRight] = React.useState('right'); - - switch (props.branch) { - case 'left': - return [left, setLeft]; - case 'right': - return [right, setRight]; - default: - throw new Error('No Props passed. This is a bug in the implementation'); - } - }, - { initialProps: { branch: 'left' } }, - ); - - expect(result.current).toEqual(['left', expect.any(Function)]); - - rerender({ branch: 'right' }); - expect(result.current).toEqual(['right', expect.any(Function)]); -}); - -test('unmount triggers cleanup effects', () => { - let cleanupCalled = false; - - function useTestHook() { - React.useEffect(() => { - return () => { - cleanupCalled = true; - }; - }, []); - - return 'test'; - } - - const { unmount } = unsafe_renderHookSync(useTestHook); - expect(cleanupCalled).toBe(false); - - unmount(); - expect(cleanupCalled).toBe(true); -}); - -function useMyHook(param: T) { - return { param }; -} - -test('props type is inferred correctly when initial props is defined', () => { - const { result, rerender } = unsafe_renderHookSync((num: number) => useMyHook(num), { - initialProps: 5, - }); - expect(result.current.param).toBe(5); - - rerender(6); - expect(result.current.param).toBe(6); -}); - -test('props type is inferred correctly when initial props is explicitly undefined', () => { - const { result, rerender } = unsafe_renderHookSync((num: number | undefined) => useMyHook(num), { - initialProps: undefined, - }); - - expect(result.current.param).toBeUndefined(); - - rerender(6); - expect(result.current.param).toBe(6); -}); diff --git a/src/__tests__/unsafe-render-sync.test.tsx b/src/__tests__/unsafe-render-sync.test.tsx deleted file mode 100644 index 38596dd94..000000000 --- a/src/__tests__/unsafe-render-sync.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -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/act.ts b/src/act.ts index 404e5c643..29c7a6e86 100644 --- a/src/act.ts +++ b/src/act.ts @@ -66,10 +66,10 @@ function withGlobalActEnvironment(actImplementation: ReactAct) { }; } -export const unsafe_act = withGlobalActEnvironment(reactAct) as ReactAct; +const _act = withGlobalActEnvironment(reactAct) as ReactAct; export function act(callback: () => T | Promise): Promise { - return unsafe_act(async () => await callback()); + return _act(async () => await callback()); } export { getIsReactActEnvironment, setIsReactActEnvironment as setReactActEnvironment }; diff --git a/src/fire-event.ts b/src/fire-event.ts index c069c7944..96276643d 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -7,7 +7,7 @@ import type { } from 'react-native'; import type { Fiber, HostElement } from 'test-renderer'; -import { act, unsafe_act } from './act'; +import { act } from './act'; import type { EventHandler } from './event-handler'; import { getEventHandlerFromProps } from './event-handler'; import { isElementMounted, isHostElement } from './helpers/component-tree'; @@ -155,40 +155,7 @@ fireEvent.changeText = async (element: HostElement, ...data: unknown[]) => fireEvent.scroll = async (element: HostElement, ...data: unknown[]) => await fireEvent(element, 'scroll', ...data); -/** @deprecated - Use async `fireEvent` instead. */ -function unsafe_fireEventSync(element: HostElement, eventName: EventName, ...data: unknown[]) { - if (!isElementMounted(element)) { - return; - } - - setNativeStateIfNeeded(element, eventName, data[0]); - - const handler = findEventHandler(element, eventName); - if (!handler) { - return; - } - - let returnValue; - void unsafe_act(() => { - returnValue = handler(...data); - }); - - return returnValue; -} - -/** @deprecated - Use async `fireEvent.press` instead. */ -unsafe_fireEventSync.press = (element: HostElement, ...data: unknown[]) => - unsafe_fireEventSync(element, 'press', ...data); - -/** @deprecated - Use async `fireEvent.changeText` instead. */ -unsafe_fireEventSync.changeText = (element: HostElement, ...data: unknown[]) => - unsafe_fireEventSync(element, 'changeText', ...data); - -/** @deprecated - Use async `fireEvent.scroll` instead. */ -unsafe_fireEventSync.scroll = (element: HostElement, ...data: unknown[]) => - unsafe_fireEventSync(element, 'scroll', ...data); - -export { fireEvent, unsafe_fireEventSync }; +export { fireEvent }; const scrollEventNames = new Set([ 'scroll', diff --git a/src/pure.ts b/src/pure.ts index 8ae817104..cdc62704c 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -1,8 +1,7 @@ -export { act, unsafe_act } from './act'; +export { act } from './act'; export { cleanup } from './cleanup'; -export { fireEvent, unsafe_fireEventSync } from './fire-event'; +export { fireEvent } from './fire-event'; export { render } from './render'; -export { unsafe_renderSync } from './unsafe-render-sync'; export { waitFor } from './wait-for'; export { waitForElementToBeRemoved } from './wait-for-element-to-be-removed'; export { within, getQueriesForElement } from './within'; @@ -10,12 +9,11 @@ export { within, getQueriesForElement } from './within'; export { configure, resetToDefaults } from './config'; export { isHiddenFromAccessibility, isInaccessible } from './helpers/accessibility'; export { getDefaultNormalizer } from './matches'; -export { renderHook, unsafe_renderHookSync } from './render-hook'; +export { renderHook } from './render-hook'; export { screen } from './screen'; export { userEvent } from './user-event'; export type { RenderOptions, RenderResult, DebugFunction } from './render'; -export type { RenderSyncOptions, RenderSyncResult } from './unsafe-render-sync'; -export type { RenderHookOptions, RenderHookResult, RenderHookSyncResult } from './render-hook'; +export type { RenderHookOptions, RenderHookResult } from './render-hook'; export type { Config } from './config'; export type { UserEventConfig } from './user-event'; diff --git a/src/render-hook.tsx b/src/render-hook.tsx index b04e61dae..564530c35 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { render } from './render'; import type { RefObject } from './types'; -import { unsafe_renderSync } from './unsafe-render-sync'; export type RenderHookResult = { result: RefObject; @@ -10,13 +9,6 @@ export type RenderHookResult = { unmount: () => Promise; }; -/** @deprecated - Use `renderHook` and `RenderHookResult` instead. */ -export type RenderHookSyncResult = { - result: RefObject; - rerender: (props: Props) => void; - unmount: () => void; -}; - export type RenderHookOptions = { /** * The initial props to pass to the hook. @@ -59,33 +51,3 @@ export async function renderHook( unmount, }; } - -/** @deprecated - Use async `renderHook` instead. */ -export function unsafe_renderHookSync( - hookToRender: (props: Props) => Result, - options?: RenderHookOptions>, -): RenderHookSyncResult { - const result = React.createRef() as RefObject; - - function HookContainer({ hookProps }: { hookProps: Props }) { - const renderResult = hookToRender(hookProps); - React.useEffect(() => { - result.current = renderResult; - }); - - return null; - } - - const { initialProps, ...renderOptions } = options ?? {}; - const { rerender: rerenderComponent, unmount } = unsafe_renderSync( - // @ts-expect-error since option can be undefined, initialProps can be undefined when it should'nt - , - renderOptions, - ); - - return { - result: result, - rerender: (hookProps: Props) => rerenderComponent(), - unmount, - }; -} diff --git a/src/unsafe-render-sync.tsx b/src/unsafe-render-sync.tsx deleted file mode 100644 index 7966cf2a9..000000000 --- a/src/unsafe-render-sync.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import * as React from 'react'; -import { - createRoot, - type HostElement, - type JsonElement, - type Root, - type RootOptions, -} from 'test-renderer'; - -import { act, unsafe_act } from './act'; -import { addToCleanupQueue } from './cleanup'; -import { getConfig } from './config'; -import type { DebugOptions } from './helpers/debug'; -import { debug } from './helpers/debug'; -import { HOST_TEXT_NAMES } from './helpers/host-component-names'; -import { setRenderResult } from './screen'; -import { getQueriesForElement } from './within'; - -export interface RenderSyncOptions { - /** - * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating - * reusable custom render functions for common data providers. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - wrapper?: React.ComponentType; - - createNodeMock?: (element: React.ReactElement) => object; -} - -/** - * @deprecated Use `render` (async) instead. This function is provided for migration purposes only. - * Renders test component deeply using React Test Renderer and exposes helpers - * to assert on the output. - */ -export function unsafe_renderSync( - component: React.ReactElement, - options: RenderSyncOptions = {}, -) { - return renderInternal(component, options); -} - -export type RenderSyncResult = ReturnType; - -export function renderInternal(component: React.ReactElement, options?: RenderSyncOptions) { - const { wrapper: Wrapper, createNodeMock } = options || {}; - - const rendererOptions: RootOptions = { - textComponentTypes: HOST_TEXT_NAMES, - publicTextComponentTypes: ['Text'], - createNodeMock, - }; - - const wrap = (element: React.ReactElement) => (Wrapper ? {element} : element); - - const renderer = createRoot(rendererOptions); - - unsafe_act(() => { - renderer.render(wrap(component)); - }); - - const rerender = (component: React.ReactElement) => { - unsafe_act(() => { - renderer.render(wrap(component)); - }); - }; - - const rerenderAsync = async (component: React.ReactElement) => { - await act(() => { - renderer.render(wrap(component)); - }); - }; - - const unmount = () => { - unsafe_act(() => { - renderer.unmount(); - }); - }; - - const unmountAsync = async () => { - await act(() => { - renderer.unmount(); - }); - }; - - const toJSON = (): JsonElement | null => { - const json = renderer.container.toJSON(); - if (json?.children?.length === 0) { - return null; - } - - if (json?.children?.length === 1 && typeof json.children[0] !== 'string') { - return json.children[0]; - } - - return json; - }; - - addToCleanupQueue(unmountAsync); - - const result = { - ...getQueriesForElement(renderer.container), - rerender, - update: rerender, // alias for 'rerender' - unmount, - toJSON, - debug: makeDebug(renderer), - get container(): HostElement { - return renderer.container; - }, - get root(): HostElement | null { - const firstChild = renderer.container.children[0]; - if (typeof firstChild === 'string') { - throw new Error( - 'Invariant Violation: Root element must be a host element. Detected attempt to render a string within the root element.', - ); - } - - return firstChild; - }, - }; - - setRenderResult({ - ...result, - rerender: rerenderAsync, - update: rerenderAsync, - unmount: unmountAsync, - }); - - return result; -} - -export type DebugFunction = (options?: DebugOptions) => void; - -function makeDebug(renderer: Root): DebugFunction { - function debugImpl(options?: DebugOptions) { - const { defaultDebugOptions } = getConfig(); - const debugOptions = { ...defaultDebugOptions, ...options }; - const json = renderer.container.toJSON(); - if (json) { - return debug(json, debugOptions); - } - } - return debugImpl; -} diff --git a/website/docs/14.x/docs/api/events/fire-event.mdx b/website/docs/14.x/docs/api/events/fire-event.mdx index 0feee2dda..287b99cd0 100644 --- a/website/docs/14.x/docs/api/events/fire-event.mdx +++ b/website/docs/14.x/docs/api/events/fire-event.mdx @@ -167,12 +167,3 @@ render( await fireEvent.scroll(screen.getByText('scroll-view'), eventData); ``` - -## `unsafe_fireEventSync` {#unsafe-fire-event-sync} - -```ts -function unsafe_fireEventSync(element: HostElement, eventName: string, ...data: unknown[]): void; -``` - -Synchronous version of `fireEvent`. Deprecated and will be removed in future versions. -Use only if you cannot use `await fireEvent` for some reason. diff --git a/website/docs/14.x/docs/api/misc/other.mdx b/website/docs/14.x/docs/api/misc/other.mdx index b6e1a1cb3..24c808049 100644 --- a/website/docs/14.x/docs/api/misc/other.mdx +++ b/website/docs/14.x/docs/api/misc/other.mdx @@ -52,28 +52,6 @@ it('should update state', async () => { Consult our [Understanding Act function](docs/advanced/understanding-act.md) document for more understanding of its intricacies. -## `unsafe_act` - -```ts -function unsafe_act(callback: () => T | Promise): T | Thenable; -``` - -**⚠️ Deprecated**: This function is provided for migration purposes only. Use async `act` instead. - -The synchronous version of `act` that maintains the same behavior as v13. It returns immediately for sync callbacks or a thenable for async callbacks. This is not recommended for new code as it doesn't work well with React 19's async features. - -```ts -// Deprecated - use act instead -import { unsafe_act } from '@testing-library/react-native'; - -it('should update state', () => { - unsafe_act(() => { - setState('new value'); - }); - expect(state).toBe('new value'); -}); -``` - ## `cleanup` ```ts diff --git a/website/docs/14.x/docs/api/misc/render-hook.mdx b/website/docs/14.x/docs/api/misc/render-hook.mdx index a590d3bfa..1afde0486 100644 --- a/website/docs/14.x/docs/api/misc/render-hook.mdx +++ b/website/docs/14.x/docs/api/misc/render-hook.mdx @@ -167,11 +167,3 @@ it('handles hook with suspense', async () => { expect(result.current).toBe('resolved'); }); ``` - -## Migration from v13 - -If you're migrating from React Native Testing Library v13, you may have existing tests using the synchronous `renderHook` API. To ease migration, we provide `unsafe_renderHookSync` which maintains the same synchronous behavior. - -**⚠️ Deprecated**: `unsafe_renderHookSync` is provided for migration purposes only. Use async `renderHook` instead. - -For detailed migration instructions, see the [v14 migration guide](../../migration/v14). diff --git a/website/docs/14.x/docs/api/render.mdx b/website/docs/14.x/docs/api/render.mdx index 7c1b7608f..5dd514c3e 100644 --- a/website/docs/14.x/docs/api/render.mdx +++ b/website/docs/14.x/docs/api/render.mdx @@ -66,43 +66,3 @@ See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testin When using `render`, the lifecycle methods `rerender`, `update`, and `unmount` are async and must be awaited. ::: - -## `unsafe_renderSync` function {#unsafe-render-sync} - -:::warning Deprecated - -This function is deprecated and provided for migration purposes only. Use async `render` instead. - -::: - -```tsx -function unsafe_renderSync( - component: React.Element, - options?: RenderSyncOptions -): RenderSyncResult; -``` - -The `unsafe_renderSync` function is the synchronous version of [`render`](#render). It was the default `render` function in previous versions but is now deprecated in favor of the async `render` function. - -```jsx -import { unsafe_renderSync, screen } from '@testing-library/react-native'; - -test('sync component test', () => { - unsafe_renderSync(); - expect(screen.getAllByRole('button', { name: 'start' })).toBeOnTheScreen(); -}); -``` - -### Options - -`unsafe_renderSync` accepts the same options as `render`. - -### Result - -The `unsafe_renderSync` function returns the same queries and utilities as the [`screen`](docs/api/screen) object. We recommended using the `screen` object as more developer-friendly way. - -:::note Migration - -To migrate from `unsafe_renderSync` to `render`, see the [migration guide](../migration/v14#render-async-default). - -::: diff --git a/website/docs/14.x/docs/guides/react-19.mdx b/website/docs/14.x/docs/guides/react-19.mdx index c04b96f0c..c899b6cda 100644 --- a/website/docs/14.x/docs/guides/react-19.mdx +++ b/website/docs/14.x/docs/guides/react-19.mdx @@ -36,12 +36,6 @@ The main change is using [`render`](docs/api/render#render) which is now async b 2. Adding `await` before `render` ```tsx -// Synchronous approach (deprecated in v14) -test('my component', () => { - unsafe_renderSync(); - expect(screen.getByText('Hello')).toBeOnTheScreen(); -}); - // Async approach (React 19 ready, default in v14) test('my component', async () => { await render(); @@ -67,10 +61,10 @@ Use the async-ready APIs (`render`, User Event, Jest Matchers, etc.) from the st You can migrate gradually: -- **Existing tests** can use `unsafe_renderSync` for backward compatibility +- **Existing tests** should be migrated to use async `render` (see migration guide) - **New tests** should use async `render` (default in v14) - **Tests with Suspense/`React.use()`** must use async `render` ### Migration -See the [v14 migration guide](../migration/v14#render-async-default) for detailed migration steps from `unsafe_renderSync` to `render`. +See the [v14 migration guide](../migration/v14#render-async-default) for detailed migration steps. diff --git a/website/docs/14.x/docs/migration/v14.mdx b/website/docs/14.x/docs/migration/v14.mdx index 8f05d0cc4..71aef82c6 100644 --- a/website/docs/14.x/docs/migration/v14.mdx +++ b/website/docs/14.x/docs/migration/v14.mdx @@ -105,7 +105,7 @@ For more details, see the [`screen` API documentation](/docs/api/screen#containe ### `render` is now async by default {#render-async-default} -In v14, `render` is now async by default and returns a Promise. The previous synchronous `render` function has been renamed to `unsafe_renderSync`. This change makes it compatible with React 19, React Suspense, and `React.use()`. +In v14, `render` is now async by default and returns a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`. **Before (v13):** @@ -129,44 +129,17 @@ it('should render component', async () => { }); ``` -#### Migration path - -To ease migration, we provide `unsafe_renderSync` which maintains the same synchronous behavior as v13. This allows you to migrate gradually. - -##### `unsafe_renderSync` {#unsafe-render-sync} - -```ts -function unsafe_renderSync( - component: React.Element, - options?: RenderSyncOptions -): RenderSyncResult; -``` - -**⚠️ Deprecated**: This function is provided for migration purposes only. Use async `render` instead. - -The synchronous version of `render` that returns immediately without awaiting React updates. This maintains backward compatibility with v13 tests but is not recommended for new code. - -```ts -// Old v13 code (still works but deprecated) -import { unsafe_renderSync } from '@testing-library/react-native'; - -it('should render component', () => { - unsafe_renderSync(); - expect(screen.getByText('Hello')).toBeOnTheScreen(); -}); -``` - #### Step-by-step migration guide -To migrate from `unsafe_renderSync` to `render`: +To migrate from v13 `render` to v14 `render`: ##### 1. Add `async` to your test function ```ts -// Before +// Before (v13) it('should render component', () => { -// After +// After (v14) it('should render component', async () => { ``` @@ -273,67 +246,9 @@ it('should test hook', async () => { }); ``` -#### Migration path - -To ease migration, we provide `unsafe_renderHookSync` which maintains the same synchronous behavior as v13. This allows you to migrate gradually. - -##### `unsafe_renderHookSync` {#unsafe-render-hook-sync} - -```ts -function unsafe_renderHookSync( - hookFn: (props?: Props) => Result, - options?: RenderHookOptions -): RenderHookSyncResult; -``` - -**⚠️ Deprecated**: This function is provided for migration purposes only. Use async `renderHook` instead. - -The synchronous version of `renderHook` that returns immediately without awaiting React updates. This maintains backward compatibility with v13 tests but is not recommended for new code. - -```ts -// Old v13 code (still works but deprecated) -import { unsafe_renderHookSync } from '@testing-library/react-native'; - -it('should increment count', () => { - const { result, rerender } = unsafe_renderHookSync(() => useCount()); - - expect(result.current.count).toBe(0); - act(() => { - result.current.increment(); - }); - expect(result.current.count).toBe(1); - - rerender(); -}); -``` - -##### Result types - -**v13 (deprecated):** - -```ts -interface RenderHookSyncResult { - result: { current: Result }; - rerender: (props: Props) => void; - unmount: () => void; -} -``` - -**v14 (default):** - -```ts -interface RenderHookResult { - result: { current: Result }; - rerender: (props: Props) => Promise; - unmount: () => Promise; -} -``` - -**Note**: Both `RenderHookSyncResult` and `RenderHookResult` use `rerender` and `unmount` methods. The difference is that `RenderHookSyncResult` methods are synchronous (return `void`), while `RenderHookResult` methods are async (return `Promise`). - #### Step-by-step migration guide -To migrate from `unsafe_renderHookSync` to `renderHook`: +To migrate from v13 `renderHook` to v14 `renderHook`: ##### 1. Add `async` to your test function @@ -348,10 +263,10 @@ it('should test hook', async () => { ##### 2. Await `renderHook` ```ts -// Before -const { result } = unsafe_renderHookSync(() => useMyHook()); +// Before (v13) +const { result } = renderHook(() => useMyHook()); -// After +// After (v14) const { result } = await renderHook(() => useMyHook()); ``` @@ -472,31 +387,6 @@ it('should press button', async () => { }); ``` -#### Migration path - -To ease migration, we provide `unsafe_fireEventSync` which maintains the same synchronous behavior as v13. This allows you to migrate gradually. - -##### `unsafe_fireEventSync` {#unsafe-fire-event-sync} - -```ts -function unsafe_fireEventSync(element: HostElement, eventName: string, ...data: unknown[]): void; -``` - -**⚠️ Deprecated**: This function is provided for migration purposes only. Use async `fireEvent` instead. - -The synchronous version of `fireEvent` that returns immediately without awaiting React updates. This maintains backward compatibility with v13 tests but is not recommended for new code. - -```ts -// Old v13 code (still works but deprecated) -import { unsafe_fireEventSync } from '@testing-library/react-native'; - -it('should press button', () => { - render(); - unsafe_fireEventSync.press(screen.getByText('Press me')); - expect(onPress).toHaveBeenCalled(); -}); -``` - ### Text string validation now enforced by default In v14, Test Renderer automatically enforces React Native's requirement that text strings must be rendered within a `` component. This means the `unstable_validateStringsRenderedWithinText` option has been removed from `RenderOptions`, as this validation is now always enabled. @@ -526,13 +416,12 @@ If you were relying on the previous behavior where strings could be rendered out ### `act` is now async by default -In v14, `act` is now async by default and always returns a Promise. The previous synchronous `act` function has been renamed to `unsafe_act`. This change makes it compatible with React 19, React Suspense, and `React.use()`. +In v14, `act` is now async by default and always returns a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`. **What changed:** - `act` now always returns a `Promise` instead of `T | Thenable` -- `unsafe_act` is available for the old behavior (direct React.act wrapper) -- Both `act` and `unsafe_act` are now named exports (no default export) +- `act` is now a named export (no default export) **Before (v13):** @@ -564,35 +453,9 @@ it('should update state', async () => { }); ``` -#### Migration path - -To ease migration, we provide `unsafe_act` which maintains the same behavior as v13. This allows you to migrate gradually. - -##### `unsafe_act` {#unsafe-act} - -```ts -function unsafe_act(callback: () => T | Promise): T | Thenable; -``` - -**⚠️ Deprecated**: This function is provided for migration purposes only. Use async `act` instead. - -The synchronous version of `act` that returns immediately for sync callbacks or a thenable for async callbacks. This maintains backward compatibility with v13 tests but is not recommended for new code. - -```ts -// Old v13 code (still works but deprecated) -import { unsafe_act } from '@testing-library/react-native'; - -it('should update state', () => { - unsafe_act(() => { - setState('new value'); - }); - expect(state).toBe('new value'); -}); -``` - #### Step-by-step migration guide -To migrate from `unsafe_act` to `act`: +To migrate from v13 `act` to v14 `act`: ##### 1. Update import statement