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