diff --git a/packages/measure/package.json b/packages/measure/package.json
index 7f8d33ec..39dd69ea 100644
--- a/packages/measure/package.json
+++ b/packages/measure/package.json
@@ -48,7 +48,7 @@
"@babel/runtime": "^7.26.9",
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
"@testing-library/react": "^16.2.0",
- "@testing-library/react-native": "^13.2.0",
+ "@testing-library/react-native": "^13.3.0",
"@types/jest": "^30.0.0",
"@types/react": "^19.0.0",
"babel-jest": "^30.0.2",
@@ -63,8 +63,14 @@
"typescript": "^5.8.2"
},
"peerDependencies": {
+ "@react-native/testing-library": "^13.3.0",
"react": ">=18.0.0"
},
+ "peerDependenciesMeta": {
+ "@react-native/testing-library": {
+ "optional": true
+ }
+ },
"react-native-builder-bob": {
"source": "src",
"output": "lib",
diff --git a/packages/measure/src/__tests__/measure-renders.test.tsx b/packages/measure/src/__tests__/measure-renders.test.tsx
index 1a75bef4..be6de0f9 100644
--- a/packages/measure/src/__tests__/measure-renders.test.tsx
+++ b/packages/measure/src/__tests__/measure-renders.test.tsx
@@ -2,7 +2,8 @@ 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 { buildUiToRender, measureRenders } from '../measure-renders';
+import { measureRenders } from '../measure-renders';
+import { buildUiToRender } from '../measure-renders-common';
import { setHasShownFlagsOutput } from '../output';
const errorsToIgnore = ['❌ Measure code is running under incorrect Node.js configuration.'];
@@ -225,9 +226,9 @@ const AsyncMicrotaskEffect = () => {
);
};
-test('ignores async micro-tasks effect', async () => {
+test('does not ignore async micro-tasks effect', async () => {
const results = await measureRenders(, { writeFile: false });
- expect(results.issues.initialUpdateCount).toBe(0);
+ expect(results.issues.initialUpdateCount).toBe(1);
expect(results.issues.redundantUpdates).toEqual([]);
});
diff --git a/packages/measure/src/index.ts b/packages/measure/src/index.ts
index c0a1899f..99e67dba 100644
--- a/packages/measure/src/index.ts
+++ b/packages/measure/src/index.ts
@@ -2,7 +2,7 @@ export { configure, resetToDefaults } from './config';
export { measureRenders, measurePerformance } from './measure-renders';
export { measureFunction } from './measure-function';
export { measureAsyncFunction } from './measure-async-function';
-export type { MeasureRendersOptions } from './measure-renders';
+export type { MeasureRendersOptions } from './measure-renders-common';
export type { MeasureFunctionOptions } from './measure-function';
export type { MeasureAsyncFunctionOptions } from './measure-async-function';
export type { MeasureType, MeasureResults } from './types';
diff --git a/packages/measure/src/measure-renders-common.tsx b/packages/measure/src/measure-renders-common.tsx
new file mode 100644
index 00000000..b1a21edb
--- /dev/null
+++ b/packages/measure/src/measure-renders-common.tsx
@@ -0,0 +1,116 @@
+import * as React from 'react';
+import * as logger from '@callstack/reassure-logger';
+import { config } from './config';
+import { RunResult, processRunResults } from './measure-helpers';
+import { showFlagsOutputIfNeeded } from './output';
+import { applyRenderPolyfills, revertRenderPolyfills } from './polyfills';
+import { ElementJsonTree, detectRedundantUpdates } from './redundant-renders';
+import { resolveTestingLibrary, getTestingLibrary } from './testing-library';
+import type { MeasureRendersResults } from './types';
+
+logger.configure({
+ verbose: process.env.REASSURE_VERBOSE === 'true' || process.env.REASSURE_VERBOSE === '1',
+ silent: process.env.REASSURE_SILENT === 'true' || process.env.REASSURE_SILENT === '1',
+});
+
+export interface MeasureRendersOptions {
+ runs?: number;
+ warmupRuns?: number;
+ removeOutliers?: boolean;
+ wrapper?: React.ComponentType<{ children: React.ReactElement }>;
+ scenario?: (screen: any) => Promise;
+ writeFile?: boolean;
+ beforeEach?: () => Promise | void;
+ afterEach?: () => Promise | void;
+}
+
+export async function measureRendersInternal(
+ ui: React.ReactElement,
+ options?: MeasureRendersOptions
+): Promise {
+ const runs = options?.runs ?? config.runs;
+ const scenario = options?.scenario;
+ const warmupRuns = options?.warmupRuns ?? config.warmupRuns;
+ const removeOutliers = options?.removeOutliers ?? config.removeOutliers;
+
+ const { render, cleanup } = resolveTestingLibrary();
+ const testingLibrary = getTestingLibrary();
+
+ showFlagsOutputIfNeeded();
+ applyRenderPolyfills();
+
+ const runResults: RunResult[] = [];
+ const renderJsonTrees: ElementJsonTree[] = [];
+ let initialRenderCount = 0;
+
+ for (let iteration = 0; iteration < runs + warmupRuns; iteration += 1) {
+ await options?.beforeEach?.();
+
+ let duration = 0;
+ let count = 0;
+ let renderResult: any = null;
+
+ const captureRenderDetails = () => {
+ // We capture render details only on the first run
+ if (iteration !== 0) {
+ return;
+ }
+
+ // Initial render did not finish yet, so there is no "render" result yet and we cannot analyze the element tree.
+ if (renderResult == null) {
+ initialRenderCount += 1;
+ return;
+ }
+
+ if (testingLibrary === 'react-native') {
+ renderJsonTrees.push(renderResult.toJSON());
+ }
+ };
+
+ const handleRender = (_id: string, _phase: string, actualDuration: number) => {
+ duration += actualDuration;
+ count += 1;
+
+ captureRenderDetails();
+ };
+
+ const uiToRender = buildUiToRender(ui, handleRender, options?.wrapper);
+ renderResult = render(uiToRender);
+ captureRenderDetails();
+
+ if (scenario) {
+ await scenario(renderResult);
+ }
+
+ cleanup();
+ global.gc?.();
+
+ await options?.afterEach?.();
+
+ runResults.push({ duration, count });
+ }
+
+ revertRenderPolyfills();
+
+ return {
+ ...processRunResults(runResults, { warmupRuns, removeOutliers }),
+ issues: {
+ initialUpdateCount: initialRenderCount - 1,
+ redundantUpdates: detectRedundantUpdates(renderJsonTrees, initialRenderCount),
+ },
+ };
+}
+
+export function buildUiToRender(
+ ui: React.ReactElement,
+ onRender: React.ProfilerOnRenderCallback,
+ Wrapper?: React.ComponentType<{ children: React.ReactElement }>
+) {
+ const uiWithProfiler = (
+
+ {ui}
+
+ );
+
+ return Wrapper ? {uiWithProfiler} : uiWithProfiler;
+}
diff --git a/packages/measure/src/measure-renders-native.tsx b/packages/measure/src/measure-renders-native.tsx
new file mode 100644
index 00000000..d5ada45e
--- /dev/null
+++ b/packages/measure/src/measure-renders-native.tsx
@@ -0,0 +1,103 @@
+import * as React from 'react';
+import * as logger from '@callstack/reassure-logger';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { renderAsync, screen, cleanup } from '@testing-library/react-native';
+import { config } from './config';
+import { RunResult, processRunResults } from './measure-helpers';
+import { showFlagsOutputIfNeeded } from './output';
+import { applyRenderPolyfills, revertRenderPolyfills } from './polyfills';
+import { ElementJsonTree, detectRedundantUpdates } from './redundant-renders';
+import type { MeasureRendersResults } from './types';
+import { MeasureRendersOptions } from './measure-renders-common';
+
+logger.configure({
+ verbose: process.env.REASSURE_VERBOSE === 'true' || process.env.REASSURE_VERBOSE === '1',
+ silent: process.env.REASSURE_SILENT === 'true' || process.env.REASSURE_SILENT === '1',
+});
+
+export async function measureRendersNative(
+ ui: React.ReactElement,
+ options?: MeasureRendersOptions
+): Promise {
+ const runs = options?.runs ?? config.runs;
+ const scenario = options?.scenario;
+ const warmupRuns = options?.warmupRuns ?? config.warmupRuns;
+ const removeOutliers = options?.removeOutliers ?? config.removeOutliers;
+
+ showFlagsOutputIfNeeded();
+ applyRenderPolyfills();
+
+ const runResults: RunResult[] = [];
+ const renderJsonTrees: ElementJsonTree[] = [];
+ let initialRenderCount = 0;
+
+ for (let iteration = 0; iteration < runs + warmupRuns; iteration += 1) {
+ await options?.beforeEach?.();
+
+ let duration = 0;
+ let count = 0;
+ let renderResult: any = null;
+
+ const captureRenderDetails = () => {
+ // We capture render details only on the first run
+ if (iteration !== 0) {
+ return;
+ }
+
+ // Initial render did not finish yet, so there is no "render" result yet and we cannot analyze the element tree.
+ if (renderResult == null) {
+ initialRenderCount += 1;
+ return;
+ }
+
+ renderJsonTrees.push(renderResult.toJSON());
+ };
+
+ const handleRender = (_id: string, _phase: string, actualDuration: number) => {
+ duration += actualDuration;
+ count += 1;
+
+ captureRenderDetails();
+ };
+
+ const uiToRender = buildUiToRender(ui, handleRender, options?.wrapper);
+ renderResult = await renderAsync(uiToRender);
+ captureRenderDetails();
+
+ if (scenario) {
+ await scenario(renderResult);
+ }
+
+ await screen.unmountAsync();
+ cleanup();
+ global.gc?.();
+
+ await options?.afterEach?.();
+
+ runResults.push({ duration, count });
+ }
+
+ revertRenderPolyfills();
+
+ return {
+ ...processRunResults(runResults, { warmupRuns, removeOutliers }),
+ issues: {
+ initialUpdateCount: initialRenderCount - 1,
+ redundantUpdates: detectRedundantUpdates(renderJsonTrees, initialRenderCount),
+ },
+ };
+}
+
+export function buildUiToRender(
+ ui: React.ReactElement,
+ onRender: React.ProfilerOnRenderCallback,
+ Wrapper?: React.ComponentType<{ children: React.ReactElement }>
+) {
+ const uiWithProfiler = (
+
+ {ui}
+
+ );
+
+ return Wrapper ? {uiWithProfiler} : uiWithProfiler;
+}
diff --git a/packages/measure/src/measure-renders.tsx b/packages/measure/src/measure-renders.tsx
index 6e746d18..68df741b 100644
--- a/packages/measure/src/measure-renders.tsx
+++ b/packages/measure/src/measure-renders.tsx
@@ -1,34 +1,20 @@
-import * as React from 'react';
import * as logger from '@callstack/reassure-logger';
-import { config } from './config';
-import { RunResult, processRunResults } from './measure-helpers';
-import { showFlagsOutputIfNeeded, writeTestStats } from './output';
-import { applyRenderPolyfills, revertRenderPolyfills } from './polyfills';
-import { ElementJsonTree, detectRedundantUpdates } from './redundant-renders';
-import { resolveTestingLibrary, getTestingLibrary } from './testing-library';
-import type { MeasureRendersResults } from './types';
-
-logger.configure({
- verbose: process.env.REASSURE_VERBOSE === 'true' || process.env.REASSURE_VERBOSE === '1',
- silent: process.env.REASSURE_SILENT === 'true' || process.env.REASSURE_SILENT === '1',
-});
-
-export interface MeasureRendersOptions {
- runs?: number;
- warmupRuns?: number;
- removeOutliers?: boolean;
- wrapper?: React.ComponentType<{ children: React.ReactElement }>;
- scenario?: (screen: any) => Promise;
- writeFile?: boolean;
- beforeEach?: () => Promise | void;
- afterEach?: () => Promise | void;
-}
+import { measureRendersInternal, MeasureRendersOptions } from './measure-renders-common';
+import { measureRendersNative } from './measure-renders-native';
+import { writeTestStats } from './output';
+import { getTestingLibrary } from './testing-library';
+import { MeasureRendersResults } from './types';
export async function measureRenders(
ui: React.ReactElement,
options?: MeasureRendersOptions
): Promise {
- const stats = await measureRendersInternal(ui, options);
+ let stats: MeasureRendersResults;
+ if (getTestingLibrary() === 'react-native') {
+ stats = await measureRendersNative(ui, options);
+ } else {
+ stats = await measureRendersInternal(ui, options);
+ }
if (options?.writeFile !== false) {
await writeTestStats(stats, 'render');
@@ -50,94 +36,3 @@ export async function measurePerformance(
return await measureRenders(ui, options);
}
-
-async function measureRendersInternal(
- ui: React.ReactElement,
- options?: MeasureRendersOptions
-): Promise {
- const runs = options?.runs ?? config.runs;
- const scenario = options?.scenario;
- const warmupRuns = options?.warmupRuns ?? config.warmupRuns;
- const removeOutliers = options?.removeOutliers ?? config.removeOutliers;
-
- const { render, cleanup } = resolveTestingLibrary();
- const testingLibrary = getTestingLibrary();
-
- showFlagsOutputIfNeeded();
- applyRenderPolyfills();
-
- const runResults: RunResult[] = [];
- const renderJsonTrees: ElementJsonTree[] = [];
- let initialRenderCount = 0;
-
- for (let iteration = 0; iteration < runs + warmupRuns; iteration += 1) {
- await options?.beforeEach?.();
-
- let duration = 0;
- let count = 0;
- let renderResult: any = null;
-
- const captureRenderDetails = () => {
- // We capture render details only on the first run
- if (iteration !== 0) {
- return;
- }
-
- // Initial render did not finish yet, so there is no "render" result yet and we cannot analyze the element tree.
- if (renderResult == null) {
- initialRenderCount += 1;
- return;
- }
-
- if (testingLibrary === 'react-native') {
- renderJsonTrees.push(renderResult.toJSON());
- }
- };
-
- const handleRender = (_id: string, _phase: string, actualDuration: number) => {
- duration += actualDuration;
- count += 1;
-
- captureRenderDetails();
- };
-
- const uiToRender = buildUiToRender(ui, handleRender, options?.wrapper);
- renderResult = render(uiToRender);
- captureRenderDetails();
-
- if (scenario) {
- await scenario(renderResult);
- }
-
- cleanup();
- global.gc?.();
-
- await options?.afterEach?.();
-
- runResults.push({ duration, count });
- }
-
- revertRenderPolyfills();
-
- return {
- ...processRunResults(runResults, { warmupRuns, removeOutliers }),
- issues: {
- initialUpdateCount: initialRenderCount - 1,
- redundantUpdates: detectRedundantUpdates(renderJsonTrees, initialRenderCount),
- },
- };
-}
-
-export function buildUiToRender(
- ui: React.ReactElement,
- onRender: React.ProfilerOnRenderCallback,
- Wrapper?: React.ComponentType<{ children: React.ReactElement }>
-) {
- const uiWithProfiler = (
-
- {ui}
-
- );
-
- return Wrapper ? {uiWithProfiler} : uiWithProfiler;
-}
diff --git a/test-apps/native/package.json b/test-apps/native/package.json
index 2ee45c02..5b911a9c 100644
--- a/test-apps/native/package.json
+++ b/test-apps/native/package.json
@@ -24,7 +24,7 @@
"@react-native/eslint-config": "0.78.0",
"@react-native/metro-config": "0.78.0",
"@react-native/typescript-config": "0.78.0",
- "@testing-library/react-native": "^13.2.0",
+ "@testing-library/react-native": "^13.3.0",
"@types/jest": "^30.0.0",
"@types/react": "^19.0.0",
"@types/react-test-renderer": "^19.0.0",
diff --git a/yarn.lock b/yarn.lock
index 2aa8ad67..7a23b7a3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1643,7 +1643,7 @@ __metadata:
"@callstack/reassure-logger": "npm:1.4.0"
"@relmify/jest-serializer-strip-ansi": "npm:^1.0.2"
"@testing-library/react": "npm:^16.2.0"
- "@testing-library/react-native": "npm:^13.2.0"
+ "@testing-library/react-native": "npm:^13.3.0"
"@types/jest": "npm:^30.0.0"
"@types/react": "npm:^19.0.0"
babel-jest: "npm:^30.0.2"
@@ -1659,7 +1659,11 @@ __metadata:
strip-ansi: "npm:^6.0.1"
typescript: "npm:^5.8.2"
peerDependencies:
+ "@react-native/testing-library": ^13.3.0
react: ">=18.0.0"
+ peerDependenciesMeta:
+ "@react-native/testing-library":
+ optional: true
languageName: unknown
linkType: soft
@@ -2317,6 +2321,15 @@ __metadata:
languageName: node
linkType: hard
+"@jest/schemas@npm:30.0.5":
+ version: 30.0.5
+ resolution: "@jest/schemas@npm:30.0.5"
+ dependencies:
+ "@sinclair/typebox": "npm:^0.34.0"
+ checksum: 10c0/449dcd7ec5c6505e9ac3169d1143937e67044ae3e66a729ce4baf31812dfd30535f2b3b2934393c97cfdf5984ff581120e6b38f62b8560c8b5b7cc07f4175f65
+ languageName: node
+ linkType: hard
+
"@jest/schemas@npm:^29.6.3":
version: 29.6.3
resolution: "@jest/schemas@npm:29.6.3"
@@ -3396,13 +3409,13 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/react-native@npm:^13.2.0":
- version: 13.2.0
- resolution: "@testing-library/react-native@npm:13.2.0"
+"@testing-library/react-native@npm:^13.3.0":
+ version: 13.3.2
+ resolution: "@testing-library/react-native@npm:13.3.2"
dependencies:
- chalk: "npm:^4.1.2"
- jest-matcher-utils: "npm:^29.7.0"
- pretty-format: "npm:^29.7.0"
+ jest-matcher-utils: "npm:^30.0.5"
+ picocolors: "npm:^1.1.1"
+ pretty-format: "npm:^30.0.5"
redent: "npm:^3.0.0"
peerDependencies:
jest: ">=29.0.0"
@@ -3412,7 +3425,7 @@ __metadata:
peerDependenciesMeta:
jest:
optional: true
- checksum: 10c0/5ed8e09f82f45c057f12a716008f31abf934e6a3d84955540e2ab96d7534c82b9afdb0af050e986d8b63ae9dd8272f8a752c45ecb847a11e7549f30de3d84427
+ checksum: 10c0/c76baefa200183aa79e45d672370061a7ff8bdb8b45c02fd83c903604c265bae2d4ca194ddb71c90646cc2649eaf0d6e6ffc1ea375823d96e81635423b51e024
languageName: node
linkType: hard
@@ -5698,13 +5711,6 @@ __metadata:
languageName: node
linkType: hard
-"diff-sequences@npm:^29.6.3":
- version: 29.6.3
- resolution: "diff-sequences@npm:29.6.3"
- checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2
- languageName: node
- linkType: hard
-
"dir-glob@npm:^3.0.1":
version: 3.0.1
resolution: "dir-glob@npm:3.0.1"
@@ -8270,15 +8276,15 @@ __metadata:
languageName: node
linkType: hard
-"jest-diff@npm:^29.7.0":
- version: 29.7.0
- resolution: "jest-diff@npm:29.7.0"
+"jest-diff@npm:30.0.5":
+ version: 30.0.5
+ resolution: "jest-diff@npm:30.0.5"
dependencies:
- chalk: "npm:^4.0.0"
- diff-sequences: "npm:^29.6.3"
- jest-get-type: "npm:^29.6.3"
- pretty-format: "npm:^29.7.0"
- checksum: 10c0/89a4a7f182590f56f526443dde69acefb1f2f0c9e59253c61d319569856c4931eae66b8a3790c443f529267a0ddba5ba80431c585deed81827032b2b2a1fc999
+ "@jest/diff-sequences": "npm:30.0.1"
+ "@jest/get-type": "npm:30.0.1"
+ chalk: "npm:^4.1.2"
+ pretty-format: "npm:30.0.5"
+ checksum: 10c0/b218ced37b7676f578ea866762f04caa74901bdcf3f593872aa9a4991a586302651a1d16bb0386772adacc7580a452ec621359af75d733c0b50ea947fe1881d3
languageName: node
linkType: hard
@@ -8407,15 +8413,15 @@ __metadata:
languageName: node
linkType: hard
-"jest-matcher-utils@npm:^29.7.0":
- version: 29.7.0
- resolution: "jest-matcher-utils@npm:29.7.0"
+"jest-matcher-utils@npm:^30.0.5":
+ version: 30.0.5
+ resolution: "jest-matcher-utils@npm:30.0.5"
dependencies:
- chalk: "npm:^4.0.0"
- jest-diff: "npm:^29.7.0"
- jest-get-type: "npm:^29.6.3"
- pretty-format: "npm:^29.7.0"
- checksum: 10c0/0d0e70b28fa5c7d4dce701dc1f46ae0922102aadc24ed45d594dd9b7ae0a8a6ef8b216718d1ab79e451291217e05d4d49a82666e1a3cc2b428b75cd9c933244e
+ "@jest/get-type": "npm:30.0.1"
+ chalk: "npm:^4.1.2"
+ jest-diff: "npm:30.0.5"
+ pretty-format: "npm:30.0.5"
+ checksum: 10c0/231d891b29bfc218f2f5739c10873b6671426e31ad1c5538eed1531e62608fd3f60d32f41821332a6cf41f1614fd37361434c754fdd49c849b35ef2e5156c02e
languageName: node
linkType: hard
@@ -10591,6 +10597,17 @@ __metadata:
languageName: node
linkType: hard
+"pretty-format@npm:30.0.5, pretty-format@npm:^30.0.5":
+ version: 30.0.5
+ resolution: "pretty-format@npm:30.0.5"
+ dependencies:
+ "@jest/schemas": "npm:30.0.5"
+ ansi-styles: "npm:^5.2.0"
+ react-is: "npm:^18.3.1"
+ checksum: 10c0/9f6cf1af5c3169093866c80adbfdad32f69c692b62f24ba3ca8cdec8519336123323f896396f9fa40346a41b197c5f6be15aec4d8620819f12496afaaca93f81
+ languageName: node
+ linkType: hard
+
"pretty-format@npm:^26.6.2":
version: 26.6.2
resolution: "pretty-format@npm:26.6.2"
@@ -10970,7 +10987,7 @@ __metadata:
"@react-native/eslint-config": "npm:0.78.0"
"@react-native/metro-config": "npm:0.78.0"
"@react-native/typescript-config": "npm:0.78.0"
- "@testing-library/react-native": "npm:^13.2.0"
+ "@testing-library/react-native": "npm:^13.3.0"
"@types/jest": "npm:^30.0.0"
"@types/react": "npm:^19.0.0"
"@types/react-test-renderer": "npm:^19.0.0"