Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/solid-swans-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/reassure-measure': minor
---

feat: support async `render` and `cleanup` from React Native Testing Library v14.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,10 @@ type Config = {
testingLibrary?:
| 'react-native'
| 'react'
| { render: (component: React.ReactElement<any>) => any; cleanup: () => any };
| {
render: (component: React.ReactElement<any>) => any | Promise<any>;
cleanup: () => void | Promise<void>;
};
};
```

Expand Down
5 changes: 4 additions & 1 deletion docusaurus/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ type Config = {
testingLibrary?:
| 'react-native'
| 'react'
| { render: (component: React.ReactElement<any>) => any; cleanup: () => any };
| {
render: (component: React.ReactElement<any>) => any | Promise<any>;
cleanup: () => void | Promise<void>;
};
};
```

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"devDependencies": {
"@callstack/eslint-config": "^15.0.0",
"@changesets/cli": "^2.29.7",
"babel-jest": "^30.2.0",
"babel-jest": "^30.4.1",
"check-dependency-version-consistency": "^5.0.1",
"danger": "^13.0.5",
"eslint": "^8.57.1",
Expand All @@ -51,7 +51,9 @@
"typescript": "^5.9.3"
},
"resolutions": {
"react-is": "19.1.1"
"react-is": "19.1.1",
"jest-mock": "30.4.1",
"jest-environment-node": "30.4.1"
},
"eslintIgnore": [
"node_modules/",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@
"@types/jest": "^30.0.0",
"@types/react": "~19.1.17",
"@types/yargs": "^17.0.35",
"babel-jest": "^30.2.0",
"babel-jest": "^30.4.1",
"del-cli": "^7.0.0",
"jest": "^30.2.0",
"jest": "^30.4.2",
"prettier": "^3.6.2",
"react-native-builder-bob": "^0.24.0",
"typescript": "^5.9.3"
Expand Down
4 changes: 2 additions & 2 deletions packages/compare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
"@types/jest": "^30.0.0",
"@types/react": "~19.1.17",
"babel-jest": "^30.2.0",
"babel-jest": "^30.4.1",
"del-cli": "^7.0.0",
"jest": "^30.2.0",
"jest": "^30.4.2",
"prettier": "^3.6.2",
"react-native-builder-bob": "^0.24.0",
"typescript": "^5.9.3"
Expand Down
10 changes: 5 additions & 5 deletions packages/measure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"dependencies": {
"@callstack/reassure-logger": "1.4.0",
"mathjs": "^15.1.0",
"pretty-format": "^30.2.0"
"pretty-format": "^30.4.1"
},
"devDependencies": {
"@babel/core": "^7.28.5",
Expand All @@ -48,18 +48,18 @@
"@babel/runtime": "^7.28.4",
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
"@testing-library/react": "^16.3.0",
"@testing-library/react-native": "^13.3.3",
"@testing-library/react-native": "^14.0.0",
"@types/jest": "^30.0.0",
"@types/react": "~19.1.17",
"babel-jest": "^30.2.0",
"babel-jest": "^30.4.1",
"del-cli": "^7.0.0",
"jest": "^30.2.0",
"jest": "^30.4.2",
"prettier": "^3.6.2",
"react": "19.1.1",
"react-native": "0.82.1",
"react-native-builder-bob": "^0.24.0",
"react-test-renderer": "19.1.1",
"strip-ansi": "^6.0.1",
"test-renderer": "1.2.0",
"typescript": "^5.9.3"
},
"peerDependencies": {
Expand Down
80 changes: 35 additions & 45 deletions packages/measure/src/__tests__/measure-renders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { View, Text, Pressable } from 'react-native';
import { fireEvent, screen } from '@testing-library/react-native';
import stripAnsi from 'strip-ansi';
import { configure } from '../config';
import { buildUiToRender, measureRenders } from '../measure-renders';
import { setHasShownFlagsOutput } from '../output';

Expand All @@ -13,6 +14,10 @@ beforeEach(() => {
jest.mocked(realConsole.error).mockRestore?.();
});

afterEach(() => {
configure({ testingLibrary: 'react-native' });
});

test('measureRenders run test given number of times', async () => {
const scenario = jest.fn(() => Promise.resolve(null));
const results = await measureRenders(<View />, { runs: 20, scenario, writeFile: false });
Expand Down Expand Up @@ -59,6 +64,36 @@ test('measureRenders executes setup and cleanup functions for each run', async (
expect(results.counts).toHaveLength(10);
});

test('measureRenders supports async custom render and cleanup functions', async () => {
const calls: string[] = [];
const render = jest.fn(() => {
calls.push('render');
return Promise.resolve({ toJSON: () => null });
});
const cleanup = jest.fn(() => {
calls.push('cleanup');
return Promise.resolve();
});
const scenario = jest.fn(() => {
calls.push('scenario');
return Promise.resolve();
});

configure({ testingLibrary: { render, cleanup } });

await measureRenders(<View />, {
runs: 1,
warmupRuns: 0,
scenario,
writeFile: false,
});

expect(render).toHaveBeenCalledTimes(1);
expect(scenario).toHaveBeenCalledTimes(1);
expect(cleanup).toHaveBeenCalledTimes(1);
expect(calls).toEqual(['render', 'scenario', 'cleanup']);
});

test('measureRenders should log error when running under incorrect node flags', async () => {
jest.spyOn(realConsole, 'error').mockImplementation((message) => {
if (!errorsToIgnore.some((error) => message.includes(error))) {
Expand Down Expand Up @@ -186,51 +221,6 @@ test('measureRenders detects multiple redundant updates', async () => {
expect(results.issues.initialUpdateCount).toBe(0);
});

const AsyncMacroTaskEffect = () => {
const [count, setCount] = React.useState(0);

React.useEffect(() => {
setTimeout(() => setCount(1), 0);
}, []);

return (
<View>
<Text>Count: ${count}</Text>
</View>
);
};

test('ignores async macro-tasks effect', async () => {
const results = await measureRenders(<AsyncMacroTaskEffect />, { writeFile: false });
expect(results.issues.initialUpdateCount).toBe(0);
expect(results.issues.redundantUpdates).toEqual([]);
});

const AsyncMicrotaskEffect = () => {
const [count, setCount] = React.useState(0);

React.useEffect(() => {
const asyncSet = async () => {
await Promise.resolve();
setCount(1);
};

void asyncSet();
}, []);

return (
<View>
<Text>Count: ${count}</Text>
</View>
);
};

test('ignores async micro-tasks effect', async () => {
const results = await measureRenders(<AsyncMicrotaskEffect />, { writeFile: false });
expect(results.issues.initialUpdateCount).toBe(0);
expect(results.issues.redundantUpdates).toEqual([]);
});

function Wrapper({ children }: React.PropsWithChildren<{}>) {
return <View testID="wrapper">{children}</View>;
}
Expand Down
66 changes: 66 additions & 0 deletions packages/measure/src/__tests__/testing-library.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type * as React from 'react';

function loadTestingLibraryModule({ rntl, rtl }: { rntl?: Record<string, unknown>; rtl?: Record<string, unknown> }) {
jest.resetModules();

jest.doMock('@testing-library/react-native', () => {
if (rntl == null) {
throw new Error('Missing RNTL');
}

return rntl;
});

jest.doMock('@testing-library/react', () => {
if (rtl == null) {
throw new Error('Missing RTL');
}

return rtl;
});

const configModule = require('../config') as typeof import('../config');
const testingLibraryModule = require('../testing-library') as typeof import('../testing-library');

return { configModule, testingLibraryModule };
}

afterEach(() => {
jest.dontMock('@testing-library/react-native');
jest.dontMock('@testing-library/react');
jest.resetModules();
});

test('resolveTestingLibrary supports async RNTL render and cleanup', async () => {
const render = jest.fn(() => Promise.resolve('render result'));
const cleanup = jest.fn(() => Promise.resolve(undefined));

const { configModule, testingLibraryModule } = loadTestingLibraryModule({
rntl: { render, cleanup },
});

configModule.configure({ testingLibrary: 'react-native' });
const testingLibrary = testingLibraryModule.resolveTestingLibrary();

await expect(testingLibrary.render({} as React.ReactElement)).resolves.toBe('render result');
await expect(testingLibrary.cleanup()).resolves.toBeUndefined();
expect(render).toHaveBeenCalledTimes(1);
expect(cleanup).toHaveBeenCalledTimes(1);
});

test('resolveTestingLibrary supports sync RNTL render and cleanup', () => {
const render = jest.fn(() => 'render result');
const cleanup = jest.fn();

const { configModule, testingLibraryModule } = loadTestingLibraryModule({
rntl: { render, cleanup },
});

configModule.configure({ testingLibrary: 'react-native' });
const testingLibrary = testingLibraryModule.resolveTestingLibrary();

expect(testingLibrary.render({} as React.ReactElement)).toBe('render result');
expect(testingLibrary.cleanup()).toBeUndefined();
expect(render).toHaveBeenCalledTimes(1);
expect(cleanup).toHaveBeenCalledTimes(1);
});
4 changes: 2 additions & 2 deletions packages/measure/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type TestingLibrary = 'react' | 'react-native' | { render: Render; cleanup: Cleanup };

export type Render = (component: React.ReactElement<any>) => any;
export type Cleanup = () => void;
export type Render = (component: React.ReactElement<any>) => any | Promise<any>;
export type Cleanup = () => void | Promise<void>;

type Config = {
runs: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/measure/src/measure-renders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ async function measureRendersInternal(
};

const uiToRender = buildUiToRender(ui, handleRender, options?.wrapper);
renderResult = render(uiToRender);
renderResult = await render(uiToRender);
captureRenderDetails();

if (scenario) {
await scenario(renderResult);
}

cleanup();
await cleanup();
global.gc?.();

await options?.afterEach?.();
Expand Down
4 changes: 2 additions & 2 deletions packages/measure/src/redundant-renders.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactTestRendererJSON } from 'react-test-renderer';
import type { JsonElement } from 'test-renderer';
import { format as prettyFormat, plugins } from 'pretty-format';

export type ElementJsonTree = ReactTestRendererJSON | ReactTestRendererJSON[] | null;
export type ElementJsonTree = JsonElement | JsonElement[] | null;

export function detectRedundantUpdates(elementTrees: ElementJsonTree[], initialRenderCount: number): number[] {
const result = [];
Expand Down
5 changes: 4 additions & 1 deletion packages/reassure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,10 @@ type Config = {
testingLibrary?:
| 'react-native'
| 'react'
| { render: (component: React.ReactElement<any>) => any; cleanup: () => any };
| {
render: (component: React.ReactElement<any>) => any | Promise<any>;
cleanup: () => void | Promise<void>;
};
};
```

Expand Down
6 changes: 2 additions & 4 deletions test-apps/native/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
*/

import React from 'react';
import ReactTestRenderer from 'react-test-renderer';
import { render } from '@testing-library/react-native';
import App from '../src/App';

test('renders correctly', async () => {
await ReactTestRenderer.act(() => {
ReactTestRenderer.create(<App />);
});
await render(<App />);
});
7 changes: 3 additions & 4 deletions test-apps/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@
"@react-native/eslint-config": "0.82.1",
"@react-native/metro-config": "0.82.1",
"@react-native/typescript-config": "0.82.1",
"@testing-library/react-native": "^13.3.3",
"@testing-library/react-native": "^14.0.0",
"@types/jest": "^30.0.0",
"@types/react": "~19.1.17",
"@types/react-test-renderer": "~19.1.0",
"eslint": "^8.57.1",
"jest": "^30.2.0",
"jest": "^30.4.2",
"prettier": "^3.6.2",
"react-test-renderer": "19.1.1",
"reassure": "workspace:^",
"test-renderer": "1.2.0",
"typescript": "^5.9.3"
},
"engines": {
Expand Down
4 changes: 2 additions & 2 deletions test-apps/native/src/SlowList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as React from 'react';
import { render, screen } from '@testing-library/react-native';
import { SlowList } from './SlowList';

test('SlowList', () => {
render(<SlowList count={10} />);
test('SlowList', async () => {
await render(<SlowList count={10} />);

const items = screen.getAllByText(/Item/i);
expect(items).toHaveLength(10);
Expand Down
Loading
Loading