diff --git a/src/__tests__/__snapshots__/render.breaking.test.tsx.snap b/src/__tests__/__snapshots__/render.breaking.test.tsx.snap
new file mode 100644
index 000000000..018ae7d63
--- /dev/null
+++ b/src/__tests__/__snapshots__/render.breaking.test.tsx.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`toJSON renders host output 1`] = `
+
+
+ press me
+
+
+`;
diff --git a/src/__tests__/__snapshots__/render.test.tsx.snap b/src/__tests__/__snapshots__/render.test.tsx.snap
index 7b42db7e6..018ae7d63 100644
--- a/src/__tests__/__snapshots__/render.test.tsx.snap
+++ b/src/__tests__/__snapshots__/render.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`toJSON 1`] = `
+exports[`toJSON renders host output 1`] = `
{
+ configureInternal({ useBreakingChanges: true });
+});
+
+const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
+const PLACEHOLDER_CHEF = 'Who inspected freshness?';
+const INPUT_FRESHNESS = 'Custom Freshie';
+const INPUT_CHEF = 'I inspected freshie';
+const DEFAULT_INPUT_CHEF = 'What did you inspect?';
+const DEFAULT_INPUT_CUSTOMER = 'What banana?';
+
+class MyButton extends React.Component {
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+class Banana extends React.Component {
+ state = {
+ fresh: false,
+ };
+
+ componentDidUpdate() {
+ if (this.props.onUpdate) {
+ this.props.onUpdate();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.onUnmount) {
+ this.props.onUnmount();
+ }
+ }
+
+ changeFresh = () => {
+ this.setState((state) => ({
+ fresh: !state.fresh,
+ }));
+ };
+
+ render() {
+ const test = 0;
+ return (
+
+ Is the banana fresh?
+
+ {this.state.fresh ? 'fresh' : 'not fresh'}
+
+
+
+
+
+
+ Change freshness!
+
+ First Text
+ Second Text
+ {test}
+
+ );
+ }
+}
+
+test('UNSAFE_getAllByType, UNSAFE_queryAllByType', () => {
+ const { UNSAFE_getAllByType, UNSAFE_queryAllByType } = render();
+ const [text, status, button] = 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(() => UNSAFE_getAllByType(InExistent)).toThrow('No instances found');
+
+ expect(UNSAFE_queryAllByType(Text)[1]).toBe(status);
+ expect(UNSAFE_queryAllByType(InExistent)).toHaveLength(0);
+});
+
+test('UNSAFE_getByProps, UNSAFE_queryByProps', () => {
+ const { UNSAFE_getByProps, UNSAFE_queryByProps } = render();
+ const primaryType = UNSAFE_getByProps({ type: 'primary' });
+
+ expect(primaryType.props.children).toBe('Change freshness!');
+ expect(() => UNSAFE_getByProps({ type: 'inexistent' })).toThrow(
+ 'No instances found'
+ );
+
+ expect(UNSAFE_queryByProps({ type: 'primary' })).toBe(primaryType);
+ expect(UNSAFE_queryByProps({ type: 'inexistent' })).toBeNull();
+});
+
+test('UNSAFE_getAllByProp, UNSAFE_queryAllByProps', () => {
+ const { UNSAFE_getAllByProps, UNSAFE_queryAllByProps } = render();
+ const primaryTypes = UNSAFE_getAllByProps({ type: 'primary' });
+
+ expect(primaryTypes).toHaveLength(1);
+ expect(() => UNSAFE_getAllByProps({ type: 'inexistent' })).toThrow(
+ 'No instances found'
+ );
+
+ expect(UNSAFE_queryAllByProps({ type: 'primary' })).toEqual(primaryTypes);
+ expect(UNSAFE_queryAllByProps({ type: 'inexistent' })).toHaveLength(0);
+});
+
+test('update', () => {
+ const fn = jest.fn();
+ const { getByText, update, rerender } = render();
+
+ fireEvent.press(getByText('Change freshness!'));
+
+ update();
+ rerender();
+
+ expect(fn).toHaveBeenCalledTimes(3);
+});
+
+test('unmount', () => {
+ const fn = jest.fn();
+ const { unmount } = render();
+ unmount();
+ expect(fn).toHaveBeenCalled();
+});
+
+test('unmount should handle cleanup functions', () => {
+ const cleanup = jest.fn();
+ const Component = () => {
+ React.useEffect(() => cleanup);
+ return null;
+ };
+
+ const { unmount } = render();
+
+ unmount();
+
+ expect(cleanup).toHaveBeenCalledTimes(1);
+});
+
+test('toJSON renders host output', () => {
+ const { toJSON } = render(press me);
+ expect(toJSON()).toMatchSnapshot();
+});
+
+test('renders options.wrapper around node', () => {
+ type WrapperComponentProps = { children: React.ReactNode };
+ const WrapperComponent = ({ children }: WrapperComponentProps) => (
+ {children}
+ );
+
+ const { toJSON, getByTestId } = render(, {
+ wrapper: WrapperComponent,
+ });
+
+ expect(getByTestId('wrapper')).toBeTruthy();
+ expect(toJSON()).toMatchInlineSnapshot(`
+
+
+
+ `);
+});
+
+test('renders options.wrapper around updated node', () => {
+ type WrapperComponentProps = { children: React.ReactNode };
+ const WrapperComponent = ({ children }: WrapperComponentProps) => (
+ {children}
+ );
+
+ const { toJSON, getByTestId, rerender } = render(, {
+ wrapper: WrapperComponent,
+ });
+
+ rerender(
+
+ );
+
+ expect(getByTestId('wrapper')).toBeTruthy();
+ expect(toJSON()).toMatchInlineSnapshot(`
+
+
+
+ `);
+});
+
+test('returns host root', () => {
+ const { root } = render();
+
+ expect(root).toBeDefined();
+ expect(root.type).toBe('View');
+ expect(root.props.testID).toBe('inner');
+});
+
+test('returns composite UNSAFE_root', () => {
+ const { UNSAFE_root } = render();
+
+ expect(UNSAFE_root).toBeDefined();
+ expect(UNSAFE_root.type).toBe(View);
+ expect(UNSAFE_root.props.testID).toBe('inner');
+});
+
+test('container displays deprecation', () => {
+ const view = render();
+
+ expect(() => view.container).toThrowErrorMatchingInlineSnapshot(`
+ "'container' property has been renamed to 'UNSAFE_root'.
+
+ Consider using 'root' property which returns root host element."
+ `);
+ expect(() => screen.container).toThrowErrorMatchingInlineSnapshot(`
+ "'container' property has been renamed to 'UNSAFE_root'.
+
+ Consider using 'root' property which returns root host element."
+ `);
+});
+
+test('RenderAPI type', () => {
+ render() as RenderAPI;
+ expect(true).toBeTruthy();
+});
diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx
index 53a0185c8..145fc6248 100644
--- a/src/__tests__/render.test.tsx
+++ b/src/__tests__/render.test.tsx
@@ -3,6 +3,12 @@ import * as React from 'react';
import { View, Text, TextInput, Pressable, SafeAreaView } from 'react-native';
import { render, fireEvent, RenderAPI } from '..';
+type ConsoleLogMock = jest.Mock;
+
+beforeEach(() => {
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
+});
+
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
const INPUT_FRESHNESS = 'Custom Freshie';
@@ -148,7 +154,7 @@ test('unmount should handle cleanup functions', () => {
expect(cleanup).toHaveBeenCalledTimes(1);
});
-test('toJSON', () => {
+test('toJSON renders host output', () => {
const { toJSON } = render(press me);
expect(toJSON()).toMatchSnapshot();
@@ -204,9 +210,32 @@ test('renders options.wrapper around updated node', () => {
`);
});
+test('returns host root', () => {
+ const { root } = render();
+
+ expect(root).toBeDefined();
+ expect(root.type).toBe('View');
+ expect(root.props.testID).toBe('inner');
+});
+
+test('returns composite UNSAFE_root', () => {
+ const { UNSAFE_root } = render();
+
+ expect(UNSAFE_root).toBeDefined();
+ expect(UNSAFE_root.type).toBe(View);
+ expect(UNSAFE_root.props.testID).toBe('inner');
+});
+
test('returns container', () => {
const { container } = render();
+ const mockCalls = (console.warn as any as ConsoleLogMock).mock.calls;
+ expect(mockCalls[0][0]).toMatchInlineSnapshot(`
+ "'container' property is deprecated and has been renamed to 'UNSAFE_root'.
+
+ Consider using 'root' property which returns root host element."
+ `);
+
expect(container).toBeDefined();
// `View` composite component is returned. This behavior will break if we
// start returning only host components.
@@ -214,7 +243,7 @@ test('returns container', () => {
expect(container.props.testID).toBe('inner');
});
-test('returns wrapped component as container', () => {
+test('returns wrapper component as container', () => {
type WrapperComponentProps = { children: React.ReactNode };
const WrapperComponent = ({ children }: WrapperComponentProps) => (
{children}
diff --git a/src/__tests__/screen.test.tsx b/src/__tests__/screen.test.tsx
index 10f1751d4..f13685a51 100644
--- a/src/__tests__/screen.test.tsx
+++ b/src/__tests__/screen.test.tsx
@@ -52,6 +52,10 @@ test('screen works with nested re-mounting rerender', () => {
});
test('screen throws without render', () => {
+ expect(() => screen.root).toThrow('`render` method has not been called');
+ expect(() => screen.UNSAFE_root).toThrow(
+ '`render` method has not been called'
+ );
expect(() => screen.container).toThrow('`render` method has not been called');
expect(() => screen.debug()).toThrow('`render` method has not been called');
expect(() => screen.debug.shallow()).toThrow(
diff --git a/src/queries/__tests__/displayValue.breaking.test.tsx b/src/queries/__tests__/displayValue.breaking.test.tsx
index 1bcb79b88..01949d375 100644
--- a/src/queries/__tests__/displayValue.breaking.test.tsx
+++ b/src/queries/__tests__/displayValue.breaking.test.tsx
@@ -1,3 +1,5 @@
+/** This is a copy of regular tests with `useBreakingChanges` flag turned on. */
+
import * as React from 'react';
import { View, TextInput } from 'react-native';
diff --git a/src/queries/__tests__/placeholderText.breaking.test.tsx b/src/queries/__tests__/placeholderText.breaking.test.tsx
index c3a35fb0d..ab2c82264 100644
--- a/src/queries/__tests__/placeholderText.breaking.test.tsx
+++ b/src/queries/__tests__/placeholderText.breaking.test.tsx
@@ -1,3 +1,5 @@
+/** This is a copy of regular tests with `useBreakingChanges` flag turned on. */
+
import * as React from 'react';
import { View, TextInput } from 'react-native';
import { render } from '../..';
diff --git a/src/render.tsx b/src/render.tsx
index b60d15139..9fac09ee3 100644
--- a/src/render.tsx
+++ b/src/render.tsx
@@ -10,6 +10,7 @@ import { getQueriesForElement } from './within';
import { setRenderResult, screen } from './screen';
import { validateStringsRenderedWithinText } from './helpers/stringValidation';
import { getConfig } from './config';
+import { getHostChildren } from './helpers/component-tree';
import { configureHostComponentNamesIfNeeded } from './helpers/host-component-names';
export type RenderOptions = {
@@ -103,10 +104,28 @@ function buildRenderResult(
...getQueriesForElement(instance),
update,
unmount,
- container: instance,
rerender: update, // alias for `update`
toJSON: renderer.toJSON,
debug: debug(instance, renderer),
+ get root() {
+ return getHostChildren(instance)[0];
+ },
+ UNSAFE_root: instance,
+ get container() {
+ if (!getConfig().useBreakingChanges) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ "'container' property is deprecated and has been renamed to 'UNSAFE_root'.\n\n" +
+ "Consider using 'root' property which returns root host element."
+ );
+ return instance;
+ }
+
+ throw new Error(
+ "'container' property has been renamed to 'UNSAFE_root'.\n\n" +
+ "Consider using 'root' property which returns root host element."
+ );
+ },
};
setRenderResult(result);
diff --git a/src/screen.ts b/src/screen.ts
index 34a1504d5..9de2c5465 100644
--- a/src/screen.ts
+++ b/src/screen.ts
@@ -16,6 +16,12 @@ const defaultScreen: RenderResult = {
get container(): ReactTestInstance {
throw new Error(SCREEN_ERROR);
},
+ get root(): ReactTestInstance {
+ throw new Error(SCREEN_ERROR);
+ },
+ get UNSAFE_root(): ReactTestInstance {
+ throw new Error(SCREEN_ERROR);
+ },
debug: notImplementedDebug,
update: notImplemented,
unmount: notImplemented,
diff --git a/typings/index.flow.js b/typings/index.flow.js
index 112609d60..16736cf28 100644
--- a/typings/index.flow.js
+++ b/typings/index.flow.js
@@ -436,6 +436,8 @@ declare module '@testing-library/react-native' {
unmount(nextElement?: React.Element): void;
toJSON(): ReactTestRendererJSON[] | ReactTestRendererJSON | null;
debug: Debug;
+ root: ReactTestInstance;
+ UNSAFE_root: ReactTestInstance;
container: ReactTestInstance;
}
diff --git a/website/docs/API.md b/website/docs/API.md
index a859bd008..3f395fa23 100644
--- a/website/docs/API.md
+++ b/website/docs/API.md
@@ -19,7 +19,8 @@ title: API
- [`mapProps` option](#mapprops-option)
- [`debug.shallow`](#debugshallow)
- [`toJSON`](#tojson)
- - [`container`](#container)
+ - [`root`](#root)
+ - [`UNSAFE_root`](#unsafe_root)
- [`screen`](#screen)
- [`cleanup`](#cleanup)
- [`fireEvent`](#fireevent)
@@ -251,13 +252,31 @@ toJSON(): ReactTestRendererJSON | null
Get the rendered component JSON representation, e.g. for snapshot testing.
-### `container`
+### `root`
```ts
-container: ReactTestInstance;
+root: ReactTestInstance;
```
-A reference to the rendered root element.
+Returns the rendered root [host element](testing-env#host-and-composite-components).
+
+This API is primarily useful in component tests, as it allows you to access root host view without using `*ByTestId` queries or similar methods.
+
+### `UNSAFE_root`
+
+```ts
+UNSAFE_root: ReactTestInstance;
+```
+
+Returns the rendered [composite root element](testing-env#host-and-composite-components).
+
+:::caution
+This API typically will return a composite view which goes against recommended testing practices. This API is primarily available for legacy test suites that rely on such testing.
+:::
+
+:::note
+This API has been previously named `container` for compatibility with [React Testing Library](https://testing-library.com/docs/react-testing-library/api#container-1). However, despite the same name, the actual behavior has been signficantly different, hence the name change to `UNSAFE_root`.
+:::
## `screen`
diff --git a/website/docs/Queries.md b/website/docs/Queries.md
index fa539ec48..e40adb46b 100644
--- a/website/docs/Queries.md
+++ b/website/docs/Queries.md
@@ -39,7 +39,7 @@ title: Queries
## Variants
-> `getBy` queries are shown by default in the [query documentation](#queries)
+> `getBy*` queries are shown by default in the [query documentation](#queries)
> below.
### getBy
@@ -60,22 +60,22 @@ title: Queries
### findBy
-`findBy` queries return a promise which resolves when a matching element is found. The promise is rejected if no elements match or if more than one match is found after a default timeout of 1000 ms. If you need to find more than one element, then use `findAllBy`.
+`findBy*` queries return a promise which resolves when a matching element is found. The promise is rejected if no elements match or if more than one match is found after a default timeout of 1000 ms. If you need to find more than one element, then use `findAllBy*`.
### findAllBy
-`findAllBy` queries return a promise which resolves to an array of matching elements. The promise is rejected if no elements match after a default timeout of 1000 ms.
+`findAllBy*` queries return a promise which resolves to an array of matching elements. The promise is rejected if no elements match after a default timeout of 1000 ms.
:::info
-`findBy` and `findAllBy` queries accept optional `waitForOptions` object argument which can contain `timeout`, `interval` and `onTimeout` properies which have the same meaning as respective options for [`waitFor`](api#waitfor) function.
+`findBy*` and `findAllBy*` queries accept optional `waitForOptions` object argument which can contain `timeout`, `interval` and `onTimeout` properies which have the same meaning as respective options for [`waitFor`](api#waitfor) function.
:::
:::info
-In cases when your `findBy` and `findAllBy` queries throw when not able to find matching elements it is useful to pass `onTimeout: () => { screen.debug(); }` callback using `waitForOptions` parameter.
+In cases when your `findBy*` and `findAllBy*` queries throw when not able to find matching elements it is useful to pass `onTimeout: () => { screen.debug(); }` callback using `waitForOptions` parameter.
:::
:::info
-In order to properly use `findBy` and `findAllBy` queries you need at least React >=16.9.0 (featuring async `act`) or React Native >=0.61 (which comes with React >=16.9.0).
+In order to properly use `findBy*` and `findAllBy*` queries you need at least React >=16.9.0 (featuring async `act`) or React Native >=0.61 (which comes with React >=16.9.0).
:::
## Queries