Skip to content

Commit

Permalink
refactor: renderer API to be more similar to React DOM
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjastrzebski committed Oct 17, 2024
1 parent cf4b8b7 commit a9b0b44
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 146 deletions.
17 changes: 9 additions & 8 deletions src/__tests__/host-component-names.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
configureHostComponentNamesIfNeeded,
} from '../helpers/host-component-names';
import { act, render } from '..';
import * as internalRenderer from '../renderer/renderer';
import * as rendererModule from '../renderer/renderer';

describe('getHostComponentNames', () => {
test('returns host component names from internal config', () => {
Expand Down Expand Up @@ -102,13 +102,14 @@ describe('configureHostComponentNamesIfNeeded', () => {
});

test('throw an error when auto-detection fails', () => {
let mockRender: jest.SpyInstance;
const result = internalRenderer.render(<View />);
const renderer = rendererModule.createRenderer();
renderer.render(<View />);

mockRender = jest.spyOn(internalRenderer, 'render') as jest.Mock;
mockRender.mockReturnValue({
root: result.root,
});
const mockCreateRenderer = jest
.spyOn(rendererModule, 'createRenderer')
.mockReturnValue(renderer);
// @ts-expect-error
jest.spyOn(renderer, 'render').mockReturnValue(renderer.root);

expect(() => configureHostComponentNamesIfNeeded()).toThrowErrorMatchingInlineSnapshot(`
"Trying to detect host component names triggered the following error:
Expand All @@ -119,6 +120,6 @@ describe('configureHostComponentNamesIfNeeded', () => {
Please check if you are using compatible versions of React Native and React Native Testing Library."
`);

mockRender.mockReset();
mockCreateRenderer.mockReset();
});
});
8 changes: 5 additions & 3 deletions src/__tests__/render-hook.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable jest/no-conditional-expect */
import React, { ReactNode } from 'react';
import * as internalRenderer from '../renderer/renderer';
import * as rendererModule from '../renderer/renderer';
import { renderHook } from '../pure';

test('gives committed result', () => {
Expand Down Expand Up @@ -94,12 +94,14 @@ test('props type is inferred correctly when initial props is explicitly undefine
* we check the count of renders using React Test Renderers.
*/
test('does render only once', () => {
jest.spyOn(internalRenderer, 'render');
const renderer = rendererModule.createRenderer();
const renderSpy = jest.spyOn(renderer, 'render');
jest.spyOn(rendererModule, 'createRenderer').mockReturnValue(renderer);

renderHook(() => {
const [state, setState] = React.useState(1);
return [state, setState];
});

expect(internalRenderer.render).toHaveBeenCalledTimes(1);
expect(renderSpy).toHaveBeenCalledTimes(1);
});
26 changes: 15 additions & 11 deletions src/helpers/host-component-names.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Image, Modal, ScrollView, Switch, Text, TextInput, View } from 'react-n
import { configureInternal, getConfig, HostComponentNames } from '../config';
import { HostElement } from '../renderer/host-element';
import { renderWithAct } from '../render-act';

Check failure on line 5 in src/helpers/host-component-names.tsx

View workflow job for this annotation

GitHub Actions / Lint

'renderWithAct' is defined but never used

Check failure on line 5 in src/helpers/host-component-names.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unable to resolve path to module '../render-act'

Check failure on line 5 in src/helpers/host-component-names.tsx

View workflow job for this annotation

GitHub Actions / Typecheck

Cannot find module '../render-act' or its corresponding type declarations.
import { createRenderer } from '../renderer/renderer';
import act from '../act';

const userConfigErrorMessage = `There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library.`;
Expand All @@ -29,17 +31,19 @@ export function configureHostComponentNamesIfNeeded() {

function detectHostComponentNames(): HostComponentNames {
try {
const renderer = renderWithAct(
<View>
<Text testID="text">Hello</Text>
<TextInput testID="textInput" />
<Image testID="image" />
<Switch testID="switch" />
<ScrollView testID="scrollView" />
<Modal testID="modal" />
</View>,
);

const renderer = createRenderer();
act(() => {
renderer.render(
<View>
<Text testID="text">Hello</Text>
<TextInput testID="textInput" />
<Image testID="image" />
<Switch testID="switch" />
<ScrollView testID="scrollView" />
<Modal testID="modal" />
</View>,
);
});
return {
text: getByTestId(renderer.root, 'text').type as string,
textInput: getByTestId(renderer.root, 'textInput').type as string,
Expand Down
18 changes: 0 additions & 18 deletions src/render-act.ts

This file was deleted.

36 changes: 16 additions & 20 deletions src/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { getConfig } from './config';
import debugDeep, { DebugOptions } from './helpers/debug-deep';
import { configureHostComponentNamesIfNeeded } from './helpers/host-component-names';
import { HostElement } from './renderer/host-element';
import { RenderResult as RendererResult } from './renderer/renderer';
import { renderWithAct } from './render-act';
import { createRenderer, Renderer } from './renderer/renderer';
import { setRenderResult } from './screen';
import { getQueriesForElement } from './within';

export interface RenderOptions {
wrapper?: React.ComponentType<any>;
createNodeMock?: (element: React.ReactElement) => unknown;
isConcurrent?: boolean;
}

export type RenderResult = ReturnType<typeof render>;
Expand Down Expand Up @@ -40,17 +40,24 @@ export function renderInternal<T>(
}

const wrap = (element: React.ReactElement) => (Wrapper ? <Wrapper>{element}</Wrapper> : element);
const renderer = renderWithAct(wrap(component), restOptions);

const renderer = createRenderer(restOptions);
void act(() => {
renderer.render(wrap(component));
});

return buildRenderResult(renderer, wrap);
}

function buildRenderResult(
renderer: RendererResult,
wrap: (element: React.ReactElement) => JSX.Element,
) {
const update = updateWithAct(renderer, wrap);
function buildRenderResult(renderer: Renderer, wrap: (element: React.ReactElement) => JSX.Element) {
const instance = renderer.container ?? renderer.root;

const update = (element: React.ReactElement) => {
void act(() => {
renderer.render(wrap(element));
});
};

const unmount = () => {
void act(() => {
renderer.unmount();
Expand All @@ -76,20 +83,9 @@ function buildRenderResult(
return result;
}

function updateWithAct(
renderer: RendererResult,
wrap: (innerElement: React.ReactElement) => React.ReactElement,
) {
return function (component: React.ReactElement) {
void act(() => {
renderer.update(wrap(component));
});
};
}

export type DebugFunction = (options?: DebugOptions | string) => void;

function debug(renderer: RendererResult): DebugFunction {
function debug(renderer: Renderer): DebugFunction {
function debugImpl(options?: DebugOptions | string) {
const { defaultDebugOptions } = getConfig();
const debugOptions =
Expand Down
112 changes: 56 additions & 56 deletions src/renderer/renderer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,42 @@
import * as React from 'react';
import { View, Text } from 'react-native';
import { render } from './renderer';
import { createRenderer } from './renderer';

function Passthrough({ children }: { children: React.ReactNode }) {
return children;
}

test('renders View', () => {
render(<View />);
expect(true).toBe(true);
const renderer = createRenderer();
renderer.render(<View />);
expect(renderer.toJSON()).toMatchInlineSnapshot(`<View />`);
});

test('renders Text', () => {
render(<Text>Hello world</Text>);
expect(true).toBe(true);
});

test('throws when rendering string outside of Text', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});

expect(() => render(<View>Hello</View>)).toThrowErrorMatchingInlineSnapshot(
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <View> component."`,
);

expect(() => render(<Passthrough>Hello</Passthrough>)).toThrowErrorMatchingInlineSnapshot(
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <ROOT> component."`,
);

expect(() => render(<>Hello</>)).toThrowErrorMatchingInlineSnapshot(
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <ROOT> component."`,
);

jest.restoreAllMocks();
const renderer = createRenderer();
renderer.render(<Text>Hello RNTL!</Text>);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<Text>
Hello RNTL!
</Text>
`);
});

test('implements update()', () => {
const result = render(<View testID="view" />);
expect(result.toJSON()).toMatchInlineSnapshot(`
test('can update rendered element', () => {
const renderer = createRenderer();
renderer.render(<View testID="view" />);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<View
testID="view"
/>
`);

result.update(
renderer.render(
<View testID="view">
<Text>Hello</Text>
</View>,
);
expect(result.toJSON()).toMatchInlineSnapshot(`
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<View
testID="view"
>
Expand All @@ -58,46 +47,57 @@ test('implements update()', () => {
`);
});

test('implements unmount()', () => {
const result = render(<View testID="view" />);
expect(result.toJSON()).toMatchInlineSnapshot(`
test('can unmount renderer element', () => {
const renderer = createRenderer();
renderer.render(<View testID="view" />);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<View
testID="view"
/>
`);

result.unmount();
expect(result.toJSON()).toBeNull();
renderer.unmount();
expect(renderer.toJSON()).toBeNull();
});

test('implements get root()', () => {
const result = render(<View testID="view" />);
expect(result.root).toMatchInlineSnapshot(`
test('returns root view', () => {
const renderer = createRenderer();
renderer.render(<View testID="view" />);
expect(renderer.root).toMatchInlineSnapshot(`
<View
testID="view"
/>
`);
});

test('implements toJSON()', () => {
const result = render(
<View testID="view">
<Text style={{ color: 'blue' }}>Hello</Text>
</View>,
);
expect(result.toJSON()).toMatchInlineSnapshot(`
<View
testID="view"
>
<Text
style={
{
"color": "blue",
}
}
>
Hello
</Text>
</View>
test('returns container view', () => {
const renderer = createRenderer();
renderer.render(<View testID="view" />);
expect(renderer.container).toMatchInlineSnapshot(`
<CONTAINER>
<View
testID="view"
/>
</CONTAINER>
`);
});

test('throws when rendering string outside of Text', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});

expect(() => createRenderer().render(<View>Hello</View>)).toThrowErrorMatchingInlineSnapshot(
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <View> component."`,
);

expect(() =>
createRenderer().render(<Passthrough>Hello</Passthrough>),
).toThrowErrorMatchingInlineSnapshot(
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <ROOT> component."`,
);

expect(() => createRenderer().render(<>Hello</>)).toThrowErrorMatchingInlineSnapshot(
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <ROOT> component."`,
);

jest.restoreAllMocks();
});
Loading

0 comments on commit a9b0b44

Please sign in to comment.