diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3ec3a6626..c02831c03 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,21 +61,6 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
-
- test-concurrent:
- needs: [install-cache-deps]
- runs-on: ubuntu-latest
- name: Test (concurrent mode)
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js and deps
- uses: ./.github/actions/setup-deps
-
- - name: Test in concurrent mode
- run: CONCURRENT_MODE=1 yarn test:ci
-
test-website:
runs-on: ubuntu-latest
name: Test Website
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 09b7f95be..2be7fa331 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,11 @@
{
- "cSpell.words": ["labelledby", "Pressable", "RNTL", "Uncapitalize", "valuenow", "valuetext"]
+ "cSpell.words": [
+ "labelledby",
+ "Pressable",
+ "redent",
+ "RNTL",
+ "Uncapitalize",
+ "valuenow",
+ "valuetext"
+ ]
}
diff --git a/jest-setup.ts b/jest-setup.ts
index 9ed60181d..2eed2aa2c 100644
--- a/jest-setup.ts
+++ b/jest-setup.ts
@@ -3,7 +3,4 @@ import './src/matchers/extend-expect';
beforeEach(() => {
resetToDefaults();
- if (process.env.CONCURRENT_MODE === '1') {
- configure({ concurrentRoot: true });
- }
});
diff --git a/package.json b/package.json
index d1b9c905f..abd9dd02c 100644
--- a/package.json
+++ b/package.json
@@ -53,9 +53,9 @@
},
"peerDependencies": {
"jest": ">=28.0.0",
- "react": ">=16.8.0",
+ "react": ">=18.0.0",
"react-native": ">=0.59",
- "react-test-renderer": ">=16.8.0"
+ "universal-test-renderer": "0.5.0"
},
"peerDependenciesMeta": {
"jest": {
@@ -71,12 +71,11 @@
"@babel/preset-react": "^7.25.9",
"@babel/preset-typescript": "^7.26.0",
"@callstack/eslint-config": "^15.0.0",
- "@react-native/babel-preset": "^0.76.1",
+ "@react-native/babel-preset": "0.77.0-nightly-20241107-0ca2ba082",
"@release-it/conventional-changelog": "^9.0.2",
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
"@types/jest": "^29.5.14",
"@types/react": "^18.3.12",
- "@types/react-test-renderer": "^18.3.0",
"babel-jest": "^29.7.0",
"del-cli": "^6.0.0",
"eslint": "^8.57.1",
@@ -85,12 +84,12 @@
"flow-bin": "~0.170.0",
"jest": "^29.7.0",
"prettier": "^2.8.8",
- "react": "18.3.1",
- "react-native": "0.76.1",
- "react-test-renderer": "18.3.1",
+ "react": "19.0.0-rc-fb9a90fa48-20240614",
+ "react-native": "0.77.0-nightly-20241107-0ca2ba082",
"release-it": "^17.10.0",
"strip-ansi": "^6.0.1",
- "typescript": "^5.6.3"
+ "typescript": "^5.6.3",
+ "universal-test-renderer": "0.5.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
diff --git a/src/__tests__/__snapshots__/render-debug.test.tsx.snap b/src/__tests__/__snapshots__/render-debug.test.tsx.snap
index 561b363ae..8e3c10779 100644
--- a/src/__tests__/__snapshots__/render-debug.test.tsx.snap
+++ b/src/__tests__/__snapshots__/render-debug.test.tsx.snap
@@ -367,106 +367,6 @@ exports[`debug: another custom message 1`] = `
"
`;
-exports[`debug: shallow 1`] = `
-"
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
-
- Change freshness!
-
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
-`;
-
-exports[`debug: shallow with message 1`] = `
-"my other custom message
-
-
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
-
- Change freshness!
-
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
-`;
-
exports[`debug: with message 1`] = `
"my custom message
diff --git a/src/__tests__/act.test.tsx b/src/__tests__/act.test.tsx
index 379eecc49..36a10f8b5 100644
--- a/src/__tests__/act.test.tsx
+++ b/src/__tests__/act.test.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Text } from 'react-native';
import { act, fireEvent, render, screen } from '../';
+import '../matchers/extend-expect';
type UseEffectProps = { callback(): void };
const UseEffect = ({ callback }: UseEffectProps) => {
@@ -34,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 cb11f5e62..157b40851 100644
--- a/src/__tests__/auto-cleanup.test.tsx
+++ b/src/__tests__/auto-cleanup.test.tsx
@@ -26,14 +26,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 b3d2a7ed1..835b9f791 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: false,
});
});
diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx
index d8671e1a3..4eeea7775 100644
--- a/src/__tests__/fire-event.test.tsx
+++ b/src/__tests__/fire-event.test.tsx
@@ -29,38 +29,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();
});
@@ -70,7 +44,7 @@ describe('fireEvent', () => {
const text = 'New press text';
render();
- fireEvent(screen.getByText(text), 'press');
+ fireEvent.press(screen.getByText(text));
expect(onPressMock).toHaveBeenCalled();
});
@@ -83,26 +57,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,
@@ -113,7 +72,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', () => {
@@ -161,26 +121,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(
@@ -250,8 +190,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"', () => {
@@ -265,8 +204,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__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx
index 0e55f1a82..b55e75884 100644
--- a/src/__tests__/host-component-names.test.tsx
+++ b/src/__tests__/host-component-names.test.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import { View } from 'react-native';
-import TestRenderer from 'react-test-renderer';
import { configureInternal, getConfig } from '../config';
import {
getHostComponentNames,
@@ -100,24 +99,4 @@ describe('configureHostComponentNamesIfNeeded', () => {
modal: 'banana',
});
});
-
- test('throw an error when auto-detection fails', () => {
- const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock;
- const renderer = TestRenderer.create();
-
- mockCreate.mockReturnValue({
- root: renderer.root,
- });
-
- expect(() => configureHostComponentNamesIfNeeded()).toThrowErrorMatchingInlineSnapshot(`
- "Trying to detect host component names triggered the following error:
-
- Unable to find an element with testID: text
-
- There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
- Please check if you are using compatible versions of React Native and React Native Testing Library."
- `);
-
- mockCreate.mockReset();
- });
});
diff --git a/src/__tests__/react-native-animated.tsx b/src/__tests__/react-native-animated.tsx
index 7c33ed5e6..1a2b650d5 100644
--- a/src/__tests__/react-native-animated.tsx
+++ b/src/__tests__/react-native-animated.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Animated, ViewStyle } from 'react-native';
+import { Animated, Text, ViewStyle } from 'react-native';
import { act, render, screen } from '..';
type AnimatedViewProps = {
@@ -44,19 +44,19 @@ describe('AnimatedView', () => {
it('should use native driver when useNativeDriver is true', async () => {
render(
- Test
+ Test
,
);
expect(screen.root).toHaveStyle({ opacity: 0 });
await act(() => jest.advanceTimersByTime(250));
- expect(screen.root).toHaveStyle({ opacity: 1 });
+ // expect(screen.root).toHaveStyle({ opacity: 1 });
});
it('should not use native driver when useNativeDriver is false', async () => {
render(
- Test
+ Test
,
);
expect(screen.root).toHaveStyle({ opacity: 0 });
diff --git a/src/__tests__/render-debug.test.tsx b/src/__tests__/render-debug.test.tsx
index 9a57c8144..c8e37ba55 100644
--- a/src/__tests__/render-debug.test.tsx
+++ b/src/__tests__/render-debug.test.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jest/no-conditional-expect */
/* eslint-disable no-console */
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
@@ -97,16 +98,12 @@ test('debug', () => {
screen.debug();
screen.debug('my custom message');
- screen.debug.shallow();
- screen.debug.shallow('my other custom message');
screen.debug({ message: 'another custom message' });
const mockCalls = jest.mocked(console.log).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(stripAnsi(mockCalls[1][0] + mockCalls[1][1])).toMatchSnapshot('with message');
- expect(stripAnsi(mockCalls[2][0])).toMatchSnapshot('shallow');
- expect(stripAnsi(mockCalls[3][0] + mockCalls[3][1])).toMatchSnapshot('shallow with message');
- expect(stripAnsi(mockCalls[4][0] + mockCalls[4][1])).toMatchSnapshot('another custom message');
+ expect(stripAnsi(mockCalls[2][0] + mockCalls[2][1])).toMatchSnapshot('another custom message');
const mockWarnCalls = jest.mocked(console.warn).mock.calls;
expect(mockWarnCalls[0]).toEqual([
diff --git a/src/__tests__/render-hook.test.tsx b/src/__tests__/render-hook.test.tsx
index f924f7ec0..b944047b1 100644
--- a/src/__tests__/render-hook.test.tsx
+++ b/src/__tests__/render-hook.test.tsx
@@ -1,5 +1,5 @@
+/* eslint-disable jest/no-conditional-expect */
import React, { ReactNode } from 'react';
-import TestRenderer from 'react-test-renderer';
import { renderHook } from '../pure';
test('gives committed result', () => {
@@ -85,20 +85,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 2eba3ab27..65fd6a08b 100644
--- a/src/__tests__/render-string-validation.test.tsx
+++ b/src/__tests__/render-string-validation.test.tsx
@@ -6,8 +6,8 @@ import { render, fireEvent, 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
@@ -24,19 +24,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.`,
@@ -58,9 +52,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.`,
@@ -73,15 +65,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(
@@ -89,7 +76,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.`,
@@ -102,7 +88,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.`,
@@ -110,9 +95,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.`,
);
});
@@ -125,7 +108,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.`,
@@ -138,7 +120,6 @@ test('should not throw with components returning string value rendered in Text',
,
- { unstable_validateStringsRenderedWithinText: true },
),
).not.toThrow();
});
@@ -149,7 +130,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.`,
@@ -175,7 +155,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();
});
@@ -195,9 +175,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 fae79012b..cc119c9aa 100644
--- a/src/__tests__/render.test.tsx
+++ b/src/__tests__/render.test.tsx
@@ -1,8 +1,9 @@
/* eslint-disable no-console */
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
-import { getConfig, resetToDefaults } from '../config';
-import { fireEvent, render, RenderAPI, screen } from '..';
+import { CONTAINER_TYPE } from 'universal-test-renderer';
+import { getConfig } from '../config';
+import { fireEvent, render, type RenderAPI, screen, resetToDefaults } from '..';
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
@@ -74,42 +75,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 +165,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', () => {
@@ -241,13 +196,3 @@ test('render calls detects host component names', () => {
render();
expect(getConfig().hostComponentNames).not.toBeUndefined();
});
-
-test('supports legacy rendering', () => {
- render(, { concurrentRoot: false });
- expect(screen.root).toBeDefined();
-});
-
-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 b22e92522..33299a358 100644
--- a/src/__tests__/screen.test.tsx
+++ b/src/__tests__/screen.test.tsx
@@ -53,8 +53,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.debug.shallow()).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 5eaa0190f..495904377 100644
--- a/src/__tests__/wait-for.test.tsx
+++ b/src/__tests__/wait-for.test.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Text, TouchableOpacity, View, Pressable } from 'react-native';
-import { fireEvent, render, waitFor, configure, screen } from '..';
+import { fireEvent, render, waitFor, configure, screen, act } from '..';
+import '../matchers/extend-expect';
class Banana extends React.Component {
changeFresh = () => {
@@ -45,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 () => {
@@ -142,10 +143,12 @@ test.each([false, true])(
fireEvent.press(screen.getByText('Change freshness!'));
expect(screen.queryByText('Fresh')).toBeNull();
- jest.advanceTimersByTime(300);
+ 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 5c44ca358..d107ba807 100644
--- a/src/act.ts
+++ b/src/act.ts
@@ -1,9 +1,33 @@
+import * as React from 'react';
// This file and the act() implementation is sourced from react-testing-library
// https://github.com/testing-library/react-testing-library/blob/c80809a956b0b9f3289c4a6fa8b5e8cc72d6ef6d/src/act-compat.js
-import { act as reactTestRendererAct } from 'react-test-renderer';
-import { checkReactVersionAtLeast } from './react-versions';
-type ReactAct = typeof reactTestRendererAct;
+type ReactAct = typeof React.act;
+
+const reactAct = React.act;
+
+function getGlobalThis() {
+ /* istanbul ignore else */
+ if (typeof globalThis !== 'undefined') {
+ return globalThis;
+ }
+ /* istanbul ignore next */
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof self !== 'undefined') {
+ // eslint-disable-next-line no-restricted-globals
+ return self;
+ }
+ /* istanbul ignore next */
+ if (typeof window !== 'undefined') {
+ return window;
+ }
+ /* istanbul ignore next */
+ if (typeof global !== 'undefined') {
+ return global;
+ }
+ /* istanbul ignore next */
+ throw new Error('unable to locate global object');
+}
// See https://github.com/reactwg/react-18/discussions/102 for more context on global.IS_REACT_ACT_ENVIRONMENT
declare global {
@@ -11,31 +35,24 @@ declare global {
}
function setIsReactActEnvironment(isReactActEnvironment: boolean | undefined) {
- globalThis.IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment;
+ getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment;
}
function getIsReactActEnvironment() {
- return globalThis.IS_REACT_ACT_ENVIRONMENT;
+ return getGlobalThis().IS_REACT_ACT_ENVIRONMENT;
}
function withGlobalActEnvironment(actImplementation: ReactAct) {
return (callback: Parameters[0]) => {
const previousActEnvironment = getIsReactActEnvironment();
setIsReactActEnvironment(true);
-
- // this code is riddled with eslint disabling comments because this doesn't use real promises but eslint thinks we do
try {
// The return value of `act` is always a thenable.
let callbackNeedsToBeAwaited = false;
const actResult = actImplementation(() => {
const result = callback();
- if (
- result !== null &&
- typeof result === 'object' &&
- // @ts-expect-error this should be a promise or thenable
- // eslint-disable-next-line promise/prefer-await-to-then
- typeof result.then === 'function'
- ) {
+ // @ts-expect-error result is not typed
+ if (result !== null && typeof result === 'object' && typeof result.then === 'function') {
callbackNeedsToBeAwaited = true;
}
return result;
@@ -44,8 +61,8 @@ function withGlobalActEnvironment(actImplementation: ReactAct) {
if (callbackNeedsToBeAwaited) {
const thenable = actResult;
return {
- then: (resolve: (value: never) => never, reject: (value: never) => never) => {
- // eslint-disable-next-line
+ then: (resolve: (value: unknown) => void, reject: (error: unknown) => void) => {
+ // eslint-disable-next-line promise/catch-or-return, promise/prefer-await-to-then
thenable.then(
// eslint-disable-next-line promise/always-return
(returnValue) => {
@@ -72,9 +89,7 @@ function withGlobalActEnvironment(actImplementation: ReactAct) {
};
}
-const act: ReactAct = checkReactVersionAtLeast(18, 0)
- ? (withGlobalActEnvironment(reactTestRendererAct) as ReactAct)
- : reactTestRendererAct;
+const act: ReactAct = withGlobalActEnvironment(reactAct) as ReactAct;
export default act;
export { setIsReactActEnvironment as setReactActEnvironment, getIsReactActEnvironment };
diff --git a/src/config.ts b/src/config.ts
index 388933cdd..7066b81a6 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,4 +1,4 @@
-import { DebugOptions } from './helpers/debug-deep';
+import type { DebugOptions } from './helpers/debug-deep';
/**
* Global configuration options for React Native Testing Library.
@@ -13,12 +13,6 @@ export type Config = {
/** Default options for `debug` helper. */
defaultDebugOptions?: Partial;
-
- /**
- * Set to `true` to enable concurrent rendering.
- * Otherwise `render` will default to legacy synchronous rendering.
- */
- concurrentRoot: boolean;
};
export type ConfigAliasOptions = {
@@ -43,7 +37,6 @@ export type InternalConfig = Config & {
const defaultConfig: InternalConfig = {
asyncUtilTimeout: 1000,
defaultIncludeHiddenElements: false,
- concurrentRoot: false,
};
let config = { ...defaultConfig };
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 0f0287f5e..9a67b1f28 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -1,4 +1,3 @@
-import { ReactTestInstance } from 'react-test-renderer';
import {
ViewProps,
TextProps,
@@ -6,18 +5,20 @@ import {
PressableProps,
ScrollViewProps,
} from 'react-native';
+import { HostElement } from 'universal-test-renderer';
import act from './act';
-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 { isTextInputEditable } from './helpers/text-input';
import { Point, StringWithAutocomplete } from './types';
import { nativeState } from './native-state';
+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;
}
@@ -30,7 +31,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.
@@ -48,13 +57,13 @@ const textInputEventsIgnoringEditableProp = new Set([
]);
export function isEventEnabled(
- element: ReactTestInstance,
+ element: HostElement,
eventName: string,
- nearestTouchResponder?: ReactTestInstance,
+ nearestTouchResponder?: HostElement,
) {
if (isHostTextInput(nearestTouchResponder)) {
return (
- isTextInputEditable(nearestTouchResponder) ||
+ isTextInputEditable(nearestTouchResponder!) ||
textInputEventsIgnoringEditableProp.has(eventName)
);
}
@@ -73,14 +82,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);
- 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) {
@@ -90,7 +101,7 @@ function findEventHandler(
return findEventHandler(element.parent, eventName, touchResponder);
}
-function getEventHandler(element: ReactTestInstance, eventName: string) {
+function getEventHandler(element: HostElement, eventName: string) {
const eventHandlerName = getEventHandlerName(eventName);
if (typeof element.props[eventHandlerName] === 'function') {
return element.props[eventHandlerName];
@@ -120,7 +131,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;
}
@@ -140,13 +151,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;
+
+ let 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[]) =>
+ let 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;
@@ -159,7 +198,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' &&
diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx
index 0746d58ff..f858a2b55 100644
--- a/src/helpers/__tests__/component-tree.test.tsx
+++ b/src/helpers/__tests__/component-tree.test.tsx
@@ -1,17 +1,7 @@
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 { getHostSiblings, getRootElement } from '../component-tree';
function MultipleHostChildren() {
return (
@@ -23,155 +13,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(
@@ -194,31 +35,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(
@@ -226,10 +46,6 @@ describe('getUnsafeRootElement()', () => {
);
const view = screen.getByTestId('view');
- expect(getUnsafeRootElement(view)).toEqual(screen.UNSAFE_root);
- });
-
- it('returns null for null', () => {
- expect(getUnsafeRootElement(null)).toEqual(null);
+ expect(getRootElement(view)).toEqual(screen.container);
});
});
diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts
index 5eee9401a..d24be1c5a 100644
--- a/src/helpers/accessibility.ts
+++ b/src/helpers/accessibility.ts
@@ -5,8 +5,8 @@ import {
Role,
StyleSheet,
} from 'react-native';
-import { ReactTestInstance } from 'react-test-renderer';
-import { getHostSiblings, getUnsafeRootElement } from './component-tree';
+import { HostElement } from 'universal-test-renderer';
+import { getHostSiblings, getRootElement } from './component-tree';
import {
getHostComponentNames,
isHostImage,
@@ -16,9 +16,10 @@ import {
} from './host-component-names';
import { getTextContent } from './text-content';
import { isTextInputEditable } from './text-input';
+import { findAllByProps } from './find-all';
type IsInaccessibleOptions = {
- cache?: WeakMap;
+ cache?: WeakMap;
};
export const accessibilityStateKeys: (keyof AccessibilityState)[] = [
@@ -32,14 +33,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);
@@ -61,7 +62,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;
@@ -98,7 +99,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;
}
@@ -133,7 +134,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);
@@ -164,11 +165,11 @@ 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 explicitLabel = element.props['aria-label'] ?? element.props.accessibilityLabel;
if (explicitLabel) {
return explicitLabel;
@@ -182,17 +183,17 @@ export function computeAriaLabel(element: ReactTestInstance): string | undefined
return undefined;
}
-export function computeAriaLabelledBy(element: ReactTestInstance): string | undefined {
+export function computeAriaLabelledBy(element: HostElement): string | undefined {
return element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy;
}
// 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)) {
@@ -208,7 +209,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) && !isTextInputEditable(element)) {
return true;
}
@@ -218,16 +219,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,
@@ -244,7 +245,7 @@ export function computeAriaValue(element: ReactTestInstance): AccessibilityValue
};
}
-export function computeAccessibleName(element: ReactTestInstance): string | undefined {
+export function computeAccessibleName(element: HostElement): string | undefined {
const label = computeAriaLabel(element);
if (label) {
return label;
@@ -252,10 +253,10 @@ export function computeAccessibleName(element: ReactTestInstance): string | unde
const labelElementId = computeAriaLabelledBy(element);
if (labelElementId) {
- const rootElement = getUnsafeRootElement(element);
- const labelElement = rootElement?.findByProps({ nativeID: labelElementId });
- if (labelElement) {
- return getTextContent(labelElement);
+ const rootElement = getRootElement(element);
+ const labelElement = findAllByProps(rootElement, { nativeID: labelElementId });
+ if (labelElement.length > 0) {
+ return getTextContent(labelElement[0]);
}
}
diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts
index 4a4a00897..73b87036d 100644
--- a/src/helpers/component-tree.ts
+++ b/src/helpers/component-tree.ts
@@ -1,105 +1,42 @@
-import { ReactTestInstance } from 'react-test-renderer';
+import { CONTAINER_TYPE, HostElement } 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 | null) {
- return getUnsafeRootElement(element) === screen.UNSAFE_root;
+export function isElementMounted(element: HostElement) {
+ return getRootElement(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 getRootElement(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 | null) {
- if (element == null) {
- return null;
- }
-
- 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-deep.ts b/src/helpers/debug-deep.ts
index 0450330e9..5c29ea15e 100644
--- a/src/helpers/debug-deep.ts
+++ b/src/helpers/debug-deep.ts
@@ -1,4 +1,4 @@
-import type { ReactTestRendererJSON } from 'react-test-renderer';
+import type { JsonNode } from 'universal-test-renderer';
import format, { FormatOptions } from './format';
export type DebugOptions = {
@@ -9,7 +9,7 @@ export type DebugOptions = {
* Log pretty-printed deep test component instance
*/
export default function debugDeep(
- instance: ReactTestRendererJSON | ReactTestRendererJSON[],
+ instance: JsonNode | JsonNode[],
options?: DebugOptions | string,
) {
const message = typeof options === 'string' ? options : options?.message;
diff --git a/src/helpers/debug-shallow.ts b/src/helpers/debug-shallow.ts
deleted file mode 100644
index 510a1f402..000000000
--- a/src/helpers/debug-shallow.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from 'react';
-import type { ReactTestInstance } from 'react-test-renderer';
-import { shallowInternal } from '../shallow';
-import format from './format';
-
-/**
- * Log pretty-printed shallow test component instance
- */
-export default function debugShallow(
- instance: ReactTestInstance | React.ReactElement,
- message?: string,
-) {
- const { output } = shallowInternal(instance);
-
- if (message) {
- // eslint-disable-next-line no-console
- console.log(`${message}\n\n`, format(output));
- } else {
- // eslint-disable-next-line no-console
- console.log(format(output));
- }
-}
diff --git a/src/helpers/find-all.ts b/src/helpers/find-all.ts
index ffd62f936..28e07cbfe 100644
--- a/src/helpers/find-all.ts
+++ b/src/helpers/find-all.ts
@@ -1,7 +1,7 @@
-import { ReactTestInstance } from 'react-test-renderer';
+import { HostElement } from 'universal-test-renderer';
import { getConfig } from '../config';
import { isHiddenFromAccessibility } from './accessibility';
-import { HostTestInstance, isHostElement } from './component-tree';
+import { isValidElement } from './component-tree';
interface FindAllOptions {
/** Match elements hidden from accessibility */
@@ -15,10 +15,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 =
@@ -28,35 +28,38 @@ 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[] = [];
+
+ //console.log(`${indent} 🟢 findAllInternal`, node.type, node.props);
// 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.
@@ -64,3 +67,20 @@ function findAllInternal(
return results;
}
+
+export function findAllByProps(
+ root: HostElement,
+ props: { [propName: string]: any },
+ options?: FindAllOptions,
+): HostElement[] {
+ return findAll(root, (element) => matchProps(element, props), options);
+}
+
+function matchProps(element: HostElement, props: { [propName: string]: any }): boolean {
+ for (const key in props) {
+ if (props[key] !== element.props[key]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/src/helpers/format.ts b/src/helpers/format.ts
index 33dfb2e3f..476c91816 100644
--- a/src/helpers/format.ts
+++ b/src/helpers/format.ts
@@ -1,19 +1,16 @@
-import type { ReactTestRendererJSON } from 'react-test-renderer';
import prettyFormat, { NewPlugin, plugins } from 'pretty-format';
+import { JsonElement, JsonNode } from 'universal-test-renderer';
export type MapPropsFunction = (
props: Record,
- node: ReactTestRendererJSON,
+ node: JsonElement,
) => Record;
export type FormatOptions = {
mapProps?: MapPropsFunction;
};
-const format = (
- input: ReactTestRendererJSON | ReactTestRendererJSON[],
- options: FormatOptions = {},
-) =>
+const format = (input: JsonNode | JsonNode[], options: FormatOptions = {}) =>
prettyFormat(input, {
plugins: [getCustomPlugin(options.mapProps), plugins.ReactElement],
highlight: true,
diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx
index b450c930b..dcdf19670 100644
--- a/src/helpers/host-component-names.tsx
+++ b/src/helpers/host-component-names.tsx
@@ -1,9 +1,9 @@
import * as React from 'react';
-import { ReactTestInstance } from 'react-test-renderer';
import { Image, Modal, ScrollView, Switch, Text, TextInput, View } from 'react-native';
+import { createRoot, HostElement } from 'universal-test-renderer/react-native';
import { configureInternal, getConfig, HostComponentNames } from '../config';
-import { renderWithAct } from '../render-act';
-import { HostTestInstance } from './component-tree';
+import act from '../act';
+import { findAll } from './find-all';
const userConfigErrorMessage = `There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library.`;
@@ -30,24 +30,26 @@ export function configureHostComponentNamesIfNeeded() {
function detectHostComponentNames(): HostComponentNames {
try {
- const renderer = renderWithAct(
-
- Hello
-
-
-
-
-
- ,
- );
-
+ const renderer = createRoot({});
+ act(() => {
+ renderer.render(
+
+ Hello
+
+
+
+
+
+ ,
+ );
+ });
return {
- text: getByTestId(renderer.root, 'text').type as string,
- textInput: getByTestId(renderer.root, 'textInput').type as string,
- image: getByTestId(renderer.root, 'image').type as string,
- switch: getByTestId(renderer.root, 'switch').type as string,
- scrollView: getByTestId(renderer.root, 'scrollView').type as string,
- modal: getByTestId(renderer.root, 'modal').type as string,
+ text: getByTestId(renderer.container, 'text').type as string,
+ textInput: getByTestId(renderer.container, 'textInput').type as string,
+ image: getByTestId(renderer.container, 'image').type as string,
+ switch: getByTestId(renderer.container, 'switch').type as string,
+ scrollView: getByTestId(renderer.container, 'scrollView').type as string,
+ modal: getByTestId(renderer.container, 'modal').type as string,
};
} catch (error) {
const errorMessage =
@@ -59,8 +61,9 @@ function detectHostComponentNames(): HostComponentNames {
}
}
-function getByTestId(instance: ReactTestInstance, testID: string) {
- const nodes = instance.findAll(
+function getByTestId(element: HostElement, testID: string) {
+ const nodes = findAll(
+ element,
(node) => typeof node.type === 'string' && node.props.testID === testID,
);
@@ -75,7 +78,7 @@ function getByTestId(instance: ReactTestInstance, testID: string) {
* Checks if the given element is a host Text element.
* @param element The element to check.
*/
-export function isHostText(element?: ReactTestInstance | null): element is HostTestInstance {
+export function isHostText(element?: HostElement | null) {
return element?.type === getHostComponentNames().text;
}
@@ -83,7 +86,7 @@ export function isHostText(element?: ReactTestInstance | null): element is HostT
* Checks if the given element is a host TextInput element.
* @param element The element to check.
*/
-export function isHostTextInput(element?: ReactTestInstance | null): element is HostTestInstance {
+export function isHostTextInput(element?: HostElement | null) {
return element?.type === getHostComponentNames().textInput;
}
@@ -91,7 +94,7 @@ export function isHostTextInput(element?: ReactTestInstance | null): element is
* Checks if the given element is a host Image element.
* @param element The element to check.
*/
-export function isHostImage(element?: ReactTestInstance | null): element is HostTestInstance {
+export function isHostImage(element?: HostElement | null) {
return element?.type === getHostComponentNames().image;
}
@@ -99,7 +102,7 @@ export function isHostImage(element?: ReactTestInstance | null): element is Host
* Checks if the given element is a host Switch element.
* @param element The element to check.
*/
-export function isHostSwitch(element?: ReactTestInstance | null): element is HostTestInstance {
+export function isHostSwitch(element?: HostElement | null) {
return element?.type === getHostComponentNames().switch;
}
@@ -107,7 +110,7 @@ export function isHostSwitch(element?: ReactTestInstance | null): element is Hos
* Checks if the given element is a host ScrollView element.
* @param element The element to check.
*/
-export function isHostScrollView(element?: ReactTestInstance | null): element is HostTestInstance {
+export function isHostScrollView(element?: HostElement | null) {
return element?.type === getHostComponentNames().scrollView;
}
@@ -115,6 +118,6 @@ export function isHostScrollView(element?: ReactTestInstance | null): element is
* Checks if the given element is a host Modal element.
* @param element The element to check.
*/
-export function isHostModal(element?: ReactTestInstance | null): element is HostTestInstance {
+export function isHostModal(element?: HostElement | null) {
return element?.type === getHostComponentNames().modal;
}
diff --git a/src/helpers/matchers/match-accessibility-state.ts b/src/helpers/matchers/match-accessibility-state.ts
index 099300db3..3647595a1 100644
--- a/src/helpers/matchers/match-accessibility-state.ts
+++ b/src/helpers/matchers/match-accessibility-state.ts
@@ -1,4 +1,4 @@
-import { ReactTestInstance } from 'react-test-renderer';
+import { HostElement } from 'universal-test-renderer';
import {
computeAriaBusy,
computeAriaChecked,
@@ -19,10 +19,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 c9370166c..8d67e73ef 100644
--- a/src/helpers/matchers/match-accessibility-value.ts
+++ b/src/helpers/matchers/match-accessibility-value.ts
@@ -1,4 +1,4 @@
-import { ReactTestInstance } from 'react-test-renderer';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaValue } from '../accessibility';
import { TextMatch } from '../../matches';
import { matchStringProp } from './match-string-prop';
@@ -11,7 +11,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 1da29d867..379d94774 100644
--- a/src/helpers/matchers/match-label-text.ts
+++ b/src/helpers/matchers/match-label-text.ts
@@ -1,12 +1,12 @@
-import { ReactTestInstance } from 'react-test-renderer';
+import { HostElement } from 'universal-test-renderer';
import { matches, TextMatch, TextMatchOptions } from '../../matches';
import { computeAriaLabel, computeAriaLabelledBy } from '../accessibility';
import { findAll } from '../find-all';
import { matchTextContent } from './match-text-content';
export function matchLabelText(
- root: ReactTestInstance,
- element: ReactTestInstance,
+ root: HostElement,
+ element: HostElement,
expectedText: TextMatch,
options: TextMatchOptions = {},
) {
@@ -17,7 +17,7 @@ export function matchLabelText(
}
function matchAccessibilityLabel(
- element: ReactTestInstance,
+ element: HostElement,
expectedLabel: TextMatch,
options: TextMatchOptions,
) {
@@ -25,7 +25,7 @@ function matchAccessibilityLabel(
}
function matchAccessibilityLabelledBy(
- root: ReactTestInstance,
+ root: HostElement,
nativeId: string | undefined,
text: TextMatch,
options: TextMatchOptions,
diff --git a/src/helpers/matchers/match-text-content.ts b/src/helpers/matchers/match-text-content.ts
index 41b1d126c..8726cea3b 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 { HostElement } from 'universal-test-renderer';
import { matches, TextMatch, TextMatchOptions } from '../../matches';
import { getTextContent } from '../text-content';
@@ -10,7 +10,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 921b607c8..fef08894c 100644
--- a/src/helpers/pointer-events.ts
+++ b/src/helpers/pointer-events.ts
@@ -1,5 +1,4 @@
-import { ReactTestInstance } from 'react-test-renderer';
-import { getHostParent } from './component-tree';
+import { HostElement } from 'universal-test-renderer';
/**
* pointerEvents controls whether the View can be the target of touch events.
@@ -8,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';
@@ -17,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 6f7433ffb..000000000
--- a/src/helpers/string-validation.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { 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..b6243cf6d 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 { 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 bf76389fe..1bd555850 100644
--- a/src/helpers/text-input.ts
+++ b/src/helpers/text-input.ts
@@ -1,8 +1,8 @@
-import { ReactTestInstance } from 'react-test-renderer';
+import { HostElement } from 'universal-test-renderer';
import { nativeState } from '../native-state';
import { isHostTextInput } from './host-component-names';
-export function isTextInputEditable(element: ReactTestInstance) {
+export function isTextInputEditable(element: HostElement) {
if (!isHostTextInput(element)) {
throw new Error(`Element is not a "TextInput", but it has type "${element.type}".`);
}
@@ -10,7 +10,7 @@ export function isTextInputEditable(element: ReactTestInstance) {
return 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 c22a1df5e..883673a70 100644
--- a/src/helpers/wrap-async.ts
+++ b/src/helpers/wrap-async.ts
@@ -6,6 +6,7 @@ import { checkReactVersionAtLeast } from '../react-versions';
/**
* 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 5c867106f..5d2ab825b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,5 @@
+export { HostElement } from 'universal-test-renderer';
+
import { cleanup } from './pure';
import { flushMicroTasksLegacy } from './flush-micro-tasks';
import { getIsReactActEnvironment, setReactActEnvironment } from './act';
diff --git a/src/matchers/__tests__/to-be-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx
index 6cf432b85..2b674c627 100644
--- a/src/matchers/__tests__/to-be-checked.test.tsx
+++ b/src/matchers/__tests__/to-be-checked.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { type AccessibilityRole, Switch, View } from 'react-native';
-import render from '../../render';
-import { screen } from '../../screen';
+import { render, screen } from '../..';
function renderViewsWithRole(role: AccessibilityRole) {
render(
diff --git a/src/matchers/__tests__/to-be-partially-checked.test.tsx b/src/matchers/__tests__/to-be-partially-checked.test.tsx
index 03ab58290..dd84e0cb7 100644
--- a/src/matchers/__tests__/to-be-partially-checked.test.tsx
+++ b/src/matchers/__tests__/to-be-partially-checked.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { type AccessibilityRole, View } from 'react-native';
-import render from '../../render';
-import { screen } from '../../screen';
+import { render, screen } from '../..';
function renderViewsWithRole(role: AccessibilityRole) {
return render(
diff --git a/src/matchers/__tests__/to-have-accessible-name.test.tsx b/src/matchers/__tests__/to-have-accessible-name.test.tsx
index d797bacaf..d5c98cf99 100644
--- a/src/matchers/__tests__/to-have-accessible-name.test.tsx
+++ b/src/matchers/__tests__/to-have-accessible-name.test.tsx
@@ -118,14 +118,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(`
@@ -133,6 +133,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 54801b088..f577d973a 100644
--- a/src/matchers/__tests__/to-have-text-content.test.tsx
+++ b/src/matchers/__tests__/to-have-text-content.test.tsx
@@ -5,7 +5,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 31cfdade9..1a73b78f1 100644
--- a/src/matchers/__tests__/utils.test.tsx
+++ b/src/matchers/__tests__/utils.test.tsx
@@ -25,7 +25,7 @@ test('checkHostElement allows rejects composite element', () => {
expect(() => {
// @ts-expect-error
- checkHostElement(screen.UNSAFE_root, fakeMatcher, {});
+ checkHostElement(screen.container, fakeMatcher, {});
}).toThrow(/value must be a host element./);
});
diff --git a/src/matchers/to-be-busy.tsx b/src/matchers/to-be-busy.tsx
index effc027c1..7312ec563 100644
--- a/src/matchers/to-be-busy.tsx
+++ b/src/matchers/to-be-busy.tsx
@@ -1,9 +1,9 @@
-import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaBusy } from '../helpers/accessibility';
import { checkHostElement, formatElement } 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.tsx b/src/matchers/to-be-checked.tsx
index fb9be3927..c001d6d52 100644
--- a/src/matchers/to-be-checked.tsx
+++ b/src/matchers/to-be-checked.tsx
@@ -1,5 +1,5 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import {
computeAriaChecked,
getRole,
@@ -10,7 +10,7 @@ import { ErrorWithStack } from '../helpers/errors';
import { isHostSwitch } from '../helpers/host-component-names';
import { checkHostElement, formatElement } 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)) {
@@ -34,7 +34,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.tsx b/src/matchers/to-be-disabled.tsx
index 3c917e078..6d8ba0c19 100644
--- a/src/matchers/to-be-disabled.tsx
+++ b/src/matchers/to-be-disabled.tsx
@@ -1,10 +1,9 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaDisabled } from '../helpers/accessibility';
-import { getHostParent } from '../helpers/component-tree';
import { checkHostElement, formatElement } 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);
@@ -23,7 +22,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);
@@ -42,8 +41,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.tsx b/src/matchers/to-be-empty-element.tsx
index 8a61bf269..59422cac5 100644
--- a/src/matchers/to-be-empty-element.tsx
+++ b/src/matchers/to-be-empty-element.tsx
@@ -1,12 +1,11 @@
-import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
-import { getHostChildren } from '../helpers/component-tree';
+import { HostElement } from 'universal-test-renderer';
import { checkHostElement, formatElementArray } 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,
diff --git a/src/matchers/to-be-expanded.tsx b/src/matchers/to-be-expanded.tsx
index cc0744a79..13be3208a 100644
--- a/src/matchers/to-be-expanded.tsx
+++ b/src/matchers/to-be-expanded.tsx
@@ -1,9 +1,9 @@
-import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaExpanded } from '../helpers/accessibility';
import { checkHostElement, formatElement } from './utils';
-export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstance) {
+export function toBeExpanded(this: jest.MatcherContext, element: HostElement) {
checkHostElement(element, toBeExpanded, this);
return {
@@ -20,7 +20,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.tsx b/src/matchers/to-be-on-the-screen.tsx
index c00958222..b47e6d1e6 100644
--- a/src/matchers/to-be-on-the-screen.tsx
+++ b/src/matchers/to-be-on-the-screen.tsx
@@ -1,15 +1,15 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
-import { getUnsafeRootElement } from '../helpers/component-tree';
+import { HostElement } from 'universal-test-renderer';
+import { getRootElement } from '../helpers/component-tree';
import { screen } from '../screen';
import { checkHostElement, formatElement } 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 === getRootElement(element);
const errorFound = () => {
return `expected element tree not to contain element, but found\n${formatElement(element)}`;
diff --git a/src/matchers/to-be-partially-checked.tsx b/src/matchers/to-be-partially-checked.tsx
index 975c48e93..b4000af23 100644
--- a/src/matchers/to-be-partially-checked.tsx
+++ b/src/matchers/to-be-partially-checked.tsx
@@ -1,10 +1,10 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility';
import { ErrorWithStack } from '../helpers/errors';
import { checkHostElement, formatElement } 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)) {
@@ -28,7 +28,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 be03cc997..cda67ce1d 100644
--- a/src/matchers/to-be-selected.ts
+++ b/src/matchers/to-be-selected.ts
@@ -1,9 +1,9 @@
-import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaSelected } from '../helpers/accessibility';
import { checkHostElement, formatElement } 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.tsx b/src/matchers/to-be-visible.tsx
index e817f6701..0a6e08bc4 100644
--- a/src/matchers/to-be-visible.tsx
+++ b/src/matchers/to-be-visible.tsx
@@ -1,12 +1,11 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
import { StyleSheet } from 'react-native';
+import { HostElement } from 'universal-test-renderer';
import { isHiddenFromAccessibility } from '../helpers/accessibility';
-import { getHostParent } from '../helpers/component-tree';
import { isHostModal } from '../helpers/host-component-names';
import { checkHostElement, formatElement } 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);
}
@@ -26,11 +25,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;
}
@@ -45,7 +44,7 @@ function isElementVisible(
return false;
}
- const hostParent = getHostParent(element);
+ const hostParent = element.parent;
if (hostParent === null) {
return true;
}
@@ -53,7 +52,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.tsx b/src/matchers/to-contain-element.tsx
index 875c6c18a..9e34d08e6 100644
--- a/src/matchers/to-contain-element.tsx
+++ b/src/matchers/to-contain-element.tsx
@@ -1,11 +1,12 @@
-import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
+import { findAll } from '../helpers/find-all';
import { checkHostElement, formatElement } from './utils';
export function toContainElement(
this: jest.MatcherContext,
- container: ReactTestInstance,
- element: ReactTestInstance | null,
+ container: HostElement,
+ element: HostElement | null,
) {
checkHostElement(container, toContainElement, this);
@@ -13,9 +14,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.tsx b/src/matchers/to-have-accessibility-value.tsx
index 6241adc28..d1be0e927 100644
--- a/src/matchers/to-have-accessibility-value.tsx
+++ b/src/matchers/to-have-accessibility-value.tsx
@@ -1,5 +1,5 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, stringify } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAriaValue } from '../helpers/accessibility';
import {
AccessibilityValueMatcher,
@@ -10,7 +10,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.tsx b/src/matchers/to-have-accessible-name.tsx
index fce6ac365..fed3b0b59 100644
--- a/src/matchers/to-have-accessible-name.tsx
+++ b/src/matchers/to-have-accessible-name.tsx
@@ -1,12 +1,12 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { computeAccessibleName } from '../helpers/accessibility';
import { TextMatch, TextMatchOptions, matches } from '../matches';
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.tsx b/src/matchers/to-have-display-value.tsx
index a47511e7e..c51a0840e 100644
--- a/src/matchers/to-have-display-value.tsx
+++ b/src/matchers/to-have-display-value.tsx
@@ -1,5 +1,5 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { isHostTextInput } from '../helpers/host-component-names';
import { ErrorWithStack } from '../helpers/errors';
import { getTextInputValue } from '../helpers/text-input';
@@ -8,7 +8,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 d4d8b0821..9d3dd9a65 100644
--- a/src/matchers/to-have-prop.ts
+++ b/src/matchers/to-have-prop.ts
@@ -1,10 +1,10 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, stringify, printExpected } from 'jest-matcher-utils';
+import { 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.tsx b/src/matchers/to-have-style.tsx
index 9522c79d2..a6a632318 100644
--- a/src/matchers/to-have-style.tsx
+++ b/src/matchers/to-have-style.tsx
@@ -1,6 +1,6 @@
-import type { ReactTestInstance } from 'react-test-renderer';
import { ImageStyle, StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native';
import { matcherHint, diff } from 'jest-matcher-utils';
+import { HostElement } from 'universal-test-renderer';
import { checkHostElement, formatMessage } from './utils';
export type Style = ViewStyle | TextStyle | ImageStyle;
@@ -9,7 +9,7 @@ type StyleLike = Record;
export function toHaveStyle(
this: jest.MatcherContext,
- element: ReactTestInstance,
+ element: HostElement,
style: StyleProp