Skip to content

Commit c7479b0

Browse files
Jack Popefacebook-github-bot
Jack Pope
authored andcommittedJan 30, 2024·
Add react-native-test-renderer package (#42644)
Summary: The goal is to provide testing utilities that use Fabric and concurrent rendering by default to support the new RN architecture. Currently most testing is done through ReactTestRenderer, which is an overly-simplified rendering environment, non-concurrent by default, and over exposes internals. A dedicated RN environment in JS can allow for more realistic test execution. This is the initial commit to create the `react-native-test-renderer` package. It currently only offers a simple toJSON() method on the root of a test, which is used for snapshot unit tests. We will be iterating here to add a query interface, event handling, and more. ## Changelog: [GENERAL] [ADDED] - Added react-native-test-renderer package for Fabric rendered integration tests Pull Request resolved: #42644 Test Plan: ``` $> cd packages/react-native-test-renderer $> yarn jest ``` Output: ``` PASS src/renderer/__tests__/render-test.js render toJSON ✓ returns expected JSON output based on renderer component (7 ms) Test Suites: 1 passed, 1 total 1 passed, 1 total Snapshots: 1 passed, 1 total Time: 2.869 s ``` Reviewed By: yungsters Differential Revision: D53183101 Pulled By: jackpope fbshipit-source-id: 8e29ba35f55f6c4eb2613ab106bc669d72f33d1d
1 parent 2f818b4 commit c7479b0

File tree

10 files changed

+502
-0
lines changed

10 files changed

+502
-0
lines changed
 

‎jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = {
3434
'<rootDir>/packages/react-native/sdks',
3535
'<rootDir>/packages/react-native/Libraries/Renderer',
3636
'<rootDir>/packages/rn-tester/e2e',
37+
'<rootDir>/packages/react-native-test-renderer/src',
3738
],
3839
transformIgnorePatterns: ['node_modules/(?!@react-native/)'],
3940
haste: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
module.exports = {
12+
presets: [
13+
['@babel/preset-env', {targets: {node: 'current'}}],
14+
'@babel/preset-flow',
15+
],
16+
plugins: ['@babel/plugin-transform-react-jsx'],
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
module.exports = {
13+
haste: {
14+
defaultPlatform: 'ios',
15+
platforms: ['android', 'ios', 'native'],
16+
},
17+
transform: {
18+
'^.+\\.(js|ts|tsx)$': 'babel-jest',
19+
},
20+
transformIgnorePatterns: [
21+
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)',
22+
],
23+
setupFilesAfterEnv: ['./src/jest/setup-files-after-env'],
24+
testEnvironment: './src/jest/environment',
25+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@react-native/test-renderer",
3+
"private": true,
4+
"version": "0.0.0",
5+
"description": "A Test rendering library for React Native",
6+
"license": "MIT",
7+
"devDependencies": {
8+
"@babel/core": "^7.20.0",
9+
"@babel/plugin-transform-react-jsx": "^7.0.0",
10+
"@babel/preset-env": "^7.20.0",
11+
"@babel/preset-flow": "^7.20.0"
12+
},
13+
"dependencies": {},
14+
"exports": {
15+
".": "./index.js",
16+
"./jest-environment": "./dist/jest-environment/index.js",
17+
"./jest-setup": "./dist/jest-setup/index.js"
18+
},
19+
"peerDependencies": { "jest": "^29.7.0" }
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
const NodeEnv = require('jest-environment-node').TestEnvironment;
13+
14+
module.exports = class ReactNativeEnvironment extends NodeEnv {
15+
customExportConditions = ['require', 'react-native'];
16+
17+
constructor(config, context) {
18+
super(config, context);
19+
}
20+
21+
async setup() {
22+
await super.setup();
23+
this.assignGlobals();
24+
this.initializeTurboModuleRegistry();
25+
}
26+
27+
assignGlobals() {
28+
Object.defineProperties(this.global, {
29+
__DEV__: {
30+
configurable: true,
31+
enumerable: true,
32+
value: true,
33+
writable: true,
34+
},
35+
});
36+
this.global.IS_REACT_ACT_ENVIRONMENT = true;
37+
}
38+
39+
initializeTurboModuleRegistry() {
40+
const dims = {width: 100, height: 100, scale: 1, fontScale: 1};
41+
const DIMS = {
42+
screen: {
43+
...dims,
44+
},
45+
window: {
46+
...dims,
47+
},
48+
};
49+
this.global.nativeModuleProxy = name => ({})[name];
50+
this.global.__turboModuleProxy = name =>
51+
({
52+
SourceCode: {getConstants: () => ({scriptURL: ''})},
53+
WebSocketModule: {connect: () => {}},
54+
FileReaderModule: {},
55+
AppState: {getConstants: () => ({}), getCurrentAppState: () => ({})},
56+
DeviceInfo: {getConstants: () => ({Dimensions: DIMS})},
57+
UIManager: {getConstants: () => ({})},
58+
Timing: {},
59+
DevSettings: {},
60+
PlatformConstants: {
61+
getConstants: () => ({reactNativeVersion: '1000.0.0'}),
62+
},
63+
Networking: {},
64+
ImageLoader: {},
65+
NativePerformanceCxx: {},
66+
NativePerformanceObserverCxx: {},
67+
LogBox: {},
68+
SettingsManager: {
69+
getConstants: () => ({settings: {}}),
70+
},
71+
LinkingManager: {},
72+
})[name];
73+
}
74+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
jest.requireActual('@react-native/js-polyfills/error-guard');
13+
14+
jest
15+
.mock('react-native/Libraries/ReactNative/UIManager', () => ({
16+
AndroidViewPager: {
17+
Commands: {
18+
setPage: jest.fn(),
19+
setPageWithoutAnimation: jest.fn(),
20+
},
21+
},
22+
blur: jest.fn(),
23+
createView: jest.fn(),
24+
customBubblingEventTypes: {},
25+
customDirectEventTypes: {},
26+
getConstants: () => ({
27+
ViewManagerNames: [],
28+
}),
29+
getDefaultEventTypes: jest.fn(),
30+
dispatchViewManagerCommand: jest.fn(),
31+
focus: jest.fn(),
32+
getViewManagerConfig: jest.fn(name => {
33+
if (name === 'AndroidDrawerLayout') {
34+
return {
35+
Constants: {
36+
DrawerPosition: {
37+
Left: 10,
38+
},
39+
},
40+
};
41+
}
42+
43+
return {NativeProps: {}};
44+
}),
45+
hasViewManagerConfig: jest.fn(name => {
46+
return name === 'AndroidDrawerLayout';
47+
}),
48+
measure: jest.fn(),
49+
manageChildren: jest.fn(),
50+
removeSubviewsFromContainerWithID: jest.fn(),
51+
replaceExistingNonRootView: jest.fn(),
52+
setChildren: jest.fn(),
53+
updateView: jest.fn(),
54+
AndroidDrawerLayout: {
55+
Constants: {
56+
DrawerPosition: {
57+
Left: 10,
58+
},
59+
},
60+
},
61+
AndroidTextInput: {
62+
Commands: {},
63+
},
64+
ScrollView: {
65+
Constants: {},
66+
},
67+
View: {
68+
Constants: {},
69+
},
70+
}))
71+
// Mock modules defined by the native layer (ex: Objective-C, Java)
72+
.mock('react-native/Libraries/BatchedBridge/NativeModules', () => ({
73+
AlertManager: {
74+
alertWithArgs: jest.fn(),
75+
},
76+
AsyncLocalStorage: {
77+
multiGet: jest.fn((keys, callback) =>
78+
process.nextTick(() => callback(null, [])),
79+
),
80+
multiSet: jest.fn((entries, callback) =>
81+
process.nextTick(() => callback(null)),
82+
),
83+
multiRemove: jest.fn((keys, callback) =>
84+
process.nextTick(() => callback(null)),
85+
),
86+
multiMerge: jest.fn((entries, callback) =>
87+
process.nextTick(() => callback(null)),
88+
),
89+
clear: jest.fn(callback => process.nextTick(() => callback(null))),
90+
getAllKeys: jest.fn(callback =>
91+
process.nextTick(() => callback(null, [])),
92+
),
93+
},
94+
DeviceInfo: {
95+
getConstants() {
96+
return {
97+
Dimensions: {
98+
window: {
99+
fontScale: 2,
100+
height: 1334,
101+
scale: 2,
102+
width: 750,
103+
},
104+
screen: {
105+
fontScale: 2,
106+
height: 1334,
107+
scale: 2,
108+
width: 750,
109+
},
110+
},
111+
};
112+
},
113+
},
114+
DevSettings: {
115+
addMenuItem: jest.fn(),
116+
reload: jest.fn(),
117+
},
118+
ImageLoader: {
119+
getSize: jest.fn(url => Promise.resolve([320, 240])),
120+
prefetchImage: jest.fn(),
121+
},
122+
ImageViewManager: {
123+
getSize: jest.fn((uri, success) =>
124+
process.nextTick(() => success(320, 240)),
125+
),
126+
prefetchImage: jest.fn(),
127+
},
128+
KeyboardObserver: {
129+
addListener: jest.fn(),
130+
removeListeners: jest.fn(),
131+
},
132+
Networking: {
133+
sendRequest: jest.fn(),
134+
abortRequest: jest.fn(),
135+
addListener: jest.fn(),
136+
removeListeners: jest.fn(),
137+
},
138+
PlatformConstants: {
139+
getConstants() {
140+
return {
141+
reactNativeVersion: {
142+
major: 1000,
143+
minor: 0,
144+
patch: 0,
145+
},
146+
};
147+
},
148+
},
149+
PushNotificationManager: {
150+
presentLocalNotification: jest.fn(),
151+
scheduleLocalNotification: jest.fn(),
152+
cancelAllLocalNotifications: jest.fn(),
153+
removeAllDeliveredNotifications: jest.fn(),
154+
getDeliveredNotifications: jest.fn(callback =>
155+
process.nextTick(() => []),
156+
),
157+
removeDeliveredNotifications: jest.fn(),
158+
setApplicationIconBadgeNumber: jest.fn(),
159+
getApplicationIconBadgeNumber: jest.fn(callback =>
160+
process.nextTick(() => callback(0)),
161+
),
162+
cancelLocalNotifications: jest.fn(),
163+
getScheduledLocalNotifications: jest.fn(callback =>
164+
process.nextTick(() => callback()),
165+
),
166+
requestPermissions: jest.fn(() =>
167+
Promise.resolve({alert: true, badge: true, sound: true}),
168+
),
169+
abandonPermissions: jest.fn(),
170+
checkPermissions: jest.fn(callback =>
171+
process.nextTick(() =>
172+
callback({alert: true, badge: true, sound: true}),
173+
),
174+
),
175+
getInitialNotification: jest.fn(() => Promise.resolve(null)),
176+
addListener: jest.fn(),
177+
removeListeners: jest.fn(),
178+
},
179+
StatusBarManager: {
180+
setColor: jest.fn(),
181+
setStyle: jest.fn(),
182+
setHidden: jest.fn(),
183+
setNetworkActivityIndicatorVisible: jest.fn(),
184+
setBackgroundColor: jest.fn(),
185+
setTranslucent: jest.fn(),
186+
getConstants: () => ({
187+
HEIGHT: 42,
188+
}),
189+
},
190+
Timing: {
191+
createTimer: jest.fn(),
192+
deleteTimer: jest.fn(),
193+
},
194+
UIManager: {},
195+
BlobModule: {
196+
getConstants: () => ({BLOB_URI_SCHEME: 'content', BLOB_URI_HOST: null}),
197+
addNetworkingHandler: jest.fn(),
198+
enableBlobSupport: jest.fn(),
199+
disableBlobSupport: jest.fn(),
200+
createFromParts: jest.fn(),
201+
sendBlob: jest.fn(),
202+
release: jest.fn(),
203+
},
204+
WebSocketModule: {
205+
connect: jest.fn(),
206+
send: jest.fn(),
207+
sendBinary: jest.fn(),
208+
ping: jest.fn(),
209+
close: jest.fn(),
210+
addListener: jest.fn(),
211+
removeListeners: jest.fn(),
212+
},
213+
I18nManager: {
214+
allowRTL: jest.fn(),
215+
forceRTL: jest.fn(),
216+
swapLeftAndRightInRTL: jest.fn(),
217+
getConstants: () => ({
218+
isRTL: false,
219+
doLeftAndRightSwapInRTL: true,
220+
}),
221+
},
222+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`render toJSON returns expected JSON output based on renderer component 1`] = `
4+
"<>
5+
<RCTView>
6+
<RCTText accessible=true allowFontScaling=true ellipsizeMode="tail" isHighlighted=false selectionColor=null>
7+
<RCTRawText text="Hello" />
8+
</RCTText>
9+
<RCTView />
10+
</RCTView>
11+
</>"
12+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow
9+
*/
10+
11+
'use strict';
12+
13+
import * as ReactNativeTestRenderer from '../index';
14+
import * as React from 'react';
15+
import {Text, View} from 'react-native';
16+
import 'react-native/Libraries/Components/View/ViewNativeComponent';
17+
18+
function TestComponent() {
19+
return (
20+
<View>
21+
<Text>Hello</Text>
22+
<View />
23+
</View>
24+
);
25+
}
26+
27+
describe('render', () => {
28+
describe('toJSON', () => {
29+
it('returns expected JSON output based on renderer component', () => {
30+
const result = ReactNativeTestRenderer.render(<TestComponent />);
31+
expect(result.toJSON()).toMatchSnapshot();
32+
});
33+
});
34+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow
9+
*/
10+
11+
'use strict';
12+
13+
import type {Element, ElementType} from 'react';
14+
15+
import * as FabricUIManager from 'react-native/Libraries/ReactNative/__mocks__/FabricUIManager';
16+
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
17+
import {act} from 'react-test-renderer';
18+
19+
type ReactNode = {
20+
children: Array<ReactNode>,
21+
props: {...},
22+
viewName: string,
23+
instanceHandle: {
24+
memoizedProps: {testID: ?string, ...},
25+
...
26+
},
27+
};
28+
29+
type RootReactNode = $ReadOnlyArray<ReactNode>;
30+
31+
type RenderResult = {
32+
toJSON: () => string,
33+
};
34+
35+
function buildRenderResult(rootNode: RootReactNode): RenderResult {
36+
return {
37+
toJSON: () => stringify(rootNode),
38+
};
39+
}
40+
41+
export function render(element: Element<ElementType>): RenderResult {
42+
const manager = FabricUIManager.getFabricUIManager();
43+
if (!manager) {
44+
throw new Error('No FabricUIManager found');
45+
}
46+
const containerTag = Math.round(Math.random() * 1000000);
47+
act(() => {
48+
ReactFabric.render(element, containerTag, () => {}, true);
49+
});
50+
51+
// $FlowFixMe
52+
const root: RootReactNode = manager.getRoot(containerTag);
53+
54+
if (root == null) {
55+
throw new Error('No root found for containerTag ' + containerTag);
56+
}
57+
58+
return buildRenderResult(root);
59+
}
60+
61+
function stringify(
62+
node: RootReactNode | ReactNode,
63+
indent: string = '',
64+
): string {
65+
const nextIndent = ' ' + indent;
66+
if (Array.isArray(node)) {
67+
return `<>
68+
${node.map(n => nextIndent + stringify(n, nextIndent)).join('\n')}
69+
</>`;
70+
}
71+
const children = node.children;
72+
const props = node.props
73+
? Object.entries(node.props)
74+
.map(([k, v]) => ` ${k}=${JSON.stringify(v) ?? ''}`)
75+
.join('')
76+
: '';
77+
78+
if (children.length > 0) {
79+
return `<${node.viewName}${props}>
80+
${children.map(c => nextIndent + stringify(c, nextIndent)).join('\n')}
81+
${indent}</${node.viewName}>`;
82+
} else {
83+
return `<${node.viewName}${props} />`;
84+
}
85+
}

‎packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import type {
2424
Spec as FabricUIManager,
2525
} from '../FabricUIManager';
2626

27+
import {createRootTag} from '../RootTag.js';
28+
2729
export type NodeMock = {
2830
children: NodeSet,
2931
instanceHandle: InternalInstanceHandle,
@@ -160,6 +162,7 @@ function hasDisplayNone(node: Node): boolean {
160162
}
161163

162164
interface IFabricUIManagerMock extends FabricUIManager {
165+
getRoot(rootTag: RootTag | number): NodeSet;
163166
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle;
164167
__addCommitHook(commitHook: UIManagerCommitHook): void;
165168
__removeCommitHook(commitHook: UIManagerCommitHook): void;
@@ -638,6 +641,15 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
638641
return 'RN:' + fromNode(node).viewName;
639642
}),
640643

644+
getRoot(containerTag: RootTag | number): NodeSet {
645+
const tag = createRootTag(containerTag);
646+
const root = roots.get(tag);
647+
if (!root) {
648+
throw new Error('No root found for containerTag ' + Number(tag));
649+
}
650+
return root;
651+
},
652+
641653
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle {
642654
return fromNode(node).instanceHandle;
643655
},

0 commit comments

Comments
 (0)
Please sign in to comment.