11
12
let React;
13
let ReactFeatureFlags;
14
+let ReactNoop;
15
let ReactTestRenderer;
16
17
function loadModules({
18
enableProfilerTimer = true,
19
replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
20
+ useNoopRenderer = false,
21
} = {}) {
22
ReactFeatureFlags = require('shared/ReactFeatureFlags');
23
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
26
ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
27
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
28
React = require('react');
27
- ReactTestRenderer = require('react-test-renderer');
29
+
30
+ if (useNoopRenderer) {
31
+ ReactNoop = require('react-noop-renderer');
32
+ } else {
33
+ ReactTestRenderer = require('react-test-renderer');
34
+ }
35
}
36
37
+const mockDevToolsForTest = () => {
38
+ jest.mock('react-reconciler/src/ReactFiberDevToolsHook', () => ({
39
+ injectInternals: () => {},
40
+ isDevToolsPresent: true,
41
+ }));
42
+};
43
+
44
describe('Profiler', () => {
45
describe('works in profiling and non-profiling bundles', () => {
46
[true, false].forEach(flagEnabled => {
188
<AdvanceTime />
189
<AdvanceTime />
190
<AdvanceTime />
191
+ <AdvanceTime />
192
+ <AdvanceTime />
193
</div>,
194
);
195
180
- // Should be called twice: once to compute the update expiration time,
181
- // and once to record the commit time.
196
+ // Should be called two times:
197
+ // 1. To mark the start (resuming) of work
198
+ // 2. To compute the update expiration time,
199
+ // 3. To record the commit time.
200
// No additional calls from ProfilerTimer are expected.
183
- expect(mockNow).toHaveBeenCalledTimes(2);
201
+ expect(mockNow).toHaveBeenCalledTimes(3);
202
});
203
204
it('logs render times for both mount and update', () => {
978
// commit time
979
expect(mountCall[5]).toBe(flagEnabled && __DEV__ ? 54 : 44);
980
});
981
+
982
+ it('should reset the fiber stack correct after a "complete" phase error', () => {
983
+ jest.resetModules();
984
+
985
+ loadModules({
986
+ useNoopRenderer: true,
987
+ replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled,
988
+ });
989
+
990
+ // Simulate a renderer error during the "complete" phase.
991
+ // This mimics behavior like React Native's View/Text nesting validation.
992
+ ReactNoop.simulateErrorInHostConfigDuringCompletePhase(() => {
993
+ ReactNoop.render(
994
+ <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
995
+ <span>hi</span>
996
+ </React.unstable_Profiler>,
997
+ );
998
+ expect(ReactNoop.flush).toThrow('Error in host config.');
999
+ });
1000
+
1001
+ // So long as the profiler timer's fiber stack is reset correctly,
1002
+ // Subsequent renders should not error.
1003
+ ReactNoop.render(
1004
+ <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1005
+ <span>hi</span>
1006
+ </React.unstable_Profiler>,
1007
+ );
1008
+ ReactNoop.flush();
1009
+ });
1010
});
1011
});
1012
});
1049
expect(updateCall[4]).toBe(27); // start time
1050
});
1051
});
1052
+
1053
+ it('should handle interleaved async yields and batched commits', () => {
1054
+ jest.resetModules();
1055
+ mockDevToolsForTest();
1056
+ loadModules({useNoopRenderer: true});
1057
+
1058
+ const Child = ({duration, id}) => {
1059
+ ReactNoop.advanceTime(duration);
1060
+ ReactNoop.yield(`Child:render:${id}`);
1061
+ return null;
1062
+ };
1063
+
1064
+ class Parent extends React.Component {
1065
+ componentDidMount() {
1066
+ ReactNoop.yield(`Parent:componentDidMount:${this.props.id}`);
1067
+ }
1068
+ render() {
1069
+ const {duration, id} = this.props;
1070
+ return (
1071
+ <React.Fragment>
1072
+ <Child duration={duration} id={id} />
1073
+ <Child duration={duration} id={id} />
1074
+ </React.Fragment>
1075
+ );
1076
+ }
1077
+ }
1078
+
1079
+ ReactNoop.advanceTime(50);
1080
+
1081
+ ReactNoop.renderToRootWithID(<Parent duration={3} id="one" />, 'one');
1082
+
1083
+ // Process up to the <Parent> component, but yield before committing.
1084
+ // This ensures that the profiler timer still has paused fibers.
1085
+ const commitFirstRender = ReactNoop.flushWithoutCommitting(
1086
+ ['Child:render:one', 'Child:render:one'],
1087
+ 'one',
1088
+ );
1089
+
1090
+ expect(ReactNoop.getRoot('one').current.actualDuration).toBe(0);
1091
+
1092
+ ReactNoop.advanceTime(100);
1093
+
1094
+ // Process some async work, but yield before committing it.
1095
+ ReactNoop.renderToRootWithID(<Parent duration={7} id="two" />, 'two');
1096
+ ReactNoop.flushThrough(['Child:render:two']);
1097
+
1098
+ ReactNoop.advanceTime(150);
1099
+
1100
+ // Commit the previously paused, batched work.
1101
+ commitFirstRender(['Parent:componentDidMount:one']);
1102
+
1103
+ expect(ReactNoop.getRoot('one').current.actualDuration).toBe(6);
1104
+ expect(ReactNoop.getRoot('two').current.actualDuration).toBe(0);
1105
+
1106
+ ReactNoop.advanceTime(200);
1107
+
1108
+ ReactNoop.flush();
1109
+
1110
+ expect(ReactNoop.getRoot('two').current.actualDuration).toBe(14);
1111
+ });
1112
});