main
js 198 lines 5.78 KB
Raw
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 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 describe('ReactProfiler DevTools integration', () => {
14 let React;
15 let ReactFeatureFlags;
16 let ReactTestRenderer;
17 let Scheduler;
18 let AdvanceTime;
19 let hook;
20 let waitForAll;
21 let waitFor;
22 let act;
23
24 beforeEach(() => {
25 global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
26 inject: () => {},
27 onCommitFiberRoot: jest.fn((rendererId, root) => {}),
28 onCommitFiberUnmount: () => {},
29 supportsFiber: true,
30 };
31
32 jest.resetModules();
33
34 ReactFeatureFlags = require('shared/ReactFeatureFlags');
35 ReactFeatureFlags.enableProfilerTimer = true;
36 Scheduler = require('scheduler');
37 React = require('react');
38 ReactTestRenderer = require('react-test-renderer');
39
40 const InternalTestUtils = require('internal-test-utils');
41 waitForAll = InternalTestUtils.waitForAll;
42 waitFor = InternalTestUtils.waitFor;
43 act = InternalTestUtils.act;
44
45 AdvanceTime = class extends React.Component {
46 static defaultProps = {
47 byAmount: 10,
48 shouldComponentUpdate: true,
49 };
50 shouldComponentUpdate(nextProps) {
51 return nextProps.shouldComponentUpdate;
52 }
53 render() {
54 // Simulate time passing when this component is rendered
55 Scheduler.unstable_advanceTime(this.props.byAmount);
56 return this.props.children || null;
57 }
58 };
59 });
60
61 it('should auto-Profile all fibers if the DevTools hook is detected', async () => {
62 const App = ({multiplier}) => {
63 Scheduler.unstable_advanceTime(2);
64 return (
65 <React.Profiler id="Profiler" onRender={onRender}>
66 <AdvanceTime byAmount={3 * multiplier} shouldComponentUpdate={true} />
67 <AdvanceTime
68 byAmount={7 * multiplier}
69 shouldComponentUpdate={false}
70 />
71 </React.Profiler>
72 );
73 };
74
75 const onRender = jest.fn(() => {});
76 let rendered;
77 await act(() => {
78 rendered = ReactTestRenderer.create(<App multiplier={1} />, {
79 unstable_isConcurrent: true,
80 });
81 });
82
83 expect(hook.onCommitFiberRoot).toHaveBeenCalledTimes(1);
84
85 // Measure observable timing using the Profiler component.
86 // The time spent in App (above the Profiler) won't be included in the durations,
87 // But needs to be accounted for in the offset times.
88 expect(onRender).toHaveBeenCalledTimes(1);
89 expect(onRender).toHaveBeenCalledWith('Profiler', 'mount', 10, 10, 2, 12);
90 onRender.mockClear();
91
92 // Measure unobservable timing required by the DevTools profiler.
93 // At this point, the base time should include both:
94 // The time 2ms in the App component itself, and
95 // The 10ms spend in the Profiler sub-tree beneath.
96 expect(rendered.root.findByType(App)._currentFiber().treeBaseDuration).toBe(
97 12,
98 );
99
100 await act(() => {
101 rendered.update(<App multiplier={2} />);
102 });
103
104 // Measure observable timing using the Profiler component.
105 // The time spent in App (above the Profiler) won't be included in the durations,
106 // But needs to be accounted for in the offset times.
107 expect(onRender).toHaveBeenCalledTimes(1);
108 expect(onRender).toHaveBeenCalledWith('Profiler', 'update', 6, 13, 14, 20);
109
110 // Measure unobservable timing required by the DevTools profiler.
111 // At this point, the base time should include both:
112 // The initial 9ms for the components that do not re-render, and
113 // The updated 6ms for the component that does.
114 expect(rendered.root.findByType(App)._currentFiber().treeBaseDuration).toBe(
115 15,
116 );
117 });
118
119 it('should reset the fiber stack correctly after an error when profiling host roots', async () => {
120 Scheduler.unstable_advanceTime(20);
121
122 let rendered;
123 await act(() => {
124 rendered = ReactTestRenderer.create(
125 <div>
126 <AdvanceTime byAmount={2} />
127 </div>,
128 {unstable_isConcurrent: true},
129 );
130 });
131
132 Scheduler.unstable_advanceTime(20);
133
134 function Throws() {
135 throw new Error('Oops!');
136 }
137
138 await expect(async () => {
139 await act(() => {
140 rendered.update(
141 <Throws>
142 <AdvanceTime byAmount={3} />
143 </Throws>,
144 );
145 });
146 }).rejects.toThrow('Oops!');
147
148 Scheduler.unstable_advanceTime(20);
149
150 await act(() => {
151 // But this should render correctly, if the profiler's fiber stack has been reset.
152 rendered.update(
153 <div>
154 <AdvanceTime byAmount={7} />
155 </div>,
156 );
157 });
158
159 // Measure unobservable timing required by the DevTools profiler.
160 // At this point, the base time should include only the most recent (not failed) render.
161 // It should not include time spent on the initial render,
162 // Or time that elapsed between any of the above renders.
163 expect(
164 rendered.root.findByType('div')._currentFiber().treeBaseDuration,
165 ).toBe(7);
166 });
167
168 it('regression test: #17159', async () => {
169 function Text({text}) {
170 Scheduler.log(text);
171 return text;
172 }
173
174 let root;
175 await act(() => {
176 root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
177 });
178
179 // Commit something
180 root.update(<Text text="A" />);
181 await waitForAll(['A']);
182 expect(root).toMatchRenderedOutput('A');
183
184 // Advance time by many seconds, larger than the default expiration time
185 // for updates.
186 Scheduler.unstable_advanceTime(10000);
187 // Schedule an update.
188 React.startTransition(() => {
189 root.update(<Text text="B" />);
190 });
191
192 // Update B should not instantly expire.
193 await waitFor([]);
194
195 await waitForAll(['B']);
196 expect(root).toMatchRenderedOutput('B');
197 });
198 });