diff --git a/.codecov.yml b/.codecov.yml
index b357b453c..43e3f2a3b 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -8,7 +8,7 @@ coverage:
target: 80% # Required patch coverage target
project:
default:
- threshold: 0.5% # Allowable coverage drop in percentage points
+ threshold: 0.5% # Allowable coverage drop in percentage points
comment:
behavior: default
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/examples/cookbook/.eslintrc b/examples/cookbook/.eslintrc
index 076062c68..2fc9d646b 100644
--- a/examples/cookbook/.eslintrc
+++ b/examples/cookbook/.eslintrc
@@ -3,6 +3,6 @@
"rules": {
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": "off",
"react-native/no-color-literals": "off",
- "react-native-a11y/has-valid-accessibility-descriptors": "off",
+ "react-native-a11y/has-valid-accessibility-descriptors": "off"
}
}
diff --git a/examples/cookbook/README.md b/examples/cookbook/README.md
index 0b87a8b02..15fbb622a 100644
--- a/examples/cookbook/README.md
+++ b/examples/cookbook/README.md
@@ -3,6 +3,7 @@
# React Native Testing Library Cookbook App
+
Welcome to the React Native Testing Library (RNTL) Cookbook! This app is designed to provide developers with a collection of best practices, ready-made recipes, and tips & tricks to help you effectively test your React Native applications. Whether you’re just starting out with testing or looking to deepen your skills, this cookbook offers something for everyone.
Each recipe described in the Cookbook should have a corresponding code example screen in this repo.
@@ -12,6 +13,7 @@ Since examples will showcase usage of different dependencies, the dependencies i
file will grow much larger that in a normal React Native. This is fine 🐶☕️🔥.
## Running the App
+
1. Clone the repo `git clone git@github.com:callstack/react-native-testing-library.git`
2. Go to the `examples/cookbook` directory `cd examples/cookbook`
3. Install dependencies `yarn`
@@ -19,12 +21,14 @@ file will grow much larger that in a normal React Native. This is fine 🐶☕
5. Run the app either on iOS or Android by clicking on `i` or `a` in the terminal.
## How to Contribute
+
We invite all developers, from beginners to experts, to contribute your own recipes! If you have a clever solution, best practice, or useful tip, we encourage you to:
1. Submit a Pull Request with your recipe.
2. Join the conversation on GitHub [here](https://github.com/callstack/react-native-testing-library/issues/1624) to discuss ideas, ask questions, or provide feedback.
## Screenshots From the App
+
| Home Screen | Phonebook with Net. Req. Example |
-|-------------------------------------------------------|-----------------------------------------------------------------|
+| ----------------------------------------------------- | --------------------------------------------------------------- |
|  |  |
diff --git a/examples/cookbook/app/network-requests/__tests__/test-utils.ts b/examples/cookbook/app/network-requests/__tests__/test-utils.ts
index c14edd6b5..44deef49e 100644
--- a/examples/cookbook/app/network-requests/__tests__/test-utils.ts
+++ b/examples/cookbook/app/network-requests/__tests__/test-utils.ts
@@ -1,6 +1,6 @@
import { User } from '../types';
-import {http, HttpResponse} from "msw";
-import {setupServer} from "msw/node";
+import { http, HttpResponse } from 'msw';
+import { setupServer } from 'msw/node';
// Define request handlers and response resolvers for random user API.
// By default, we always return the happy path response.
diff --git a/flow-typed/npm/jest_v26.x.x.js b/flow-typed/npm/jest_v26.x.x.js
index bb0a086df..22361ba92 100644
--- a/flow-typed/npm/jest_v26.x.x.js
+++ b/flow-typed/npm/jest_v26.x.x.js
@@ -54,17 +54,13 @@ type JestMockFn, TReturn> = {
* that come from itself -- the only difference is that the implementation
* will also be executed when the mock is called.
*/
- mockImplementation(
- fn: (...args: TArguments) => TReturn
- ): JestMockFn,
+ mockImplementation(fn: (...args: TArguments) => TReturn): JestMockFn,
/**
* Accepts a function that will be used as an implementation of the mock for
* one call to the mocked function. Can be chained so that multiple function
* calls produce different results.
*/
- mockImplementationOnce(
- fn: (...args: TArguments) => TReturn
- ): JestMockFn,
+ mockImplementationOnce(fn: (...args: TArguments) => TReturn): JestMockFn,
/**
* Accepts a string to use in test result output in place of "jest.fn()" to
* indicate which mock function is being referenced.
@@ -89,9 +85,7 @@ type JestMockFn, TReturn> = {
/**
* Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value))
*/
- mockResolvedValueOnce(
- value: TReturn
- ): JestMockFn>,
+ mockResolvedValueOnce(value: TReturn): JestMockFn>,
/**
* Sugar for jest.fn().mockImplementation(() => Promise.reject(value))
*/
@@ -182,7 +176,7 @@ type JestStyledComponentsMatchersType = {
toHaveStyleRule(
property: string,
value: JestStyledComponentsMatcherValue,
- options?: JestStyledComponentsMatcherOptions
+ options?: JestStyledComponentsMatcherOptions,
): void,
...
};
@@ -205,20 +199,17 @@ type EnzymeMatchersType = {
toExist(): void,
toHaveClassName(className: string): void,
toHaveHTML(html: string): void,
- toHaveProp: ((propKey: string, propValue?: any) => void) &
- ((props: { ... }) => void),
+ toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: { ... }) => void),
toHaveRef(refName: string): void,
- toHaveState: ((stateKey: string, stateValue?: any) => void) &
- ((state: { ... }) => void),
- toHaveStyle: ((styleKey: string, styleValue?: any) => void) &
- ((style: { ... }) => void),
+ toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: { ... }) => void),
+ toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: { ... }) => void),
toHaveTagName(tagName: string): void,
toHaveText(text: string): void,
toHaveValue(value: any): void,
toIncludeText(text: string): void,
toMatchElement(
element: React$Element,
- options?: {| ignoreProps?: boolean, verbose?: boolean |}
+ options?: {| ignoreProps?: boolean, verbose?: boolean |},
): void,
toMatchSelector(selector: string): void,
// 7.x
@@ -250,10 +241,7 @@ type DomTestingLibraryType = {
toHaveFocus(): void,
toHaveFormValues(expectedValues: { [name: string]: any, ... }): void,
toHaveStyle(css: string | { [name: string]: any, ... }): void,
- toHaveTextContent(
- text: string | RegExp,
- options?: {| normalizeWhitespace: boolean |}
- ): void,
+ toHaveTextContent(text: string | RegExp, options?: {| normalizeWhitespace: boolean |}): void,
toHaveValue(value?: string | string[] | number): void,
// 5.x
@@ -562,7 +550,7 @@ type SnapshotDiffType = {
aAnnotation?: string,
bAnnotation?: string,
|},
- testName?: string
+ testName?: string,
): void,
...
};
@@ -816,7 +804,7 @@ type JestObjectType = {
* implementation.
*/
fn, TReturn>(
- implementation?: (...args: TArguments) => TReturn
+ implementation?: (...args: TArguments) => TReturn,
): JestMockFn,
/**
* Determines if the given function is a mocked function.
@@ -836,11 +824,7 @@ type JestObjectType = {
* The third argument can be used to create virtual mocks -- mocks of modules
* that don't exist anywhere in the system.
*/
- mock(
- moduleName: string,
- moduleFactory?: any,
- options?: Object
- ): JestObjectType,
+ mock(moduleName: string, moduleFactory?: any, options?: Object): JestObjectType,
/**
* Returns the actual module instead of a mock, bypassing all checks on
* whether the module should receive a mock implementation or not.
@@ -920,11 +904,7 @@ type JestObjectType = {
* Creates a mock function similar to jest.fn but also tracks calls to
* object[methodName].
*/
- spyOn(
- object: Object,
- methodName: string,
- accessType?: 'get' | 'set'
- ): JestMockFn,
+ spyOn(object: Object, methodName: string, accessType?: 'get' | 'set'): JestMockFn,
/**
* Set the default timeout interval for tests and before/after hooks in milliseconds.
* Note: The default timeout interval is 5 seconds if this method is not called.
@@ -941,25 +921,13 @@ type JestDoneFn = {|
|};
/** Runs this function after every test inside this context */
-declare function afterEach(
- fn: (done: JestDoneFn) => ?Promise,
- timeout?: number
-): void;
+declare function afterEach(fn: (done: JestDoneFn) => ?Promise, timeout?: number): void;
/** Runs this function before every test inside this context */
-declare function beforeEach(
- fn: (done: JestDoneFn) => ?Promise,
- timeout?: number
-): void;
+declare function beforeEach(fn: (done: JestDoneFn) => ?Promise, timeout?: number): void;
/** Runs this function after all tests have finished inside this context */
-declare function afterAll(
- fn: (done: JestDoneFn) => ?Promise,
- timeout?: number
-): void;
+declare function afterAll(fn: (done: JestDoneFn) => ?Promise, timeout?: number): void;
/** Runs this function before any tests have started inside this context */
-declare function beforeAll(
- fn: (done: JestDoneFn) => ?Promise,
- timeout?: number
-): void;
+declare function beforeAll(fn: (done: JestDoneFn) => ?Promise, timeout?: number): void;
/** A context for grouping tests together */
declare var describe: {
@@ -982,11 +950,7 @@ declare var describe: {
*/
each(
...table: Array | mixed> | [Array, string]
- ): (
- name: JestTestName,
- fn?: (...args: Array) => ?Promise,
- timeout?: number
- ) => void,
+ ): (name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number) => void,
...
};
@@ -999,11 +963,7 @@ declare var it: {
* @param {Function} Test
* @param {number} Timeout for the test, in milliseconds.
*/
- (
- name: JestTestName,
- fn?: (done: JestDoneFn) => ?Promise,
- timeout?: number
- ): void,
+ (name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number): void,
/**
* Only run this test
*
@@ -1012,17 +972,13 @@ declare var it: {
* @param {number} Timeout for the test, in milliseconds.
*/
only: {|
- (
- name: JestTestName,
- fn?: (done: JestDoneFn) => ?Promise,
- timeout?: number
- ): void,
+ (name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number): void,
each(
...table: Array | mixed> | [Array, string]
): (
name: JestTestName,
fn?: (...args: Array) => ?Promise,
- timeout?: number
+ timeout?: number,
) => void,
|},
/**
@@ -1032,11 +988,7 @@ declare var it: {
* @param {Function} Test
* @param {number} Timeout for the test, in milliseconds.
*/
- skip(
- name: JestTestName,
- fn?: (done: JestDoneFn) => ?Promise,
- timeout?: number
- ): void,
+ skip(name: JestTestName, fn?: (done: JestDoneFn) => ?Promise, timeout?: number): void,
/**
* Highlight planned tests in the summary output
*
@@ -1053,7 +1005,7 @@ declare var it: {
concurrent(
name: JestTestName,
fn?: (done: JestDoneFn) => ?Promise,
- timeout?: number
+ timeout?: number,
): void,
/**
* each runs this test against array of argument arrays per each run
@@ -1062,18 +1014,14 @@ declare var it: {
*/
each(
...table: Array | mixed> | [Array, string]
- ): (
- name: JestTestName,
- fn?: (...args: Array) => ?Promise,
- timeout?: number
- ) => void,
+ ): (name: JestTestName, fn?: (...args: Array) => ?Promise, timeout?: number) => void,
...
};
declare function fit(
name: JestTestName,
fn: (done: JestDoneFn) => ?Promise,
- timeout?: number
+ timeout?: number,
): void;
/** An individual test unit */
declare var test: typeof it;
@@ -1146,7 +1094,7 @@ type JestPrettyFormatPlugin = {
serialize: JestPrettyFormatPrint,
indent: JestPrettyFormatIndent,
opts: JestPrettyFormatOptions,
- colors: JestPrettyFormatColors
+ colors: JestPrettyFormatColors,
) => string,
test: (any) => boolean,
...
@@ -1158,7 +1106,7 @@ type JestPrettyFormatPlugins = Array;
declare var expect: {
/** The object that you want to make assertions against */
(
- value: any
+ value: any,
): JestExpectType &
JestPromiseType &
EnzymeMatchersType &
@@ -1210,7 +1158,7 @@ declare var jasmine: {
createSpy(name: string): JestSpyType,
createSpyObj(
baseName: string,
- methodNames: Array
+ methodNames: Array,
): { [methodName: string]: JestSpyType, ... },
objectContaining(value: Object): Object,
stringMatching(value: string): string,
diff --git a/flow-typed/npm/react-test-renderer_v16.x.x.js b/flow-typed/npm/react-test-renderer_v16.x.x.js
index 87a149a1d..84f7c17c0 100644
--- a/flow-typed/npm/react-test-renderer_v16.x.x.js
+++ b/flow-typed/npm/react-test-renderer_v16.x.x.js
@@ -31,15 +31,12 @@ type ReactTestInstance = {
findAll(
predicate: (node: ReactTestInstance) => boolean,
- options?: { deep: boolean }
- ): ReactTestInstance[],
- findAllByType(
- type: React$ElementType,
- options?: { deep: boolean }
+ options?: { deep: boolean },
): ReactTestInstance[],
+ findAllByType(type: React$ElementType, options?: { deep: boolean }): ReactTestInstance[],
findAllByProps(
props: { [propName: string]: any },
- options?: { deep: boolean }
+ options?: { deep: boolean },
): ReactTestInstance[],
};
@@ -63,7 +60,7 @@ declare module 'react-test-renderer' {
declare function create(
nextElement: React$Element,
- options?: TestRendererOptions
+ options?: TestRendererOptions,
): ReactTestRenderer;
declare function act(callback: () => void): Thenable;
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..ddbb4ed9e 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.9.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"packageManager": "yarn@4.6.0",
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
}
diff --git a/src/__tests__/__snapshots__/render-debug.test.tsx.snap b/src/__tests__/__snapshots__/render-debug.test.tsx.snap
index 6618efcba..84cc1e843 100644
--- a/src/__tests__/__snapshots__/render-debug.test.tsx.snap
+++ b/src/__tests__/__snapshots__/render-debug.test.tsx.snap
@@ -1,297 +1,311 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`debug 1`] = `
-"
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
-
+"<>
+
+
+ Is the banana fresh?
+
+
+ not fresh
+
+
+
+
+
+
+
+ Change freshness!
+
+
+
+ First Text
+
+
+ Second Text
+
- Change freshness!
+ 0
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
+>"
`;
exports[`debug changing component: bananaFresh button message should now be "fresh" 1`] = `
-"
-
- Is the banana fresh?
-
-
- fresh
-
-
-
-
-
-
+
+
+ Is the banana fresh?
+
+
+ fresh
+
+
+
+
+
+
+ accessible={true}
+ collapsable={false}
+ focusable={true}
+ onBlur={[Function onBlur]}
+ onClick={[Function onClick]}
+ onFocus={[Function onFocus]}
+ onResponderGrant={[Function onResponderGrant]}
+ onResponderMove={[Function onResponderMove]}
+ onResponderRelease={[Function onResponderRelease]}
+ onResponderTerminate={[Function onResponderTerminate]}
+ onResponderTerminationRequest={[Function onResponderTerminationRequest]}
+ onStartShouldSetResponder={[Function onStartShouldSetResponder]}
+ role="button"
+ >
+
+ Change freshness!
+
+
+
+ First Text
+
+
+ Second Text
+
- Change freshness!
+ 0
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
+>"
`;
exports[`debug should use debugOptions from config when no option is specified 1`] = `
-"
-
- hello
-
-"
+"<>
+
+
+ hello
+
+
+>"
`;
exports[`debug should use given options over config debugOptions 1`] = `
-"
+
-
- hello
-
-"
+ >
+
+ hello
+
+
+>"
`;
exports[`debug with only children prop 1`] = `
-"
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
+"<>
- Change freshness!
+ Is the banana fresh?
+
+
+ not fresh
+
+
+
+
+
+
+
+ Change freshness!
+
+
+
+ First Text
+
+
+ Second Text
+
+
+ 0
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
+>"
`;
exports[`debug with only prop whose value is bananaChef 1`] = `
-"
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
+"<>
- Change freshness!
+ Is the banana fresh?
+
+
+ not fresh
+
+
+
+
+
+
+
+ Change freshness!
+
+
+
+ First Text
+
+
+ Second Text
+
+
+ 0
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
+>"
`;
exports[`debug: All Props 1`] = `
-"
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
-
+
+
+ Is the banana fresh?
+
+
+ not fresh
+
+
+
+
+
+
+ accessible={true}
+ collapsable={false}
+ focusable={true}
+ onBlur={[Function onBlur]}
+ onClick={[Function onClick]}
+ onFocus={[Function onFocus]}
+ onResponderGrant={[Function onResponderGrant]}
+ onResponderMove={[Function onResponderMove]}
+ onResponderRelease={[Function onResponderRelease]}
+ onResponderTerminate={[Function onResponderTerminate]}
+ onResponderTerminationRequest={[Function onResponderTerminationRequest]}
+ onStartShouldSetResponder={[Function onStartShouldSetResponder]}
+ role="button"
+ >
+
+ Change freshness!
+
+
+
+ First Text
+
+
+ Second Text
+
- Change freshness!
+ 0
-
- First Text
-
-
- Second Text
-
-
- 0
-
-
+>
undefined"
`;
@@ -299,53 +313,55 @@ exports[`debug: Option message 1`] = `
"another custom message
-
-
- Is the banana fresh?
-
-
- not fresh
-
-
-
-
-
-
+<>
+
- Change freshness!
+ Is the banana fresh?
+
+
+ not fresh
+
+
+
+
+
+
+
+ Change freshness!
+
+
+
+ First Text
+
+
+ Second Text
+
+
+ 0
-
- First Text
-
-
- Second Text
-
-
- 0
-
-"
+>"
`;
diff --git a/src/__tests__/__snapshots__/render.test.tsx.snap b/src/__tests__/__snapshots__/render.test.tsx.snap
index 018ae7d63..1fa87e1e6 100644
--- a/src/__tests__/__snapshots__/render.test.tsx.snap
+++ b/src/__tests__/__snapshots__/render.test.tsx.snap
@@ -1,39 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`toJSON renders host output 1`] = `
-
+
-
- press me
-
-
+ accessible={true}
+ collapsable={false}
+ focusable={true}
+ onBlur={[Function]}
+ onClick={[Function]}
+ onFocus={[Function]}
+ onResponderGrant={[Function]}
+ onResponderMove={[Function]}
+ onResponderRelease={[Function]}
+ onResponderTerminate={[Function]}
+ onResponderTerminationRequest={[Function]}
+ onStartShouldSetResponder={[Function]}
+ >
+
+ press me
+
+
+>
`;
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__/event-handler.test.tsx b/src/__tests__/event-handler.test.tsx
index 2b4eb577e..2c7f5ac43 100644
--- a/src/__tests__/event-handler.test.tsx
+++ b/src/__tests__/event-handler.test.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { Text, View } from 'react-native';
import { render, screen } from '..';
-import { getEventHandler } from '../event-handler';
+import { getEventHandlerFromProps } from '../event-handler';
test('getEventHandler strict mode', () => {
const onPress = jest.fn();
@@ -22,13 +22,13 @@ test('getEventHandler strict mode', () => {
const testOnly = screen.getByTestId('testOnly');
const both = screen.getByTestId('both');
- expect(getEventHandler(regular, 'press')).toBe(onPress);
- expect(getEventHandler(testOnly, 'press')).toBe(testOnlyOnPress);
- expect(getEventHandler(both, 'press')).toBe(onPress);
+ expect(getEventHandlerFromProps(regular.props, 'press')).toBe(onPress);
+ expect(getEventHandlerFromProps(testOnly.props, 'press')).toBe(testOnlyOnPress);
+ expect(getEventHandlerFromProps(both.props, 'press')).toBe(onPress);
- expect(getEventHandler(regular, 'onPress')).toBe(undefined);
- expect(getEventHandler(testOnly, 'onPress')).toBe(undefined);
- expect(getEventHandler(both, 'onPress')).toBe(undefined);
+ expect(getEventHandlerFromProps(regular.props, 'onPress')).toBe(undefined);
+ expect(getEventHandlerFromProps(testOnly.props, 'onPress')).toBe(undefined);
+ expect(getEventHandlerFromProps(both.props, 'onPress')).toBe(undefined);
});
test('getEventHandler loose mode', () => {
@@ -49,11 +49,13 @@ test('getEventHandler loose mode', () => {
const testOnly = screen.getByTestId('testOnly');
const both = screen.getByTestId('both');
- expect(getEventHandler(regular, 'press', { loose: true })).toBe(onPress);
- expect(getEventHandler(testOnly, 'press', { loose: true })).toBe(testOnlyOnPress);
- expect(getEventHandler(both, 'press', { loose: true })).toBe(onPress);
+ expect(getEventHandlerFromProps(regular.props, 'press', { loose: true })).toBe(onPress);
+ expect(getEventHandlerFromProps(testOnly.props, 'press', { loose: true })).toBe(testOnlyOnPress);
+ expect(getEventHandlerFromProps(both.props, 'press', { loose: true })).toBe(onPress);
- expect(getEventHandler(regular, 'onPress', { loose: true })).toBe(onPress);
- expect(getEventHandler(testOnly, 'onPress', { loose: true })).toBe(testOnlyOnPress);
- expect(getEventHandler(both, 'onPress', { loose: true })).toBe(onPress);
+ expect(getEventHandlerFromProps(regular.props, 'onPress', { loose: true })).toBe(onPress);
+ expect(getEventHandlerFromProps(testOnly.props, 'onPress', { loose: true })).toBe(
+ testOnlyOnPress,
+ );
+ expect(getEventHandlerFromProps(both.props, 'onPress', { loose: true })).toBe(onPress);
});
diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx
index cdada565a..0db31fd32 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,39 +45,21 @@ describe('fireEvent', () => {
const text = 'New press text';
render();
- fireEvent(screen.getByText(text), 'press');
+ fireEvent.press(screen.getByText(text));
expect(onPressMock).toHaveBeenCalled();
});
- test('should not fire if the press handler is not passed to children', () => {
+ test('should fire if the press handler is not passed to children', () => {
const onPressMock = jest.fn();
- render(
- // TODO: this functionality is buggy, i.e. it will fail if we wrap this component with a View.
- ,
- );
+ render();
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);
+ expect(onPressMock).toHaveBeenCalled();
});
});
test('fireEvent.press', () => {
const onPressMock = jest.fn();
- const text = 'Fireevent press';
+ const text = 'FireEvent press';
const eventData = {
nativeEvent: {
pageX: 20,
@@ -114,7 +70,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 +119,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 +188,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 +202,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__/react-native-api.test.tsx b/src/__tests__/react-native-api.test.tsx
index 15fcbfc03..f69a7a46c 100644
--- a/src/__tests__/react-native-api.test.tsx
+++ b/src/__tests__/react-native-api.test.tsx
@@ -12,9 +12,11 @@ test('React Native API assumption: renders a single host element', () =>
render();
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
+
+ >
`);
});
@@ -22,11 +24,13 @@ test('React Native API assumption: renders a single host element', () =>
render(Hello);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
- Hello
-
+ <>
+
+ Hello
+
+ >
`);
});
@@ -42,25 +46,27 @@ test('React Native API assumption: nested renders a single host element',
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
-
- Before
-
- Hello
+ <>
+ Before
+
+ Hello
+
- Deeply nested
+
+ Deeply nested
+
-
+ >
`);
});
@@ -75,12 +81,14 @@ test('React Native API assumption: renders a single host element', (
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
+
+ >
`);
});
@@ -92,14 +100,16 @@ test('React Native API assumption: with nested Text renders single h
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
-
- Hello
-
-
+ <>
+
+
+ Hello
+
+
+ >
`);
});
@@ -107,20 +117,22 @@ test('React Native API assumption: renders a single host element', () =
render();
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+
+ testID="test"
+ value={true}
+ />
+ >
`);
});
@@ -128,15 +140,17 @@ test('React Native API assumption: renders a single host element', () =>
render();
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+
+ testID="test"
+ />
+ >
`);
});
@@ -148,15 +162,17 @@ test('React Native API assumption: renders a single host element',
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
-
-
-
-
+ <>
+
+
+
+
+
+ >
`);
});
@@ -166,51 +182,53 @@ test('React Native API assumption: renders a single host
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
-
-
-
- 1
-
-
-
-
- 2
-
+ <>
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
-
-
+
+ >
`);
});
@@ -222,15 +240,17 @@ test('React Native API assumption: renders a single host element', () =>
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
-
- Modal Content
-
-
+ <>
+
+
+ Modal Content
+
+
+ >
`);
});
@@ -259,26 +279,28 @@ test('React Native API assumption: aria-* props render directly on host View', (
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
+
+ >
`);
});
@@ -307,26 +329,28 @@ test('React Native API assumption: aria-* props render directly on host Text', (
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
+
+ >
`);
});
@@ -355,25 +379,27 @@ test('React Native API assumption: aria-* props render directly on host TextInpu
);
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
+
+ >
`);
});
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..479fa4682 100644
--- a/src/__tests__/render.test.tsx
+++ b/src/__tests__/render.test.tsx
@@ -1,8 +1,7 @@
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
-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 +73,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();
@@ -160,13 +123,15 @@ test('renders options.wrapper around node', () => {
expect(screen.getByTestId('wrapper')).toBeTruthy();
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
-
+ testID="wrapper"
+ >
+
+
+ >
`);
});
@@ -184,15 +149,17 @@ test('renders options.wrapper around updated node', () => {
expect(screen.getByTestId('wrapper')).toBeTruthy();
expect(screen.toJSON()).toMatchInlineSnapshot(`
-
+ <>
-
+ testID="wrapper"
+ >
+
+
+ >
`);
});
@@ -200,26 +167,8 @@ test('returns host root', () => {
render();
expect(screen.root).toBeDefined();
- expect(screen.root.type).toBe('View');
- expect(screen.root.props.testID).toBe('inner');
-});
-
-test('returns composite UNSAFE_root', () => {
- 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.root?.type).toBe('View');
+ expect(screen.root?.props.testID).toBe('inner');
});
test('RenderAPI type', () => {
@@ -233,13 +182,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..f2c6ec83e 100644
--- a/src/event-handler.ts
+++ b/src/event-handler.ts
@@ -1,30 +1,30 @@
-import type { ReactTestInstance } from 'react-test-renderer';
+export type EventHandler = (...args: unknown[]) => unknown;
export type EventHandlerOptions = {
/** Include check for event handler named without adding `on*` prefix. */
loose?: boolean;
};
-export function getEventHandler(
- element: ReactTestInstance,
+export function getEventHandlerFromProps(
+ props: Record,
eventName: string,
options?: EventHandlerOptions,
-) {
+): EventHandler | undefined {
const handlerName = getEventHandlerName(eventName);
- if (typeof element.props[handlerName] === 'function') {
- return element.props[handlerName];
+ if (typeof props[handlerName] === 'function') {
+ return props[handlerName] as EventHandler;
}
- if (options?.loose && typeof element.props[eventName] === 'function') {
- return element.props[eventName];
+ if (options?.loose && typeof props[eventName] === 'function') {
+ return props[eventName] as EventHandler;
}
- if (typeof element.props[`testOnly_${handlerName}`] === 'function') {
- return element.props[`testOnly_${handlerName}`];
+ if (typeof props[`testOnly_${handlerName}`] === 'function') {
+ return props[`testOnly_${handlerName}`] as EventHandler;
}
- if (options?.loose && typeof element.props[`testOnly_${eventName}`] === 'function') {
- return element.props[`testOnly_${eventName}`];
+ if (options?.loose && typeof props[`testOnly_${eventName}`] === 'function') {
+ return props[`testOnly_${eventName}`] as EventHandler;
}
return undefined;
diff --git a/src/fire-event.ts b/src/fire-event.ts
index a843fad09..921d00cd3 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -5,26 +5,19 @@ 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 type { EventHandler } from './event-handler';
+import { getEventHandlerFromProps } from './event-handler';
+import { isElementMounted } 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';
-type EventHandler = (...args: unknown[]) => unknown;
-
-export function isTouchResponder(element: ReactTestInstance) {
- if (!isHostElement(element)) {
- return false;
- }
-
- return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element);
-}
+type Fiber = HostElement['unstable_fiber'];
/**
* List of events affected by `pointerEvents` prop.
@@ -32,7 +25,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.
@@ -49,10 +50,50 @@ const textInputEventsIgnoringEditableProp = new Set([
'onScroll',
]);
+function findEventHandler(
+ element: HostElement,
+ eventName: string,
+ nearestTouchResponder?: HostElement,
+): EventHandler | null {
+ const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
+
+ const handler =
+ getEventHandlerFromProps(element.props, eventName, { loose: true }) ??
+ findEventHandlerFromRelatedFibers(element.unstable_fiber, eventName);
+ if (handler && isEventEnabled(element, eventName, touchResponder)) {
+ return handler;
+ }
+
+ if (element.parent === null) {
+ return null;
+ }
+
+ return findEventHandler(element.parent, eventName, touchResponder);
+}
+
+function findEventHandlerFromRelatedFibers(fiber: Fiber, eventName: string): EventHandler | null {
+ // Container fibers have memoizedProps set to null
+ if (!fiber?.memoizedProps) {
+ return null;
+ }
+
+ const handler = getEventHandlerFromProps(fiber.memoizedProps, eventName, { loose: true });
+ if (handler) {
+ return handler;
+ }
+
+ // No parent fiber or we reached another host element
+ if (fiber.return === null || typeof fiber.return.type === 'string') {
+ return null;
+ }
+
+ return findEventHandlerFromRelatedFibers(fiber.return, eventName);
+}
+
export function isEventEnabled(
- element: ReactTestInstance,
+ element: HostElement,
eventName: string,
- nearestTouchResponder?: ReactTestInstance,
+ nearestTouchResponder?: HostElement,
) {
if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) {
return (
@@ -74,22 +115,8 @@ export function isEventEnabled(
return touchStart === undefined && touchMove === undefined;
}
-function findEventHandler(
- element: ReactTestInstance,
- eventName: string,
- nearestTouchResponder?: ReactTestInstance,
-): EventHandler | null {
- const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
-
- const handler = getEventHandler(element, eventName, { loose: true });
- 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) {
- return null;
- }
-
- return findEventHandler(element.parent, eventName, touchResponder);
+export function isTouchResponder(element: HostElement) {
+ return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element);
}
// String union type of keys of T that start with on, stripped of 'on'
@@ -105,7 +132,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 +152,13 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un
return returnValue;
}
-fireEvent.press = (element: ReactTestInstance, ...data: unknown[]) =>
+fireEvent.press = (element: HostElement, ...data: unknown[]) =>
fireEvent(element, 'press', ...data);
-fireEvent.changeText = (element: ReactTestInstance, ...data: unknown[]) =>
+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 +171,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,19 +150,17 @@ 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 labelElement = findAll(
- rootElement,
- (node) => isHostElement(node) && node.props.nativeID === labelElementId,
- { includeHiddenElements: true },
- );
+ const container = getContainerElement(element);
+ const labelElement = findAll(container, (node) => node.props.nativeID === labelElementId, {
+ includeHiddenElements: true,
+ });
if (labelElement.length > 0) {
return getTextContent(labelElement[0]);
}
@@ -182,12 +180,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 +201,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 +211,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 +237,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..22829fdcd 100644
--- a/src/helpers/component-tree.ts
+++ b/src/helpers/component-tree.ts
@@ -1,102 +1,43 @@
-import type { ReactTestInstance } from 'react-test-renderer';
+import 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 isValidHostElement(element?: HostElement | null): element is HostElement {
+ return typeof element?.type === 'string' && element.props !== null;
}
-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..daaf48df5 100644
--- a/src/helpers/find-all.ts
+++ b/src/helpers/find-all.ts
@@ -1,9 +1,7 @@
-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';
interface FindAllOptions {
/** Match elements hidden from accessibility */
@@ -17,11 +15,11 @@ interface FindAllOptions {
}
export function findAll(
- root: ReactTestInstance,
- predicate: (element: ReactTestInstance) => boolean,
+ root: HostElement,
+ predicate: (element: HostElement) => boolean,
options?: FindAllOptions,
-): HostTestInstance[] {
- const results = findAllInternal(root, predicate, options);
+): HostElement[] {
+ const results = root.findAll(predicate, options);
const includeHiddenElements =
options?.includeHiddenElements ?? options?.hidden ?? getConfig()?.defaultIncludeHiddenElements;
@@ -30,39 +28,6 @@ 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,
- options?: FindAllOptions,
-): HostTestInstance[] {
- const results: HostTestInstance[] = [];
-
- // Match descendants first but do not add them to results yet.
- const matchingDescendants: HostTestInstance[] = [];
- root.children.forEach((child) => {
- if (typeof child === 'string') {
- return;
- }
- matchingDescendants.push(...findAllInternal(child, predicate, options));
- });
-
- if (
- // When matchDeepestOnly = true: add current element only if no descendants match
- (!options?.matchDeepestOnly || matchingDescendants.length === 0) &&
- isHostElement(root) &&
- predicate(root)
- ) {
- results.push(root);
- }
-
- // Add matching descendants after element to preserve original tree walk order.
- results.push(...matchingDescendants);
-
- return results;
-}
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..25d41f62a 100644
--- a/src/helpers/host-component-names.ts
+++ b/src/helpers/host-component-names.ts
@@ -1,8 +1,6 @@
-import type { ReactTestInstance } from 'react-test-renderer';
+import type { HostElement } from 'universal-test-renderer';
-import type { HostTestInstance } 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 +11,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 element != null && 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 element != null && 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 element != null && 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 element != null && 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 element != null && 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 element != null && 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-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx
index 2b0b3f22a..4d92b8f29 100644
--- a/src/matchers/__tests__/to-be-checked.test.tsx
+++ b/src/matchers/__tests__/to-be-checked.test.tsx
@@ -5,7 +5,7 @@ import { render, screen } from '../..';
function renderViewsWithRole(role: AccessibilityRole) {
render(
- <>
+
- >,
+ ,
);
}
test('toBeCheck() with Switch', () => {
render(
- <>
+
- >,
+ ,
);
const checked = screen.getByTestId('checked');
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-be-visible.test.tsx b/src/matchers/__tests__/to-be-visible.test.tsx
index be4f82da6..8e90f77ef 100644
--- a/src/matchers/__tests__/to-be-visible.test.tsx
+++ b/src/matchers/__tests__/to-be-visible.test.tsx
@@ -239,20 +239,20 @@ test('toBeVisible() on null elements', () => {
test('toBeVisible() on non-React elements', () => {
expect(() => expect({ name: 'Non-React element' }).not.toBeVisible())
.toThrowErrorMatchingInlineSnapshot(`
- "expect(received).not.toBeVisible()
+ "expect(received).not.toBeVisible()
- received value must be a host element.
- Received has type: object
- Received has value: {"name": "Non-React element"}"
- `);
+ received value must be a host element.
+ Received has type: object
+ Received has value: {"name": "Non-React element"}"
+ `);
expect(() => expect(true).not.toBeVisible()).toThrowErrorMatchingInlineSnapshot(`
- "expect(received).not.toBeVisible()
+ "expect(received).not.toBeVisible()
- received value must be a host element.
- Received has type: boolean
- Received has value: true"
- `);
+ received value must be a host element.
+ Received has type: boolean
+ Received has value: true"
+ `);
});
test('toBeVisible() does not throw on invalid style', () => {
diff --git a/src/matchers/__tests__/to-contain-element.test.tsx b/src/matchers/__tests__/to-contain-element.test.tsx
index 9b6aa5a25..129745298 100644
--- a/src/matchers/__tests__/to-contain-element.test.tsx
+++ b/src/matchers/__tests__/to-contain-element.test.tsx
@@ -110,12 +110,12 @@ test('toContainElement() handles non-element container', () => {
`);
expect(() => expect(true).not.toContainElement(view)).toThrowErrorMatchingInlineSnapshot(`
- "expect(received).not.toContainElement()
+ "expect(received).not.toContainElement()
- received value must be a host element.
- Received has type: boolean
- Received has value: true"
- `);
+ received value must be a host element.
+ Received has type: boolean
+ Received has value: true"
+ `);
});
test('toContainElement() handles non-element element', () => {
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