main
js 236 lines 7.34 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 * @flow
8 */
9
10 import type Store from 'react-devtools-shared/src/devtools/store';
11
12 import {getVersionedRenderImplementation} from './utils';
13
14 describe('ProfilerStore', () => {
15 let React;
16 let store: Store;
17 let utils;
18
19 beforeEach(() => {
20 global.IS_REACT_ACT_ENVIRONMENT = true;
21
22 utils = require('./utils');
23 utils.beforeEachProfiling();
24
25 store = global.store;
26 store.collapseNodesByDefault = false;
27 store.recordChangeDescriptions = true;
28
29 React = require('react');
30 });
31
32 const {render, unmount} = getVersionedRenderImplementation();
33 const {render: renderOther, unmount: unmountOther} =
34 getVersionedRenderImplementation();
35
36 // @reactVersion >= 16.9
37 it('should not remove profiling data when roots are unmounted', async () => {
38 const Parent = ({count}) =>
39 new Array(count)
40 .fill(true)
41 .map((_, index) => <Child key={index} duration={index} />);
42 const Child = () => <div>Hi!</div>;
43
44 utils.act(() => {
45 render(<Parent key="A" count={3} />);
46 renderOther(<Parent key="B" count={2} />);
47 });
48
49 utils.act(() => store.profilerStore.startProfiling());
50
51 utils.act(() => {
52 render(<Parent key="A" count={4} />);
53 renderOther(<Parent key="B" count={1} />);
54 });
55
56 utils.act(() => store.profilerStore.stopProfiling());
57
58 const rootA = store.roots[0];
59 const rootB = store.roots[1];
60
61 utils.act(() => unmountOther());
62 expect(store.profilerStore.getDataForRoot(rootA)).not.toBeNull();
63
64 utils.act(() => unmount());
65 expect(store.profilerStore.getDataForRoot(rootB)).not.toBeNull();
66 });
67
68 // @reactVersion >= 16.9
69 it('should not allow new/saved profiling data to be set while profiling is in progress', () => {
70 utils.act(() => store.profilerStore.startProfiling());
71 const fauxProfilingData = {
72 dataForRoots: new Map(),
73 };
74 jest.spyOn(console, 'warn').mockImplementation(() => {});
75 store.profilerStore.profilingData = fauxProfilingData;
76 expect(store.profilerStore.profilingData).not.toBe(fauxProfilingData);
77 expect(console.warn).toHaveBeenCalledTimes(1);
78 expect(console.warn).toHaveBeenCalledWith(
79 'Profiling data cannot be updated while profiling is in progress.',
80 );
81 utils.act(() => store.profilerStore.stopProfiling());
82 store.profilerStore.profilingData = fauxProfilingData;
83 expect(store.profilerStore.profilingData).toBe(fauxProfilingData);
84 });
85
86 // @reactVersion >= 16.9
87 // This test covers current broken behavior (arguably) with the synthetic event system.
88 it('should filter empty commits', () => {
89 const inputRef = React.createRef();
90 const ControlledInput = () => {
91 const [name, setName] = React.useState('foo');
92 const handleChange = event => setName(event.target.value);
93 return <input ref={inputRef} value={name} onChange={handleChange} />;
94 };
95
96 // It's important that this test uses legacy sync mode.
97 // The root API does not trigger this particular failing case.
98 utils.act(() => render(<ControlledInput />));
99
100 utils.act(() => store.profilerStore.startProfiling());
101
102 // Sets a value in a way that React doesn't see,
103 // so that a subsequent "change" event will trigger the event handler.
104 const setUntrackedValue = Object.getOwnPropertyDescriptor(
105 HTMLInputElement.prototype,
106 'value',
107 ).set;
108
109 const target = inputRef.current;
110 setUntrackedValue.call(target, 'bar');
111 target.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
112 expect(target.value).toBe('bar');
113
114 utils.act(() => store.profilerStore.stopProfiling());
115
116 // Only one commit should have been recorded (in response to the "change" event).
117 const root = store.roots[0];
118 const data = store.profilerStore.getDataForRoot(root);
119 expect(data.commitData).toHaveLength(1);
120 expect(data.operations).toHaveLength(1);
121 });
122
123 // @reactVersion >= 16.9
124 it('should filter empty commits alt', () => {
125 let commitCount = 0;
126
127 const inputRef = React.createRef();
128 const Example = () => {
129 const [, setTouched] = React.useState(false);
130
131 const handleBlur = () => {
132 setTouched(true);
133 };
134
135 require('scheduler').unstable_advanceTime(1);
136
137 React.useLayoutEffect(() => {
138 commitCount++;
139 });
140
141 return <input ref={inputRef} onBlur={handleBlur} />;
142 };
143
144 // It's important that this test uses legacy sync mode.
145 // The root API does not trigger this particular failing case.
146 utils.act(() => render(<Example />));
147
148 expect(commitCount).toBe(1);
149 commitCount = 0;
150
151 utils.act(() => store.profilerStore.startProfiling());
152
153 // Focus and blur.
154 const target = inputRef.current;
155 utils.act(() => target.focus());
156 utils.act(() => target.blur());
157 utils.act(() => target.focus());
158 utils.act(() => target.blur());
159 expect(commitCount).toBe(1);
160
161 utils.act(() => store.profilerStore.stopProfiling());
162
163 // Only one commit should have been recorded (in response to the "change" event).
164 const root = store.roots[0];
165 const data = store.profilerStore.getDataForRoot(root);
166 expect(data.commitData).toHaveLength(1);
167 expect(data.operations).toHaveLength(1);
168 });
169
170 // @reactVersion >= 16.9
171 it('should throw if component filters are modified while profiling', () => {
172 utils.act(() => store.profilerStore.startProfiling());
173
174 expect(() => {
175 utils.act(() => {
176 const {
177 ElementTypeHostComponent,
178 } = require('react-devtools-shared/src/frontend/types');
179 store.componentFilters = [
180 utils.createElementTypeFilter(ElementTypeHostComponent),
181 ];
182 });
183 }).toThrow('Cannot modify filter preferences while profiling');
184 });
185
186 // @reactVersion >= 16.9
187 it('should not throw if state contains a property hasOwnProperty', () => {
188 let setStateCallback;
189 const ControlledInput = () => {
190 const [state, setState] = React.useState({hasOwnProperty: true});
191 setStateCallback = setState;
192 return state.hasOwnProperty;
193 };
194
195 // It's important that this test uses legacy sync mode.
196 // The root API does not trigger this particular failing case.
197 utils.act(() => render(<ControlledInput />));
198
199 utils.act(() => store.profilerStore.startProfiling());
200 utils.act(() =>
201 setStateCallback({
202 hasOwnProperty: false,
203 }),
204 );
205 utils.act(() => store.profilerStore.stopProfiling());
206
207 // Only one commit should have been recorded (in response to the "change" event).
208 const root = store.roots[0];
209 const data = store.profilerStore.getDataForRoot(root);
210 expect(data.commitData).toHaveLength(1);
211 expect(data.operations).toHaveLength(1);
212 });
213
214 // @reactVersion >= 18.0
215 it('should not throw while initializing context values for Fibers within a not-yet-mounted subtree', () => {
216 const promise = new Promise(resolve => {});
217 const SuspendingView = () => {
218 if (React.use) {
219 React.use(promise);
220 } else {
221 throw promise;
222 }
223 };
224
225 const App = () => {
226 return (
227 <React.Suspense fallback="Fallback">
228 <SuspendingView />
229 </React.Suspense>
230 );
231 };
232
233 utils.act(() => render(<App />));
234 utils.act(() => store.profilerStore.startProfiling());
235 });
236 });