main
js 1,005 lines 34.2 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 typeof ReactTestRenderer from 'react-test-renderer';
11 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
12 import type {Context} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext';
13 import type {DispatcherContext} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
14 import type Store from 'react-devtools-shared/src/devtools/store';
15
16 import {getVersionedRenderImplementation} from './utils';
17
18 describe('ProfilerContext', () => {
19 let React;
20 let ReactDOM;
21 let ReactDOMClient;
22 let TestRenderer: ReactTestRenderer;
23 let bridge: FrontendBridge;
24 let legacyRender;
25 let store: Store;
26 let utils;
27
28 let BridgeContext;
29 let ProfilerContext;
30 let ProfilerContextController;
31 let StoreContext;
32 let TreeContextController;
33 let TreeDispatcherContext;
34 let TreeStateContext;
35
36 beforeEach(() => {
37 utils = require('./utils');
38 utils.beforeEachProfiling();
39
40 legacyRender = utils.legacyRender;
41
42 bridge = global.bridge;
43 store = global.store;
44 store.collapseNodesByDefault = false;
45 store.recordChangeDescriptions = true;
46
47 React = require('react');
48 ReactDOM = require('react-dom');
49 ReactDOMClient = require('react-dom/client');
50 TestRenderer = utils.requireTestRenderer();
51
52 BridgeContext =
53 require('react-devtools-shared/src/devtools/views/context').BridgeContext;
54 ProfilerContext =
55 require('react-devtools-shared/src/devtools/views/Profiler/ProfilerContext').ProfilerContext;
56 ProfilerContextController =
57 require('react-devtools-shared/src/devtools/views/Profiler/ProfilerContext').ProfilerContextController;
58 StoreContext =
59 require('react-devtools-shared/src/devtools/views/context').StoreContext;
60 TreeContextController =
61 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController;
62 TreeDispatcherContext =
63 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeDispatcherContext;
64 TreeStateContext =
65 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeStateContext;
66 });
67
68 const {render} = getVersionedRenderImplementation();
69
70 const Contexts = ({
71 children = null,
72 defaultInspectedElementID = null,
73 defaultInspectedElementIndex = null,
74 }: any) => (
75 <BridgeContext.Provider value={bridge}>
76 <StoreContext.Provider value={store}>
77 <TreeContextController
78 defaultInspectedElementID={defaultInspectedElementID}
79 defaultInspectedElementIndex={defaultInspectedElementIndex}>
80 <ProfilerContextController>{children}</ProfilerContextController>
81 </TreeContextController>
82 </StoreContext.Provider>
83 </BridgeContext.Provider>
84 );
85
86 // @reactVersion <= 18.2
87 // @reactVersion >= 18.0
88 it('updates updates profiling support based on the attached roots (legacy render)', async () => {
89 const Component = () => null;
90
91 let context: Context = ((null: any): Context);
92
93 function ContextReader() {
94 context = React.useContext(ProfilerContext);
95 return null;
96 }
97 await utils.actAsync(() => {
98 TestRenderer.create(
99 <Contexts>
100 <ContextReader />
101 </Contexts>,
102 );
103 });
104
105 expect(context.supportsProfiling).toBe(false);
106
107 const containerA = document.createElement('div');
108 const containerB = document.createElement('div');
109
110 await utils.actAsync(() => legacyRender(<Component />, containerA));
111 expect(context.supportsProfiling).toBe(true);
112
113 await utils.actAsync(() => legacyRender(<Component />, containerB));
114 await utils.actAsync(() => ReactDOM.unmountComponentAtNode(containerA));
115 expect(context.supportsProfiling).toBe(true);
116
117 await utils.actAsync(() => ReactDOM.unmountComponentAtNode(containerB));
118 expect(context.supportsProfiling).toBe(false);
119 });
120
121 // @reactVersion >= 18
122 it('updates updates profiling support based on the attached roots (createRoot)', async () => {
123 const Component = () => null;
124
125 let context: Context = ((null: any): Context);
126
127 function ContextReader() {
128 context = React.useContext(ProfilerContext);
129 return null;
130 }
131 await utils.actAsync(() => {
132 TestRenderer.create(
133 <Contexts>
134 <ContextReader />
135 </Contexts>,
136 );
137 });
138
139 expect(context.supportsProfiling).toBe(false);
140
141 const containerA = document.createElement('div');
142 const containerB = document.createElement('div');
143
144 const rootA = ReactDOMClient.createRoot(containerA);
145 const rootB = ReactDOMClient.createRoot(containerB);
146
147 await utils.actAsync(() => rootA.render(<Component />));
148 expect(context.supportsProfiling).toBe(true);
149
150 await utils.actAsync(() => rootB.render(<Component />));
151 await utils.actAsync(() => rootA.unmount());
152 expect(context.supportsProfiling).toBe(true);
153
154 await utils.actAsync(() => rootB.unmount());
155 expect(context.supportsProfiling).toBe(false);
156 });
157
158 it('should gracefully handle an empty profiling session (with no recorded commits)', async () => {
159 const Example = () => null;
160
161 utils.act(() => render(<Example />));
162
163 let context: Context = ((null: any): Context);
164
165 function ContextReader() {
166 context = React.useContext(ProfilerContext);
167 return null;
168 }
169
170 // Profile but don't record any updates.
171 await utils.actAsync(() => store.profilerStore.startProfiling());
172 await utils.actAsync(() => {
173 TestRenderer.create(
174 <Contexts>
175 <ContextReader />
176 </Contexts>,
177 );
178 });
179 expect(context).not.toBeNull();
180 expect(context.didRecordCommits).toBe(false);
181 expect(context.isProcessingData).toBe(false);
182 expect(context.isProfiling).toBe(true);
183 expect(context.profilingData).toBe(null);
184 await utils.actAsync(() => store.profilerStore.stopProfiling());
185
186 expect(context).not.toBeNull();
187 expect(context.didRecordCommits).toBe(false);
188 expect(context.isProcessingData).toBe(false);
189 expect(context.isProfiling).toBe(false);
190 expect(context.profilingData).toBe(null);
191 });
192
193 // @reactVersion <= 18.2
194 // @reactVersion >= 18.0
195 it('should auto-select the root ID matching the Components tab selection if it has profiling data (legacy render)', async () => {
196 const Parent = () => <Child />;
197 const Child = () => null;
198
199 const containerOne = document.createElement('div');
200 const containerTwo = document.createElement('div');
201 utils.act(() => legacyRender(<Parent />, containerOne));
202 utils.act(() => legacyRender(<Parent />, containerTwo));
203 expect(store).toMatchInlineSnapshot(`
204 [root]
205 ▾ <Parent>
206 <Child>
207 [root]
208 ▾ <Parent>
209 <Child>
210 `);
211
212 // Profile and record updates to both roots.
213 await utils.actAsync(() => store.profilerStore.startProfiling());
214 await utils.actAsync(() => legacyRender(<Parent />, containerOne));
215 await utils.actAsync(() => legacyRender(<Parent />, containerTwo));
216 await utils.actAsync(() => store.profilerStore.stopProfiling());
217
218 let context: Context = ((null: any): Context);
219 function ContextReader() {
220 context = React.useContext(ProfilerContext);
221 return null;
222 }
223
224 // Select an element within the second root.
225 await utils.actAsync(() =>
226 TestRenderer.create(
227 <Contexts
228 defaultInspectedElementID={store.getElementIDAtIndex(3)}
229 defaultInspectedElementIndex={3}>
230 <ContextReader />
231 </Contexts>,
232 ),
233 );
234
235 expect(context).not.toBeNull();
236 expect(context.rootID).toBe(
237 store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number)),
238 );
239 });
240
241 // @reactVersion >= 18
242 it('should auto-select the root ID matching the Components tab selection if it has profiling data (createRoot)', async () => {
243 const Parent = () => <Child />;
244 const Child = () => null;
245
246 const containerOne = document.createElement('div');
247 const containerTwo = document.createElement('div');
248
249 const rootOne = ReactDOMClient.createRoot(containerOne);
250 const rootTwo = ReactDOMClient.createRoot(containerTwo);
251
252 utils.act(() => rootOne.render(<Parent />));
253 utils.act(() => rootTwo.render(<Parent />));
254 expect(store).toMatchInlineSnapshot(`
255 [root]
256 ▾ <Parent>
257 <Child>
258 [root]
259 ▾ <Parent>
260 <Child>
261 `);
262
263 // Profile and record updates to both roots.
264 await utils.actAsync(() => store.profilerStore.startProfiling());
265 await utils.actAsync(() => rootOne.render(<Parent />));
266 await utils.actAsync(() => rootTwo.render(<Parent />));
267 await utils.actAsync(() => store.profilerStore.stopProfiling());
268
269 let context: Context = ((null: any): Context);
270 function ContextReader() {
271 context = React.useContext(ProfilerContext);
272 return null;
273 }
274
275 // Select an element within the second root.
276 await utils.actAsync(() =>
277 TestRenderer.create(
278 <Contexts
279 defaultInspectedElementID={store.getElementIDAtIndex(3)}
280 defaultInspectedElementIndex={3}>
281 <ContextReader />
282 </Contexts>,
283 ),
284 );
285
286 expect(context).not.toBeNull();
287 expect(context.rootID).toBe(
288 store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number)),
289 );
290 });
291
292 // @reactVersion <= 18.2
293 // @reactVersion >= 18.0
294 it('should not select the root ID matching the Components tab selection if it has no profiling data (legacy render)', async () => {
295 const Parent = () => <Child />;
296 const Child = () => null;
297
298 const containerOne = document.createElement('div');
299 const containerTwo = document.createElement('div');
300 utils.act(() => legacyRender(<Parent />, containerOne));
301 utils.act(() => legacyRender(<Parent />, containerTwo));
302 expect(store).toMatchInlineSnapshot(`
303 [root]
304 ▾ <Parent>
305 <Child>
306 [root]
307 ▾ <Parent>
308 <Child>
309 `);
310
311 // Profile and record updates to only the first root.
312 await utils.actAsync(() => store.profilerStore.startProfiling());
313 await utils.actAsync(() => legacyRender(<Parent />, containerOne));
314 await utils.actAsync(() => store.profilerStore.stopProfiling());
315
316 let context: Context = ((null: any): Context);
317 function ContextReader() {
318 context = React.useContext(ProfilerContext);
319 return null;
320 }
321
322 // Select an element within the second root.
323 await utils.actAsync(() =>
324 TestRenderer.create(
325 <Contexts
326 defaultInspectedElementID={store.getElementIDAtIndex(3)}
327 defaultInspectedElementIndex={3}>
328 <ContextReader />
329 </Contexts>,
330 ),
331 );
332
333 // Verify the default profiling root is the first one.
334 expect(context).not.toBeNull();
335 expect(context.rootID).toBe(
336 store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number)),
337 );
338 });
339
340 // @reactVersion >= 18
341 it('should not select the root ID matching the Components tab selection if it has no profiling data (createRoot)', async () => {
342 const Parent = () => <Child />;
343 const Child = () => null;
344
345 const containerOne = document.createElement('div');
346 const containerTwo = document.createElement('div');
347
348 const rootOne = ReactDOMClient.createRoot(containerOne);
349 const rootTwo = ReactDOMClient.createRoot(containerTwo);
350
351 utils.act(() => rootOne.render(<Parent />));
352 utils.act(() => rootTwo.render(<Parent />));
353 expect(store).toMatchInlineSnapshot(`
354 [root]
355 ▾ <Parent>
356 <Child>
357 [root]
358 ▾ <Parent>
359 <Child>
360 `);
361
362 // Profile and record updates to only the first root.
363 await utils.actAsync(() => store.profilerStore.startProfiling());
364 await utils.actAsync(() => rootOne.render(<Parent />));
365 await utils.actAsync(() => store.profilerStore.stopProfiling());
366
367 let context: Context = ((null: any): Context);
368 function ContextReader() {
369 context = React.useContext(ProfilerContext);
370 return null;
371 }
372
373 // Select an element within the second root.
374 await utils.actAsync(() =>
375 TestRenderer.create(
376 <Contexts
377 defaultInspectedElementID={store.getElementIDAtIndex(3)}
378 defaultInspectedElementIndex={3}>
379 <ContextReader />
380 </Contexts>,
381 ),
382 );
383
384 // Verify the default profiling root is the first one.
385 expect(context).not.toBeNull();
386 expect(context.rootID).toBe(
387 store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number)),
388 );
389 });
390
391 // @reactVersion <= 18.2
392 // @reactVersion >= 18.0
393 it('should maintain root selection between profiling sessions so long as there is data for that root (legacy render)', async () => {
394 const Parent = () => <Child />;
395 const Child = () => null;
396
397 const containerA = document.createElement('div');
398 const containerB = document.createElement('div');
399 utils.act(() => legacyRender(<Parent />, containerA));
400 utils.act(() => legacyRender(<Parent />, containerB));
401 expect(store).toMatchInlineSnapshot(`
402 [root]
403 ▾ <Parent>
404 <Child>
405 [root]
406 ▾ <Parent>
407 <Child>
408 `);
409
410 // Profile and record updates.
411 await utils.actAsync(() => store.profilerStore.startProfiling());
412 await utils.actAsync(() => legacyRender(<Parent />, containerA));
413 await utils.actAsync(() => legacyRender(<Parent />, containerB));
414 await utils.actAsync(() => store.profilerStore.stopProfiling());
415
416 let context: Context = ((null: any): Context);
417 let dispatch: DispatcherContext = ((null: any): DispatcherContext);
418 let inspectedElementID = null;
419 function ContextReader() {
420 context = React.useContext(ProfilerContext);
421 dispatch = React.useContext(TreeDispatcherContext);
422 inspectedElementID =
423 React.useContext(TreeStateContext).inspectedElementID;
424 return null;
425 }
426
427 const id = ((store.getElementIDAtIndex(3): any): number);
428
429 // Select an element within the second root.
430 await utils.actAsync(() =>
431 TestRenderer.create(
432 <Contexts
433 defaultInspectedElementID={id}
434 defaultInspectedElementIndex={3}>
435 <ContextReader />
436 </Contexts>,
437 ),
438 );
439
440 expect(inspectedElementID).toBe(id);
441
442 // Profile and record more updates to both roots
443 await utils.actAsync(() => store.profilerStore.startProfiling());
444 await utils.actAsync(() => legacyRender(<Parent />, containerA));
445 await utils.actAsync(() => legacyRender(<Parent />, containerB));
446 await utils.actAsync(() => store.profilerStore.stopProfiling());
447
448 const otherID = ((store.getElementIDAtIndex(0): any): number);
449
450 // Change the selected element within a the Components tab.
451 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
452
453 // Verify that the initial Profiler root selection is maintained.
454 expect(inspectedElementID).toBe(otherID);
455 expect(context).not.toBeNull();
456 expect(context.rootID).toBe(store.getRootIDForElement(id));
457 });
458
459 // @reactVersion >= 18.0
460 it('should maintain root selection between profiling sessions so long as there is data for that root (createRoot)', async () => {
461 const Parent = () => <Child />;
462 const Child = () => null;
463
464 const containerA = document.createElement('div');
465 const containerB = document.createElement('div');
466
467 const rootA = ReactDOMClient.createRoot(containerA);
468 const rootB = ReactDOMClient.createRoot(containerB);
469
470 utils.act(() => rootA.render(<Parent />));
471 utils.act(() => rootB.render(<Parent />));
472
473 expect(store).toMatchInlineSnapshot(`
474 [root]
475 ▾ <Parent>
476 <Child>
477 [root]
478 ▾ <Parent>
479 <Child>
480 `);
481
482 // Profile and record updates.
483 await utils.actAsync(() => store.profilerStore.startProfiling());
484 await utils.actAsync(() => rootA.render(<Parent />));
485 await utils.actAsync(() => rootB.render(<Parent />));
486 await utils.actAsync(() => store.profilerStore.stopProfiling());
487
488 let context: Context = ((null: any): Context);
489 let dispatch: DispatcherContext = ((null: any): DispatcherContext);
490 let inspectedElementID = null;
491 function ContextReader() {
492 context = React.useContext(ProfilerContext);
493 dispatch = React.useContext(TreeDispatcherContext);
494 inspectedElementID =
495 React.useContext(TreeStateContext).inspectedElementID;
496 return null;
497 }
498
499 const id = ((store.getElementIDAtIndex(3): any): number);
500
501 // Select an element within the second root.
502 await utils.actAsync(() =>
503 TestRenderer.create(
504 <Contexts
505 defaultInspectedElementID={id}
506 defaultInspectedElementIndex={3}>
507 <ContextReader />
508 </Contexts>,
509 ),
510 );
511
512 expect(inspectedElementID).toBe(id);
513
514 // Profile and record more updates to both roots
515 await utils.actAsync(() => store.profilerStore.startProfiling());
516 await utils.actAsync(() => rootA.render(<Parent />));
517 await utils.actAsync(() => rootB.render(<Parent />));
518 await utils.actAsync(() => store.profilerStore.stopProfiling());
519
520 const otherID = ((store.getElementIDAtIndex(0): any): number);
521
522 // Change the selected element within a the Components tab.
523 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
524
525 // Verify that the initial Profiler root selection is maintained.
526 expect(inspectedElementID).toBe(otherID);
527 expect(context).not.toBeNull();
528 expect(context.rootID).toBe(store.getRootIDForElement(id));
529 });
530
531 it('should sync selected element in the Components tab too, provided the element is a match', async () => {
532 const GrandParent = ({includeChild}) => (
533 <Parent includeChild={includeChild} />
534 );
535 const Parent = ({includeChild}) => (includeChild ? <Child /> : null);
536 const Child = () => null;
537
538 utils.act(() => render(<GrandParent includeChild={true} />));
539 expect(store).toMatchInlineSnapshot(`
540 [root]
541 ▾ <GrandParent>
542 ▾ <Parent>
543 <Child>
544 `);
545
546 const parentID = ((store.getElementIDAtIndex(1): any): number);
547 const childID = ((store.getElementIDAtIndex(2): any): number);
548
549 // Profile and record updates.
550 await utils.actAsync(() => store.profilerStore.startProfiling());
551 await utils.actAsync(() => render(<GrandParent includeChild={true} />));
552 await utils.actAsync(() => render(<GrandParent includeChild={false} />));
553 await utils.actAsync(() => store.profilerStore.stopProfiling());
554
555 expect(store).toMatchInlineSnapshot(`
556 [root]
557 ▾ <GrandParent>
558 <Parent>
559 `);
560
561 let context: Context = ((null: any): Context);
562 let inspectedElementID = null;
563 function ContextReader() {
564 context = React.useContext(ProfilerContext);
565 inspectedElementID =
566 React.useContext(TreeStateContext).inspectedElementID;
567 return null;
568 }
569
570 await utils.actAsync(() =>
571 TestRenderer.create(
572 <Contexts>
573 <ContextReader />
574 </Contexts>,
575 ),
576 );
577 expect(inspectedElementID).toBeNull();
578
579 // Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
580 await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
581 expect(inspectedElementID).toBe(parentID);
582
583 // Select an unmounted element and verify no Components tab selection doesn't change.
584 await utils.actAsync(() => context.selectFiber(childID, 'Child'));
585 expect(inspectedElementID).toBe(parentID);
586 });
587
588 it('should toggle profiling when the keyboard shortcut is pressed', async () => {
589 // Context providers
590 const Profiler =
591 require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
592 const {
593 SettingsContextController,
594 } = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
595 const {
596 ModalDialogContextController,
597 } = require('react-devtools-shared/src/devtools/views/ModalDialog');
598
599 // Dom component for profiling to be enabled
600 const Component = () => null;
601 utils.act(() => render(<Component />));
602
603 const profilerContainer = document.createElement('div');
604 document.body.appendChild(profilerContainer);
605
606 // Create a root for the profiler
607 const profilerRoot = ReactDOMClient.createRoot(profilerContainer);
608
609 // Render the profiler
610 utils.act(() => {
611 profilerRoot.render(
612 <Contexts>
613 <SettingsContextController browserTheme="light">
614 <ModalDialogContextController>
615 <Profiler />
616 </ModalDialogContextController>
617 </SettingsContextController>
618 </Contexts>,
619 );
620 });
621
622 // Verify that the profiler is not profiling.
623 expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
624
625 // Trigger the keyboard shortcut.
626 const ownerWindow = profilerContainer.ownerDocument.defaultView;
627 const isMac =
628 typeof navigator !== 'undefined' &&
629 navigator.platform.toUpperCase().indexOf('MAC') >= 0;
630
631 const keyEvent = new KeyboardEvent('keydown', {
632 key: 'e',
633 metaKey: isMac,
634 ctrlKey: !isMac,
635 bubbles: true,
636 });
637
638 // Dispatch keyboard event to toggle profiling on
639 // Try utils.actAsync with recursivelyFlush=false
640 await utils.actAsync(() => {
641 ownerWindow.dispatchEvent(keyEvent);
642 }, false);
643 expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(true);
644
645 // Dispatch keyboard event to toggle profiling off
646 await utils.actAsync(() => {
647 ownerWindow.dispatchEvent(keyEvent);
648 }, false);
649 expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
650
651 document.body.removeChild(profilerContainer);
652 }, 20000);
653
654 it('should navigate between commits when the keyboard shortcut is pressed', async () => {
655 const Parent = () => <Child />;
656 const Child = () => null;
657
658 const container = document.createElement('div');
659 const root = ReactDOMClient.createRoot(container);
660 utils.act(() => root.render(<Parent />));
661
662 // Profile and record multiple commits
663 await utils.actAsync(() => store.profilerStore.startProfiling());
664 await utils.actAsync(() => root.render(<Parent />)); // Commit 1
665 await utils.actAsync(() => root.render(<Parent />)); // Commit 2
666 await utils.actAsync(() => root.render(<Parent />)); // Commit 3
667 await utils.actAsync(() => store.profilerStore.stopProfiling());
668
669 const Profiler =
670 require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
671 const {
672 SettingsContextController,
673 } = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
674 const {
675 ModalDialogContextController,
676 } = require('react-devtools-shared/src/devtools/views/ModalDialog');
677
678 let context: Context = ((null: any): Context);
679 function ContextReader() {
680 context = React.useContext(ProfilerContext);
681 return null;
682 }
683
684 const profilerContainer = document.createElement('div');
685 document.body.appendChild(profilerContainer);
686
687 const profilerRoot = ReactDOMClient.createRoot(profilerContainer);
688
689 await utils.actAsync(() => {
690 profilerRoot.render(
691 <Contexts>
692 <SettingsContextController browserTheme="light">
693 <ModalDialogContextController>
694 <Profiler />
695 <ContextReader />
696 </ModalDialogContextController>
697 </SettingsContextController>
698 </Contexts>,
699 );
700 });
701
702 // Verify we have profiling data with 3 commits
703 expect(context.didRecordCommits).toBe(true);
704 expect(context.profilingData).not.toBeNull();
705 const rootID = context.rootID;
706 expect(rootID).not.toBeNull();
707 const dataForRoot = context.profilingData.dataForRoots.get(rootID);
708 expect(dataForRoot.commitData.length).toBe(3);
709 // Should start at the first commit
710 expect(context.selectedCommitIndex).toBe(0);
711
712 const ownerWindow = profilerContainer.ownerDocument.defaultView;
713 const isMac =
714 typeof navigator !== 'undefined' &&
715 navigator.platform.toUpperCase().indexOf('MAC') >= 0;
716
717 // Test ArrowRight navigation (forward) with correct modifier
718 const arrowRightEvent = new KeyboardEvent('keydown', {
719 key: 'ArrowRight',
720 metaKey: isMac,
721 ctrlKey: !isMac,
722 bubbles: true,
723 });
724
725 await utils.actAsync(() => {
726 ownerWindow.dispatchEvent(arrowRightEvent);
727 }, false);
728 expect(context.selectedCommitIndex).toBe(1);
729
730 await utils.actAsync(() => {
731 ownerWindow.dispatchEvent(arrowRightEvent);
732 }, false);
733 expect(context.selectedCommitIndex).toBe(2);
734
735 // Test wrap-around (last -> first)
736 await utils.actAsync(() => {
737 ownerWindow.dispatchEvent(arrowRightEvent);
738 }, false);
739 expect(context.selectedCommitIndex).toBe(0);
740
741 // Test ArrowLeft navigation (backward) with correct modifier
742 const arrowLeftEvent = new KeyboardEvent('keydown', {
743 key: 'ArrowLeft',
744 metaKey: isMac,
745 ctrlKey: !isMac,
746 bubbles: true,
747 });
748
749 await utils.actAsync(() => {
750 ownerWindow.dispatchEvent(arrowLeftEvent);
751 }, false);
752 expect(context.selectedCommitIndex).toBe(2);
753
754 await utils.actAsync(() => {
755 ownerWindow.dispatchEvent(arrowLeftEvent);
756 }, false);
757 expect(context.selectedCommitIndex).toBe(1);
758
759 await utils.actAsync(() => {
760 ownerWindow.dispatchEvent(arrowLeftEvent);
761 }, false);
762 expect(context.selectedCommitIndex).toBe(0);
763
764 // Cleanup
765 await utils.actAsync(() => profilerRoot.unmount());
766 document.body.removeChild(profilerContainer);
767 });
768
769 it('should reset commit index when switching to a different root', async () => {
770 const Parent = () => <Child />;
771 const Child = () => null;
772
773 const containerA = document.createElement('div');
774 const containerB = document.createElement('div');
775
776 const rootA = ReactDOMClient.createRoot(containerA);
777 const rootB = ReactDOMClient.createRoot(containerB);
778
779 utils.act(() => rootA.render(<Parent />));
780 utils.act(() => rootB.render(<Parent />));
781
782 // Profile and record different numbers of commits for each root
783 // Root A: 5 commits, Root B: 2 commits
784 await utils.actAsync(() => store.profilerStore.startProfiling());
785 await utils.actAsync(() => rootA.render(<Parent />)); // Root A commit 1
786 await utils.actAsync(() => rootA.render(<Parent />)); // Root A commit 2
787 await utils.actAsync(() => rootA.render(<Parent />)); // Root A commit 3
788 await utils.actAsync(() => rootA.render(<Parent />)); // Root A commit 4
789 await utils.actAsync(() => rootA.render(<Parent />)); // Root A commit 5
790 await utils.actAsync(() => rootB.render(<Parent />)); // Root B commit 1
791 await utils.actAsync(() => rootB.render(<Parent />)); // Root B commit 2
792 await utils.actAsync(() => store.profilerStore.stopProfiling());
793
794 let context: Context = ((null: any): Context);
795 function ContextReader() {
796 context = React.useContext(ProfilerContext);
797 return null;
798 }
799
800 await utils.actAsync(() =>
801 TestRenderer.create(
802 <Contexts>
803 <ContextReader />
804 </Contexts>,
805 ),
806 );
807
808 // Verify we have profiling data for both roots
809 expect(context.didRecordCommits).toBe(true);
810 expect(context.profilingData).not.toBeNull();
811
812 const rootIDs = Array.from(context.profilingData.dataForRoots.keys());
813 expect(rootIDs.length).toBe(2);
814
815 const [rootAID, rootBID] = rootIDs;
816 const rootAData = context.profilingData.dataForRoots.get(rootAID);
817 const rootBData = context.profilingData.dataForRoots.get(rootBID);
818
819 expect(rootAData.commitData.length).toBe(5);
820 expect(rootBData.commitData.length).toBe(2);
821
822 // Select root A and navigate to commit 4 (index 3)
823 await utils.actAsync(() => context.setRootID(rootAID));
824 expect(context.rootID).toBe(rootAID);
825 expect(context.selectedCommitIndex).toBe(0);
826
827 await utils.actAsync(() => context.selectCommitIndex(3));
828 expect(context.selectedCommitIndex).toBe(3);
829
830 // Switch to root B which only has 2 commits
831 // The commit index should be reset to 0, not stay at 3 (which would be invalid)
832 await utils.actAsync(() => context.setRootID(rootBID));
833 expect(context.rootID).toBe(rootBID);
834 // Should be reset to 0 since commit 3 doesn't exist in root B
835 expect(context.selectedCommitIndex).toBe(0);
836
837 // Verify we can still navigate in root B
838 await utils.actAsync(() => context.selectCommitIndex(1));
839 expect(context.selectedCommitIndex).toBe(1);
840
841 // Switch back to root A - should reset to 0
842 await utils.actAsync(() => context.setRootID(rootAID));
843 expect(context.rootID).toBe(rootAID);
844 expect(context.selectedCommitIndex).toBe(0);
845 });
846
847 it('should handle commit selection edge cases when filtering commits', async () => {
848 const Scheduler = require('scheduler');
849
850 // Create components that do varying amounts of work to generate different commit durations
851 const Parent = ({count}) => {
852 Scheduler.unstable_advanceTime(10);
853 const items = [];
854 for (let i = 0; i < count; i++) {
855 items.push(<Child key={i} duration={i} />);
856 }
857 return <div>{items}</div>;
858 };
859 const Child = ({duration}) => {
860 Scheduler.unstable_advanceTime(duration);
861 return <span>{duration}</span>;
862 };
863
864 const container = document.createElement('div');
865 const root = ReactDOMClient.createRoot(container);
866 utils.act(() => root.render(<Parent count={1} />));
867
868 // Profile and record multiple commits with different amounts of work
869 await utils.actAsync(() => store.profilerStore.startProfiling());
870 await utils.actAsync(() => root.render(<Parent count={5} />)); // Commit 1 - 20ms
871 await utils.actAsync(() => root.render(<Parent count={20} />)); // Commit 2 - 200ms
872 await utils.actAsync(() => root.render(<Parent count={50} />)); // Commit 3 - 1235ms
873 await utils.actAsync(() => root.render(<Parent count={10} />)); // Commit 4 - 55ms
874 await utils.actAsync(() => store.profilerStore.stopProfiling());
875
876 // Context providers
877 const Profiler =
878 require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
879 const {
880 SettingsContextController,
881 } = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
882 const {
883 ModalDialogContextController,
884 } = require('react-devtools-shared/src/devtools/views/ModalDialog');
885
886 let context: Context = ((null: any): Context);
887 function ContextReader() {
888 context = React.useContext(ProfilerContext);
889 return null;
890 }
891
892 const profilerContainer = document.createElement('div');
893 document.body.appendChild(profilerContainer);
894
895 const profilerRoot = ReactDOMClient.createRoot(profilerContainer);
896
897 await utils.actAsync(() => {
898 profilerRoot.render(
899 <Contexts>
900 <SettingsContextController browserTheme="light">
901 <ModalDialogContextController>
902 <Profiler />
903 <ContextReader />
904 </ModalDialogContextController>
905 </SettingsContextController>
906 </Contexts>,
907 );
908 });
909
910 // Verify we have profiling data with 4 commits
911 expect(context.didRecordCommits).toBe(true);
912 expect(context.profilingData).not.toBeNull();
913 const rootID = context.rootID;
914 expect(rootID).not.toBeNull();
915 const dataForRoot = context.profilingData.dataForRoots.get(rootID);
916 expect(dataForRoot.commitData.length).toBe(4);
917 // Edge case 1: Should start at the first commit
918 expect(context.selectedCommitIndex).toBe(0);
919
920 const ownerWindow = profilerContainer.ownerDocument.defaultView;
921 const isMac =
922 typeof navigator !== 'undefined' &&
923 navigator.platform.toUpperCase().indexOf('MAC') >= 0;
924
925 const arrowRightEvent = new KeyboardEvent('keydown', {
926 key: 'ArrowRight',
927 metaKey: isMac,
928 ctrlKey: !isMac,
929 bubbles: true,
930 });
931
932 await utils.actAsync(() => {
933 ownerWindow.dispatchEvent(arrowRightEvent);
934 }, false);
935 expect(context.selectedCommitIndex).toBe(1);
936
937 await utils.actAsync(() => {
938 context.setIsCommitFilterEnabled(true);
939 });
940
941 // Edge case 2: When filtering is enabled, selected commit should remain if it's still visible
942 expect(context.filteredCommitIndices.length).toBe(4);
943 expect(context.selectedCommitIndex).toBe(1);
944 expect(context.selectedFilteredCommitIndex).toBe(1);
945
946 await utils.actAsync(() => {
947 context.setMinCommitDuration(1000000);
948 });
949
950 // Edge case 3: When all commits are filtered out, selection should be null
951 expect(context.filteredCommitIndices).toEqual([]);
952 expect(context.selectedCommitIndex).toBe(null);
953 expect(context.selectedFilteredCommitIndex).toBe(null);
954
955 await utils.actAsync(() => {
956 context.setMinCommitDuration(0);
957 });
958
959 // Edge case 4: After restoring commits, first commit should be auto-selected
960 expect(context.filteredCommitIndices.length).toBe(4);
961 expect(context.selectedCommitIndex).toBe(0);
962 expect(context.selectedFilteredCommitIndex).toBe(0);
963
964 await utils.actAsync(() => {
965 ownerWindow.dispatchEvent(arrowRightEvent);
966 }, false);
967 expect(context.selectedCommitIndex).toBe(1);
968
969 await utils.actAsync(() => {
970 ownerWindow.dispatchEvent(arrowRightEvent);
971 }, false);
972 expect(context.selectedCommitIndex).toBe(2);
973
974 await utils.actAsync(() => {
975 ownerWindow.dispatchEvent(arrowRightEvent);
976 }, false);
977 expect(context.selectedCommitIndex).toBe(3);
978
979 // Filter out the currently selected commit using actual commit data
980 const commitDurations = dataForRoot.commitData.map(
981 commit => commit.duration,
982 );
983 const selectedCommitDuration = commitDurations[3];
984 const filterThreshold = selectedCommitDuration + 0.001;
985 await utils.actAsync(() => {
986 context.setMinCommitDuration(filterThreshold);
987 });
988
989 // Edge case 5: Should auto-select first available commit when current one is filtered
990 expect(context.selectedCommitIndex).not.toBe(null);
991 expect(context.selectedFilteredCommitIndex).toBe(1);
992
993 await utils.actAsync(() => {
994 context.setIsCommitFilterEnabled(false);
995 });
996
997 // Edge case 6: When filtering is disabled, selected commit should remain
998 expect(context.filteredCommitIndices.length).toBe(4);
999 expect(context.selectedCommitIndex).toBe(2);
1000 expect(context.selectedFilteredCommitIndex).toBe(2);
1001
1002 await utils.actAsync(() => profilerRoot.unmount());
1003 document.body.removeChild(profilerContainer);
1004 });
1005 });