main
js 3,313 lines 88 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 {
12 withErrorsOrWarningsIgnored,
13 getLegacyRenderImplementation,
14 getModernRenderImplementation,
15 getVersionedRenderImplementation,
16 } from 'react-devtools-shared/src/__tests__/utils';
17
18 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
19 import type Store from 'react-devtools-shared/src/devtools/store';
20
21 describe('InspectedElement', () => {
22 let React;
23 let ReactDOM;
24 let ReactDOMClient;
25 let PropTypes;
26 let TestRenderer: ReactTestRenderer;
27 let bridge: FrontendBridge;
28 let store: Store;
29 let utils;
30
31 let BridgeContext;
32 let InspectedElementContext;
33 let InspectedElementContextController;
34 let SettingsContextController;
35 let StoreContext;
36 let TreeContextController;
37 let TreeStateContext;
38 let TreeDispatcherContext;
39
40 let TestUtilsAct;
41 let TestRendererAct;
42
43 let testRendererInstance;
44
45 let ErrorBoundary;
46 let errorBoundaryInstance;
47
48 global.IS_REACT_ACT_ENVIRONMENT = true;
49
50 beforeEach(() => {
51 utils = require('./utils');
52 utils.beforeEachProfiling();
53
54 bridge = global.bridge;
55 store = global.store;
56 store.collapseNodesByDefault = false;
57
58 React = require('react');
59 ReactDOM = require('react-dom');
60 ReactDOMClient = require('react-dom/client');
61 PropTypes = require('prop-types');
62 TestUtilsAct = require('internal-test-utils').act;
63 TestRenderer = utils.requireTestRenderer();
64 TestRendererAct = require('internal-test-utils').act;
65
66 BridgeContext =
67 require('react-devtools-shared/src/devtools/views/context').BridgeContext;
68 InspectedElementContext =
69 require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext').InspectedElementContext;
70 InspectedElementContextController =
71 require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext').InspectedElementContextController;
72 SettingsContextController =
73 require('react-devtools-shared/src/devtools/views/Settings/SettingsContext').SettingsContextController;
74 StoreContext =
75 require('react-devtools-shared/src/devtools/views/context').StoreContext;
76 TreeContextController =
77 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController;
78 TreeStateContext =
79 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeStateContext;
80 TreeDispatcherContext =
81 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeDispatcherContext;
82
83 // Used by inspectElementAtIndex() helper function
84 utils.act(() => {
85 testRendererInstance = TestRenderer.create(null, {
86 unstable_isConcurrent: true,
87 });
88 });
89
90 errorBoundaryInstance = null;
91
92 ErrorBoundary = class extends React.Component {
93 state = {error: null};
94 componentDidCatch(error) {
95 this.setState({error});
96 }
97 render() {
98 errorBoundaryInstance = this;
99
100 if (this.state.error) {
101 return null;
102 }
103 return this.props.children;
104 }
105 };
106 });
107
108 afterEach(() => {
109 jest.restoreAllMocks();
110 });
111
112 const {render: legacyRender} = getLegacyRenderImplementation();
113 const {render: modernRender} = getModernRenderImplementation();
114 const {render} = getVersionedRenderImplementation();
115
116 const Contexts = ({
117 children,
118 defaultInspectedElementID = null,
119 defaultInspectedElementIndex = null,
120 }) => (
121 <BridgeContext.Provider value={bridge}>
122 <StoreContext.Provider value={store}>
123 <SettingsContextController>
124 <TreeContextController
125 defaultInspectedElementID={defaultInspectedElementID}
126 defaultInspectedElementIndex={defaultInspectedElementIndex}>
127 <InspectedElementContextController>
128 {children}
129 </InspectedElementContextController>
130 </TreeContextController>
131 </SettingsContextController>
132 </StoreContext.Provider>
133 </BridgeContext.Provider>
134 );
135
136 function useInspectedElement() {
137 const {inspectedElement} = React.useContext(InspectedElementContext);
138 return inspectedElement;
139 }
140
141 function useInspectElementPath() {
142 const {inspectPaths} = React.useContext(InspectedElementContext);
143 return inspectPaths;
144 }
145
146 function noop() {}
147
148 async function inspectElementAtIndex(
149 index,
150 useCustomHook = noop,
151 shouldThrow = false,
152 ) {
153 let didFinish = false;
154 let inspectedElement = null;
155
156 function Suspender() {
157 useCustomHook();
158 inspectedElement = useInspectedElement();
159 didFinish = true;
160 return null;
161 }
162
163 const id = ((store.getElementIDAtIndex(index): any): number);
164
165 await utils.actAsync(() => {
166 testRendererInstance.update(
167 <ErrorBoundary>
168 <Contexts
169 defaultInspectedElementID={id}
170 defaultInspectedElementIndex={index}>
171 <React.Suspense fallback={null}>
172 <Suspender id={id} index={index} />
173 </React.Suspense>
174 </Contexts>
175 </ErrorBoundary>,
176 );
177 }, false);
178
179 if (!shouldThrow) {
180 expect(didFinish).toBe(true);
181 }
182
183 return inspectedElement;
184 }
185
186 // TODO(hoxyq): Enable this test for versions ~18, currently broken
187 // @reactVersion <= 18.2
188 // eslint-disable-next-line jest/no-disabled-tests
189 it.skip('should inspect the currently selected element (legacy render)', async () => {
190 const Example = () => {
191 const [count] = React.useState(1);
192 return count;
193 };
194
195 await utils.actAsync(() => {
196 legacyRender(<Example a={1} b="abc" />);
197 });
198
199 const inspectedElement = await inspectElementAtIndex(0);
200 expect(inspectedElement).toMatchInlineSnapshot(`
201 {
202 "context": null,
203 "events": undefined,
204 "hooks": [
205 {
206 "debugInfo": null,
207 "hookSource": {
208 "columnNumber": "removed by Jest serializer",
209 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
210 "functionName": "Example",
211 "lineNumber": "removed by Jest serializer",
212 },
213 "id": 0,
214 "isStateEditable": true,
215 "name": "State",
216 "subHooks": [],
217 "value": 1,
218 },
219 ],
220 "id": 2,
221 "owners": null,
222 "props": {
223 "a": 1,
224 "b": "abc",
225 },
226 "rootType": "render()",
227 "state": null,
228 }
229 `);
230 });
231
232 it('should inspect the currently selected element (createRoot)', async () => {
233 const Example = () => {
234 const [count] = React.useState(1);
235 return count;
236 };
237
238 await utils.actAsync(() => {
239 modernRender(<Example a={1} b="abc" />);
240 });
241
242 const inspectedElement = await inspectElementAtIndex(0);
243 expect(inspectedElement).toMatchInlineSnapshot(`
244 {
245 "context": null,
246 "events": undefined,
247 "hooks": [
248 {
249 "debugInfo": null,
250 "hookSource": {
251 "columnNumber": "removed by Jest serializer",
252 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
253 "functionName": "Example",
254 "lineNumber": "removed by Jest serializer",
255 },
256 "id": 0,
257 "isStateEditable": true,
258 "name": "State",
259 "subHooks": [],
260 "value": 1,
261 },
262 ],
263 "id": 2,
264 "owners": null,
265 "props": {
266 "a": 1,
267 "b": "abc",
268 },
269 "rootType": "createRoot()",
270 "state": null,
271 }
272 `);
273 });
274
275 it('should have hasLegacyContext flag set to either "true" or "false" depending on which context API is used.', async () => {
276 const contextData = {
277 bool: true,
278 };
279
280 // Legacy Context API.
281 class LegacyContextProvider extends React.Component<any> {
282 static childContextTypes = {
283 bool: PropTypes.bool,
284 };
285 getChildContext() {
286 return contextData;
287 }
288 render() {
289 return this.props.children;
290 }
291 }
292 class LegacyContextConsumer extends React.Component<any> {
293 static contextTypes = {
294 bool: PropTypes.bool,
295 };
296 render() {
297 return null;
298 }
299 }
300
301 // Modern Context API
302 const BoolContext = React.createContext(contextData.bool);
303 BoolContext.displayName = 'BoolContext';
304
305 class ModernContextType extends React.Component<any> {
306 static contextType = BoolContext;
307 render() {
308 return null;
309 }
310 }
311
312 const ModernContext = React.createContext();
313 ModernContext.displayName = 'ModernContext';
314
315 await utils.actAsync(() =>
316 render(
317 <React.Fragment>
318 <LegacyContextProvider>
319 <LegacyContextConsumer />
320 </LegacyContextProvider>
321 <BoolContext.Consumer>{value => null}</BoolContext.Consumer>
322 <ModernContextType />
323 <ModernContext.Provider value={contextData}>
324 <ModernContext.Consumer>{value => null}</ModernContext.Consumer>
325 </ModernContext.Provider>
326 </React.Fragment>,
327 ),
328 );
329
330 const cases = [
331 {
332 // <LegacyContextConsumer />
333 index: 1,
334 shouldHaveLegacyContext: true,
335 },
336 {
337 // <BoolContext.Consumer>
338 index: 2,
339 shouldHaveLegacyContext: false,
340 },
341 {
342 // <ModernContextType />
343 index: 3,
344 shouldHaveLegacyContext: false,
345 },
346 {
347 // <ModernContext.Consumer>
348 index: 5,
349 shouldHaveLegacyContext: false,
350 },
351 ];
352
353 for (let i = 0; i < cases.length; i++) {
354 const {index, shouldHaveLegacyContext} = cases[i];
355
356 // HACK: Recreate TestRenderer instance because we rely on default state values
357 // from props like defaultInspectedElementID and it's easier to reset here than
358 // to read the TreeDispatcherContext and update the selected ID that way.
359 // We're testing the inspected values here, not the context wiring, so that's ok.
360 withErrorsOrWarningsIgnored(
361 ['An update to %s inside a test was not wrapped in act'],
362 () => {
363 testRendererInstance = TestRenderer.create(null, {
364 unstable_isConcurrent: true,
365 });
366 },
367 );
368
369 const inspectedElement = await inspectElementAtIndex(index);
370
371 expect(inspectedElement.context).not.toBe(null);
372 expect(inspectedElement.hasLegacyContext).toBe(shouldHaveLegacyContext);
373 }
374 });
375
376 it('should poll for updates for the currently selected element', async () => {
377 const Example = () => null;
378
379 await utils.actAsync(() => render(<Example a={1} b="abc" />), false);
380
381 let inspectedElement = await inspectElementAtIndex(0);
382 expect(inspectedElement.props).toMatchInlineSnapshot(`
383 {
384 "a": 1,
385 "b": "abc",
386 }
387 `);
388
389 await utils.actAsync(() => render(<Example a={2} b="def" />), false);
390
391 // TODO (cache)
392 // This test only passes if both the check-for-updates poll AND the test renderer.update() call are included below.
393 // It seems like either one of the two should be sufficient but:
394 // 1. Running only check-for-updates schedules a transition that React never renders.
395 // 2. Running only renderer.update() loads stale data (first props)
396
397 // Wait for our check-for-updates poll to get the new data.
398 jest.runOnlyPendingTimers();
399 await Promise.resolve();
400
401 inspectedElement = await inspectElementAtIndex(0);
402 expect(inspectedElement.props).toMatchInlineSnapshot(`
403 {
404 "a": 2,
405 "b": "def",
406 }
407 `);
408 });
409
410 it('should not re-render a function with hooks if it did not update since it was last inspected', async () => {
411 let targetRenderCount = 0;
412
413 const Wrapper = ({children}) => children;
414 const Target = React.memo(props => {
415 targetRenderCount++;
416 // Even though his hook isn't referenced, it's used to observe backend rendering.
417 React.useState(0);
418 return null;
419 });
420
421 await utils.actAsync(() =>
422 render(
423 <Wrapper>
424 <Target a={1} b="abc" />
425 </Wrapper>,
426 ),
427 );
428
429 targetRenderCount = 0;
430
431 let inspectedElement = await inspectElementAtIndex(1);
432 // One more because we call render function for generating component stack,
433 // which is required for defining source location
434 expect(targetRenderCount).toBe(2);
435 expect(inspectedElement.props).toMatchInlineSnapshot(`
436 {
437 "a": 1,
438 "b": "abc",
439 }
440 `);
441
442 const prevInspectedElement = inspectedElement;
443
444 targetRenderCount = 0;
445 inspectedElement = await inspectElementAtIndex(1);
446 expect(targetRenderCount).toBe(0);
447 expect(inspectedElement).toEqual(prevInspectedElement);
448
449 targetRenderCount = 0;
450
451 await utils.actAsync(
452 () =>
453 render(
454 <Wrapper>
455 <Target a={2} b="def" />
456 </Wrapper>,
457 ),
458 false,
459 );
460
461 // Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
462 inspectedElement = await inspectElementAtIndex(1);
463 expect(targetRenderCount).toBe(2);
464 expect(inspectedElement.props).toMatchInlineSnapshot(`
465 {
466 "a": 2,
467 "b": "def",
468 }
469 `);
470 });
471
472 // See github.com/facebook/react/issues/22241#issuecomment-931299972
473 it('should properly recover from a cache miss on the frontend', async () => {
474 let targetRenderCount = 0;
475
476 const Wrapper = ({children}) => children;
477 const Target = React.memo(props => {
478 targetRenderCount++;
479 // Even though his hook isn't referenced, it's used to observe backend rendering.
480 React.useState(0);
481 return null;
482 });
483
484 await utils.actAsync(() =>
485 render(
486 <Wrapper>
487 <Target a={1} b="abc" />
488 </Wrapper>,
489 ),
490 );
491
492 targetRenderCount = 0;
493
494 let inspectedElement = await inspectElementAtIndex(1);
495 // One more because we call render function for generating component stack,
496 // which is required for defining source location
497 expect(targetRenderCount).toBe(2);
498 expect(inspectedElement.props).toMatchInlineSnapshot(`
499 {
500 "a": 1,
501 "b": "abc",
502 }
503 `);
504
505 const prevInspectedElement = inspectedElement;
506
507 // This test causes an intermediate error to be logged but we can ignore it.
508 jest.spyOn(console, 'error').mockImplementation(() => {});
509
510 // Clear the frontend cache to simulate DevTools being closed and re-opened.
511 // The backend still thinks the most recently-inspected element is still cached,
512 // so the frontend needs to tell it to resend a full value.
513 // We can verify this by asserting that the component is re-rendered again.
514 withErrorsOrWarningsIgnored(
515 ['An update to %s inside a test was not wrapped in act'],
516 () => {
517 testRendererInstance = TestRenderer.create(null, {
518 unstable_isConcurrent: true,
519 });
520 },
521 );
522
523 const {
524 clearCacheForTests,
525 } = require('react-devtools-shared/src/inspectedElementMutableSource');
526 clearCacheForTests();
527
528 targetRenderCount = 0;
529 inspectedElement = await inspectElementAtIndex(1);
530 expect(targetRenderCount).toBe(1);
531 expect(inspectedElement).toEqual(prevInspectedElement);
532 });
533
534 it('should temporarily disable console logging when re-running a component to inspect its hooks', async () => {
535 let targetRenderCount = 0;
536
537 jest.spyOn(console, 'error').mockImplementation(() => {});
538 jest.spyOn(console, 'info').mockImplementation(() => {});
539 jest.spyOn(console, 'log').mockImplementation(() => {});
540 jest.spyOn(console, 'warn').mockImplementation(() => {});
541
542 const Target = React.memo(props => {
543 targetRenderCount++;
544 console.error('error');
545 console.info('info');
546 console.log('log');
547 console.warn('warn');
548 React.useState(0);
549 return null;
550 });
551
552 await utils.actAsync(() => render(<Target a={1} b="abc" />));
553
554 expect(targetRenderCount).toBe(1);
555 expect(console.error).toHaveBeenCalledTimes(1);
556 expect(console.error).toHaveBeenCalledWith('error');
557 expect(console.info).toHaveBeenCalledTimes(1);
558 expect(console.info).toHaveBeenCalledWith('info');
559 expect(console.log).toHaveBeenCalledTimes(1);
560 expect(console.log).toHaveBeenCalledWith('log');
561 expect(console.warn).toHaveBeenCalledTimes(1);
562 expect(console.warn).toHaveBeenCalledWith('warn');
563
564 const inspectedElement = await inspectElementAtIndex(0);
565
566 expect(inspectedElement).not.toBe(null);
567 // One more because we call render function for generating component stack,
568 // which is required for defining source location
569 expect(targetRenderCount).toBe(3);
570 expect(console.error).toHaveBeenCalledTimes(1);
571 expect(console.info).toHaveBeenCalledTimes(1);
572 expect(console.log).toHaveBeenCalledTimes(1);
573 expect(console.warn).toHaveBeenCalledTimes(1);
574 });
575
576 it('should support simple data types', async () => {
577 const Example = () => null;
578
579 await utils.actAsync(() =>
580 render(
581 <Example
582 boolean_false={false}
583 boolean_true={true}
584 infinity={Infinity}
585 minus_infinity={-Infinity}
586 integer_zero={0}
587 integer_one={1}
588 float={1.23}
589 string="abc"
590 string_empty=""
591 nan={NaN}
592 value_null={null}
593 value_undefined={undefined}
594 />,
595 ),
596 );
597
598 const inspectedElement = await inspectElementAtIndex(0);
599
600 expect(inspectedElement.props).toMatchInlineSnapshot(`
601 {
602 "boolean_false": false,
603 "boolean_true": true,
604 "float": 1.23,
605 "infinity": Infinity,
606 "integer_one": 1,
607 "integer_zero": 0,
608 "minus_infinity": -Infinity,
609 "nan": NaN,
610 "string": "abc",
611 "string_empty": "",
612 "value_null": null,
613 "value_undefined": undefined,
614 }
615 `);
616 });
617
618 it('should support complex data types', async () => {
619 const Immutable = require('immutable');
620
621 const Example = () => null;
622
623 const arrayOfArrays = [[['abc', 123, true], []]];
624 const div = document.createElement('div');
625 const exampleFunction = () => {};
626 const exampleDateISO = '2019-12-31T23:42:42.000Z';
627 const setShallow = new Set(['abc', 123]);
628 const mapShallow = new Map([
629 ['name', 'Brian'],
630 ['food', 'sushi'],
631 ]);
632 const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
633 const mapOfMaps = new Map([
634 ['first', mapShallow],
635 ['second', mapShallow],
636 ]);
637 const objectOfObjects = {
638 inner: {string: 'abc', number: 123, boolean: true},
639 };
640 const objectWithSymbol = {
641 [Symbol('name')]: 'hello',
642 };
643 const typedArray = Int8Array.from([100, -100, 0]);
644 const arrayBuffer = typedArray.buffer;
645 const dataView = new DataView(arrayBuffer);
646 const immutableMap = Immutable.fromJS({
647 a: [{hello: 'there'}, 'fixed', true],
648 b: 123,
649 c: {
650 '1': 'xyz',
651 xyz: 1,
652 },
653 });
654
655 class Class {
656 anonymousFunction = () => {};
657 }
658 const instance = new Class();
659
660 const proxyInstance = new Proxy(() => {}, {
661 get: function (_, name) {
662 return function () {
663 return null;
664 };
665 },
666 });
667
668 await utils.actAsync(() =>
669 render(
670 <Example
671 anonymous_fn={instance.anonymousFunction}
672 array_buffer={arrayBuffer}
673 array_of_arrays={arrayOfArrays}
674 big_int={BigInt(123)}
675 bound_fn={exampleFunction.bind(this)}
676 data_view={dataView}
677 date={new Date(exampleDateISO)}
678 fn={exampleFunction}
679 html_element={div}
680 immutable={immutableMap}
681 map={mapShallow}
682 map_of_maps={mapOfMaps}
683 object_of_objects={objectOfObjects}
684 object_with_symbol={objectWithSymbol}
685 proxy={proxyInstance}
686 react_element={<span />}
687 react_lazy={React.lazy(async () => ({default: 'foo'}))}
688 regexp={/abc/giu}
689 set={setShallow}
690 set_of_sets={setOfSets}
691 symbol={Symbol('symbol')}
692 typed_array={typedArray}
693 />,
694 ),
695 );
696
697 const inspectedElement = await inspectElementAtIndex(0);
698
699 expect(inspectedElement.props).toMatchInlineSnapshot(`
700 {
701 "anonymous_fn": Dehydrated {
702 "preview_short": () => {},
703 "preview_long": () => {},
704 },
705 "array_buffer": Dehydrated {
706 "preview_short": ArrayBuffer(3),
707 "preview_long": ArrayBuffer(3),
708 },
709 "array_of_arrays": [
710 Dehydrated {
711 "preview_short": Array(2),
712 "preview_long": [Array(3), Array(0)],
713 },
714 ],
715 "big_int": Dehydrated {
716 "preview_short": 123n,
717 "preview_long": 123n,
718 },
719 "bound_fn": Dehydrated {
720 "preview_short": bound exampleFunction() {},
721 "preview_long": bound exampleFunction() {},
722 },
723 "data_view": Dehydrated {
724 "preview_short": DataView(3),
725 "preview_long": DataView(3),
726 },
727 "date": Dehydrated {
728 "preview_short": Tue Dec 31 2019 23:42:42 GMT+0000 (Coordinated Universal Time),
729 "preview_long": Tue Dec 31 2019 23:42:42 GMT+0000 (Coordinated Universal Time),
730 },
731 "fn": Dehydrated {
732 "preview_short": exampleFunction() {},
733 "preview_long": exampleFunction() {},
734 },
735 "html_element": Dehydrated {
736 "preview_short": <div />,
737 "preview_long": <div />,
738 },
739 "immutable": {
740 "0": Dehydrated {
741 "preview_short": Array(2),
742 "preview_long": ["a", List(3)],
743 },
744 "1": Dehydrated {
745 "preview_short": Array(2),
746 "preview_long": ["b", 123],
747 },
748 "2": Dehydrated {
749 "preview_short": Array(2),
750 "preview_long": ["c", Map(2)],
751 },
752 },
753 "map": {
754 "0": Dehydrated {
755 "preview_short": Array(2),
756 "preview_long": ["name", "Brian"],
757 },
758 "1": Dehydrated {
759 "preview_short": Array(2),
760 "preview_long": ["food", "sushi"],
761 },
762 },
763 "map_of_maps": {
764 "0": Dehydrated {
765 "preview_short": Array(2),
766 "preview_long": ["first", Map(2)],
767 },
768 "1": Dehydrated {
769 "preview_short": Array(2),
770 "preview_long": ["second", Map(2)],
771 },
772 },
773 "object_of_objects": {
774 "inner": Dehydrated {
775 "preview_short": {…},
776 "preview_long": {boolean: true, number: 123, string: "abc"},
777 },
778 },
779 "object_with_symbol": {
780 "Symbol(name)": "hello",
781 },
782 "proxy": Dehydrated {
783 "preview_short": () => {},
784 "preview_long": () => {},
785 },
786 "react_element": {
787 "key": null,
788 "props": Dehydrated {
789 "preview_short": {…},
790 "preview_long": {},
791 },
792 },
793 "react_lazy": {
794 "_payload": Dehydrated {
795 "preview_short": {…},
796 "preview_long": {_ioInfo: {…}, _result: () => {}, _status: -1},
797 },
798 },
799 "regexp": Dehydrated {
800 "preview_short": /abc/giu,
801 "preview_long": /abc/giu,
802 },
803 "set": {
804 "0": "abc",
805 "1": 123,
806 },
807 "set_of_sets": {
808 "0": Dehydrated {
809 "preview_short": Set(3),
810 "preview_long": Set(3) {"a", "b", "c"},
811 },
812 "1": Dehydrated {
813 "preview_short": Set(3),
814 "preview_long": Set(3) {1, 2, 3},
815 },
816 },
817 "symbol": Dehydrated {
818 "preview_short": Symbol(symbol),
819 "preview_long": Symbol(symbol),
820 },
821 "typed_array": {
822 "0": 100,
823 "1": -100,
824 "2": 0,
825 },
826 }
827 `);
828 });
829
830 it('should support Thenables in React 19', async () => {
831 const Example = () => null;
832
833 class SubclassedPromise extends Promise {}
834
835 const plainThenable = {then() {}};
836 const subclassedPromise = new SubclassedPromise(() => {});
837 const unusedPromise = Promise.resolve();
838 const usedFulfilledPromise = Promise.resolve();
839 const usedFulfilledRichPromise = Promise.resolve({
840 some: {
841 deeply: {
842 nested: {
843 object: {
844 string: 'test',
845 fn: () => {},
846 },
847 },
848 },
849 },
850 });
851 const usedPendingPromise = new Promise(resolve => {});
852 const usedRejectedPromise = Promise.reject(
853 new Error('test-error-do-not-surface'),
854 );
855
856 function Use({value}) {
857 React.use(value);
858 }
859
860 await utils.actAsync(() =>
861 render(
862 <>
863 <Example
864 plainThenable={plainThenable}
865 subclassedPromise={subclassedPromise}
866 unusedPromise={unusedPromise}
867 usedFulfilledPromise={usedFulfilledPromise}
868 usedFulfilledRichPromise={usedFulfilledRichPromise}
869 usedPendingPromise={usedPendingPromise}
870 usedRejectedPromise={usedRejectedPromise}
871 />
872 <React.Suspense>
873 <Use value={usedPendingPromise} />
874 </React.Suspense>
875 <React.Suspense>
876 <Use value={usedFulfilledPromise} />
877 </React.Suspense>
878 <React.Suspense>
879 <Use value={usedFulfilledRichPromise} />
880 </React.Suspense>
881 <ErrorBoundary>
882 <React.Suspense>
883 <Use value={usedRejectedPromise} />
884 </React.Suspense>
885 </ErrorBoundary>
886 </>,
887 ),
888 );
889
890 const inspectedElement = await inspectElementAtIndex(0);
891
892 expect(inspectedElement.props).toMatchInlineSnapshot(`
893 {
894 "plainThenable": Dehydrated {
895 "preview_short": Thenable,
896 "preview_long": Thenable,
897 },
898 "subclassedPromise": Dehydrated {
899 "preview_short": SubclassedPromise,
900 "preview_long": SubclassedPromise,
901 },
902 "unusedPromise": Dehydrated {
903 "preview_short": Promise,
904 "preview_long": Promise,
905 },
906 "usedFulfilledPromise": {
907 "value": undefined,
908 },
909 "usedFulfilledRichPromise": {
910 "value": Dehydrated {
911 "preview_short": {…},
912 "preview_long": {some: {…}},
913 },
914 },
915 "usedPendingPromise": Dehydrated {
916 "preview_short": pending Promise,
917 "preview_long": pending Promise,
918 },
919 "usedRejectedPromise": {
920 "reason": Dehydrated {
921 "preview_short": Error: test-error-do-not-surface,
922 "preview_long": Error: test-error-do-not-surface,
923 },
924 },
925 }
926 `);
927 });
928
929 it('should support Promises in React 18', async () => {
930 const Example = () => null;
931
932 const unusedPromise = Promise.resolve();
933
934 await utils.actAsync(() =>
935 render(
936 <>
937 <Example unusedPromise={unusedPromise} />
938 </>,
939 ),
940 );
941
942 const inspectedElement = await inspectElementAtIndex(0);
943
944 expect(inspectedElement.props).toMatchInlineSnapshot(`
945 {
946 "unusedPromise": Dehydrated {
947 "preview_short": Promise,
948 "preview_long": Promise,
949 },
950 }
951 `);
952 });
953
954 it('should not consume iterables while inspecting', async () => {
955 const Example = () => null;
956
957 function* generator() {
958 throw Error('Should not be consumed!');
959 }
960
961 const iterable = generator();
962 await utils.actAsync(() => render(<Example prop={iterable} />));
963
964 const inspectedElement = await inspectElementAtIndex(0);
965 expect(inspectedElement.props).toMatchInlineSnapshot(`
966 {
967 "prop": Dehydrated {
968 "preview_short": Generator,
969 "preview_long": Generator,
970 },
971 }
972 `);
973 });
974
975 it('should support objects with no prototype', async () => {
976 const Example = () => null;
977
978 const object = Object.create(null);
979 object.string = 'abc';
980 object.number = 123;
981 object.boolean = true;
982
983 await utils.actAsync(() => render(<Example object={object} />));
984
985 const inspectedElement = await inspectElementAtIndex(0);
986 expect(inspectedElement.props).toMatchInlineSnapshot(`
987 {
988 "object": {
989 "boolean": true,
990 "number": 123,
991 "string": "abc",
992 },
993 }
994 `);
995 });
996
997 it('should support objects with overridden hasOwnProperty', async () => {
998 const Example = () => null;
999
1000 const object = {
1001 name: 'blah',
1002 hasOwnProperty: true,
1003 };
1004
1005 await utils.actAsync(() => render(<Example object={object} />));
1006
1007 const inspectedElement = await inspectElementAtIndex(0);
1008
1009 expect(inspectedElement.props).toMatchInlineSnapshot(`
1010 {
1011 "object": {
1012 "hasOwnProperty": true,
1013 "name": "blah",
1014 },
1015 }
1016 `);
1017 });
1018
1019 it('should support custom objects with enumerable properties and getters', async () => {
1020 class CustomData {
1021 _number = 42;
1022 get number() {
1023 return this._number;
1024 }
1025 set number(value) {
1026 this._number = value;
1027 }
1028 }
1029
1030 const descriptor = ((Object.getOwnPropertyDescriptor(
1031 CustomData.prototype,
1032 'number',
1033 ): any): PropertyDescriptor<number>);
1034 descriptor.enumerable = true;
1035 Object.defineProperty(CustomData.prototype, 'number', descriptor);
1036
1037 const Example = () => null;
1038
1039 await utils.actAsync(() => render(<Example data={new CustomData()} />));
1040
1041 const inspectedElement = await inspectElementAtIndex(0);
1042 expect(inspectedElement.props).toMatchInlineSnapshot(`
1043 {
1044 "data": {
1045 "_number": 42,
1046 "number": 42,
1047 },
1048 }
1049 `);
1050 });
1051
1052 it('should support objects with inherited keys', async () => {
1053 const Example = () => null;
1054
1055 const base = Object.create(Object.prototype, {
1056 enumerableStringBase: {
1057 value: 1,
1058 writable: true,
1059 enumerable: true,
1060 configurable: true,
1061 },
1062 [Symbol('enumerableSymbolBase')]: {
1063 value: 1,
1064 writable: true,
1065 enumerable: true,
1066 configurable: true,
1067 },
1068 nonEnumerableStringBase: {
1069 value: 1,
1070 writable: true,
1071 enumerable: false,
1072 configurable: true,
1073 },
1074 [Symbol('nonEnumerableSymbolBase')]: {
1075 value: 1,
1076 writable: true,
1077 enumerable: false,
1078 configurable: true,
1079 },
1080 });
1081
1082 const object = Object.create(base, {
1083 enumerableString: {
1084 value: 2,
1085 writable: true,
1086 enumerable: true,
1087 configurable: true,
1088 },
1089 nonEnumerableString: {
1090 value: 3,
1091 writable: true,
1092 enumerable: false,
1093 configurable: true,
1094 },
1095 123: {
1096 value: 3,
1097 writable: true,
1098 enumerable: true,
1099 configurable: true,
1100 },
1101 [Symbol('nonEnumerableSymbol')]: {
1102 value: 2,
1103 writable: true,
1104 enumerable: false,
1105 configurable: true,
1106 },
1107 [Symbol('enumerableSymbol')]: {
1108 value: 3,
1109 writable: true,
1110 enumerable: true,
1111 configurable: true,
1112 },
1113 });
1114
1115 await utils.actAsync(() => render(<Example object={object} />));
1116
1117 const inspectedElement = await inspectElementAtIndex(0);
1118 expect(inspectedElement.props).toMatchInlineSnapshot(`
1119 {
1120 "object": {
1121 "123": 3,
1122 "Symbol(enumerableSymbol)": 3,
1123 "Symbol(enumerableSymbolBase)": 1,
1124 "enumerableString": 2,
1125 "enumerableStringBase": 1,
1126 },
1127 }
1128 `);
1129 });
1130
1131 it('should allow component prop value and value`s prototype has same name params.', async () => {
1132 const testData = Object.create(
1133 {
1134 a: undefined,
1135 b: Infinity,
1136 c: NaN,
1137 d: 'normal',
1138 },
1139 {
1140 a: {
1141 value: undefined,
1142 writable: true,
1143 enumerable: true,
1144 configurable: true,
1145 },
1146 b: {
1147 value: Infinity,
1148 writable: true,
1149 enumerable: true,
1150 configurable: true,
1151 },
1152 c: {
1153 value: NaN,
1154 writable: true,
1155 enumerable: true,
1156 configurable: true,
1157 },
1158 d: {
1159 value: 'normal',
1160 writable: true,
1161 enumerable: true,
1162 configurable: true,
1163 },
1164 },
1165 );
1166 const Example = ({data}) => null;
1167
1168 await utils.actAsync(() => render(<Example data={testData} />));
1169
1170 const inspectedElement = await inspectElementAtIndex(0);
1171 expect(inspectedElement.props).toMatchInlineSnapshot(`
1172 {
1173 "data": {
1174 "a": undefined,
1175 "b": Infinity,
1176 "c": NaN,
1177 "d": "normal",
1178 },
1179 }
1180 `);
1181 });
1182
1183 it('should not dehydrate nested values until explicitly requested', async () => {
1184 const Example = () => {
1185 const [state] = React.useState({
1186 foo: {
1187 bar: {
1188 baz: 'hi',
1189 },
1190 },
1191 });
1192
1193 return state.foo.bar.baz;
1194 };
1195
1196 await utils.actAsync(() =>
1197 render(
1198 <Example
1199 nestedObject={{
1200 a: {
1201 b: {
1202 c: [
1203 {
1204 d: {
1205 e: {},
1206 },
1207 },
1208 ],
1209 },
1210 },
1211 }}
1212 />,
1213 ),
1214 );
1215
1216 let inspectedElement = null;
1217 let inspectElementPath = null;
1218
1219 // Render once to get a handle on inspectElementPath()
1220 inspectedElement = await inspectElementAtIndex(0, () => {
1221 inspectElementPath = useInspectElementPath();
1222 });
1223
1224 async function loadPath(path) {
1225 await TestUtilsAct(async () => {
1226 await TestRendererAct(async () => {
1227 inspectElementPath(path);
1228 });
1229 });
1230
1231 inspectedElement = await inspectElementAtIndex(0);
1232 }
1233
1234 expect(inspectedElement.props).toMatchInlineSnapshot(`
1235 {
1236 "nestedObject": {
1237 "a": Dehydrated {
1238 "preview_short": {…},
1239 "preview_long": {b: {…}},
1240 },
1241 },
1242 }
1243 `);
1244
1245 await loadPath(['props', 'nestedObject', 'a']);
1246
1247 expect(inspectedElement.props).toMatchInlineSnapshot(`
1248 {
1249 "nestedObject": {
1250 "a": {
1251 "b": {
1252 "c": Dehydrated {
1253 "preview_short": Array(1),
1254 "preview_long": [{…}],
1255 },
1256 },
1257 },
1258 },
1259 }
1260 `);
1261
1262 await loadPath(['props', 'nestedObject', 'a', 'b', 'c']);
1263
1264 expect(inspectedElement.props).toMatchInlineSnapshot(`
1265 {
1266 "nestedObject": {
1267 "a": {
1268 "b": {
1269 "c": [
1270 {
1271 "d": Dehydrated {
1272 "preview_short": {…},
1273 "preview_long": {e: {…}},
1274 },
1275 },
1276 ],
1277 },
1278 },
1279 },
1280 }
1281 `);
1282
1283 await loadPath(['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);
1284
1285 expect(inspectedElement.props).toMatchInlineSnapshot(`
1286 {
1287 "nestedObject": {
1288 "a": {
1289 "b": {
1290 "c": [
1291 {
1292 "d": {
1293 "e": {},
1294 },
1295 },
1296 ],
1297 },
1298 },
1299 },
1300 }
1301 `);
1302
1303 await loadPath(['hooks', 0, 'value']);
1304
1305 expect(inspectedElement.hooks).toMatchInlineSnapshot(`
1306 [
1307 {
1308 "debugInfo": null,
1309 "hookSource": {
1310 "columnNumber": "removed by Jest serializer",
1311 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
1312 "functionName": "Example",
1313 "lineNumber": "removed by Jest serializer",
1314 },
1315 "id": 0,
1316 "isStateEditable": true,
1317 "name": "State",
1318 "subHooks": [],
1319 "value": {
1320 "foo": {
1321 "bar": Dehydrated {
1322 "preview_short": {…},
1323 "preview_long": {baz: "hi"},
1324 },
1325 },
1326 },
1327 },
1328 ]
1329 `);
1330
1331 await loadPath(['hooks', 0, 'value', 'foo', 'bar']);
1332
1333 expect(inspectedElement.hooks).toMatchInlineSnapshot(`
1334 [
1335 {
1336 "debugInfo": null,
1337 "hookSource": {
1338 "columnNumber": "removed by Jest serializer",
1339 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
1340 "functionName": "Example",
1341 "lineNumber": "removed by Jest serializer",
1342 },
1343 "id": 0,
1344 "isStateEditable": true,
1345 "name": "State",
1346 "subHooks": [],
1347 "value": {
1348 "foo": {
1349 "bar": {
1350 "baz": "hi",
1351 },
1352 },
1353 },
1354 },
1355 ]
1356 `);
1357 });
1358
1359 it('should dehydrate complex nested values when requested', async () => {
1360 const Example = () => null;
1361
1362 await utils.actAsync(() =>
1363 render(
1364 <Example
1365 set_of_sets={new Set([new Set([1, 2, 3]), new Set(['a', 'b', 'c'])])}
1366 />,
1367 ),
1368 );
1369
1370 let inspectedElement = null;
1371 let inspectElementPath = null;
1372
1373 // Render once to get a handle on inspectElementPath()
1374 inspectedElement = await inspectElementAtIndex(0, () => {
1375 inspectElementPath = useInspectElementPath();
1376 });
1377
1378 async function loadPath(path) {
1379 await TestUtilsAct(async () => {
1380 await TestRendererAct(async () => {
1381 inspectElementPath(path);
1382 });
1383 });
1384
1385 inspectedElement = await inspectElementAtIndex(0);
1386 }
1387
1388 expect(inspectedElement.props).toMatchInlineSnapshot(`
1389 {
1390 "set_of_sets": {
1391 "0": Dehydrated {
1392 "preview_short": Set(3),
1393 "preview_long": Set(3) {1, 2, 3},
1394 },
1395 "1": Dehydrated {
1396 "preview_short": Set(3),
1397 "preview_long": Set(3) {"a", "b", "c"},
1398 },
1399 },
1400 }
1401 `);
1402
1403 await loadPath(['props', 'set_of_sets', 0]);
1404
1405 expect(inspectedElement.props).toMatchInlineSnapshot(`
1406 {
1407 "set_of_sets": {
1408 "0": {
1409 "0": 1,
1410 "1": 2,
1411 "2": 3,
1412 },
1413 "1": Dehydrated {
1414 "preview_short": Set(3),
1415 "preview_long": Set(3) {"a", "b", "c"},
1416 },
1417 },
1418 }
1419 `);
1420 });
1421
1422 it('should include updates for nested values that were previously hydrated', async () => {
1423 const Example = () => null;
1424
1425 await utils.actAsync(() =>
1426 render(
1427 <Example
1428 nestedObject={{
1429 a: {
1430 value: 1,
1431 b: {
1432 value: 1,
1433 },
1434 },
1435 c: {
1436 value: 1,
1437 d: {
1438 value: 1,
1439 e: {
1440 value: 1,
1441 },
1442 },
1443 },
1444 }}
1445 />,
1446 ),
1447 );
1448
1449 let inspectedElement = null;
1450 let inspectElementPath = null;
1451
1452 // Render once to get a handle on inspectElementPath()
1453 inspectedElement = await inspectElementAtIndex(0, () => {
1454 inspectElementPath = useInspectElementPath();
1455 });
1456
1457 async function loadPath(path) {
1458 await TestUtilsAct(async () => {
1459 await TestRendererAct(async () => {
1460 inspectElementPath(path);
1461 });
1462 });
1463
1464 inspectedElement = await inspectElementAtIndex(0);
1465 }
1466
1467 expect(inspectedElement.props).toMatchInlineSnapshot(`
1468 {
1469 "nestedObject": {
1470 "a": Dehydrated {
1471 "preview_short": {…},
1472 "preview_long": {b: {…}, value: 1},
1473 },
1474 "c": Dehydrated {
1475 "preview_short": {…},
1476 "preview_long": {d: {…}, value: 1},
1477 },
1478 },
1479 }
1480 `);
1481
1482 await loadPath(['props', 'nestedObject', 'a']);
1483
1484 expect(inspectedElement.props).toMatchInlineSnapshot(`
1485 {
1486 "nestedObject": {
1487 "a": {
1488 "b": {
1489 "value": 1,
1490 },
1491 "value": 1,
1492 },
1493 "c": Dehydrated {
1494 "preview_short": {…},
1495 "preview_long": {d: {…}, value: 1},
1496 },
1497 },
1498 }
1499 `);
1500
1501 await loadPath(['props', 'nestedObject', 'c']);
1502
1503 expect(inspectedElement.props).toMatchInlineSnapshot(`
1504 {
1505 "nestedObject": {
1506 "a": {
1507 "b": {
1508 "value": 1,
1509 },
1510 "value": 1,
1511 },
1512 "c": {
1513 "d": {
1514 "e": Dehydrated {
1515 "preview_short": {…},
1516 "preview_long": {value: 1},
1517 },
1518 "value": 1,
1519 },
1520 "value": 1,
1521 },
1522 },
1523 }
1524 `);
1525
1526 await TestRendererAct(async () => {
1527 await TestUtilsAct(async () => {
1528 render(
1529 <Example
1530 nestedObject={{
1531 a: {
1532 value: 2,
1533 b: {
1534 value: 2,
1535 },
1536 },
1537 c: {
1538 value: 2,
1539 d: {
1540 value: 2,
1541 e: {
1542 value: 2,
1543 },
1544 },
1545 },
1546 }}
1547 />,
1548 );
1549 });
1550 });
1551
1552 // Wait for pending poll-for-update and then update inspected element data.
1553 jest.runOnlyPendingTimers();
1554 await Promise.resolve();
1555 inspectedElement = await inspectElementAtIndex(0);
1556
1557 expect(inspectedElement.props).toMatchInlineSnapshot(`
1558 {
1559 "nestedObject": {
1560 "a": {
1561 "b": {
1562 "value": 2,
1563 },
1564 "value": 2,
1565 },
1566 "c": {
1567 "d": {
1568 "e": Dehydrated {
1569 "preview_short": {…},
1570 "preview_long": {value: 2},
1571 },
1572 "value": 2,
1573 },
1574 "value": 2,
1575 },
1576 },
1577 }
1578 `);
1579 });
1580
1581 it('should return a full update if a path is inspected for an object that has other pending changes', async () => {
1582 const Example = () => null;
1583
1584 await utils.actAsync(() =>
1585 render(
1586 <Example
1587 nestedObject={{
1588 a: {
1589 value: 1,
1590 b: {
1591 value: 1,
1592 },
1593 },
1594 c: {
1595 value: 1,
1596 d: {
1597 value: 1,
1598 e: {
1599 value: 1,
1600 },
1601 },
1602 },
1603 }}
1604 />,
1605 ),
1606 );
1607
1608 let inspectedElement = null;
1609 let inspectElementPath = null;
1610
1611 // Render once to get a handle on inspectElementPath()
1612 inspectedElement = await inspectElementAtIndex(0, () => {
1613 inspectElementPath = useInspectElementPath();
1614 });
1615
1616 async function loadPath(path) {
1617 await TestUtilsAct(async () => {
1618 await TestRendererAct(() => {
1619 inspectElementPath(path);
1620 });
1621 });
1622
1623 inspectedElement = await inspectElementAtIndex(0);
1624 }
1625
1626 expect(inspectedElement.props).toMatchInlineSnapshot(`
1627 {
1628 "nestedObject": {
1629 "a": Dehydrated {
1630 "preview_short": {…},
1631 "preview_long": {b: {…}, value: 1},
1632 },
1633 "c": Dehydrated {
1634 "preview_short": {…},
1635 "preview_long": {d: {…}, value: 1},
1636 },
1637 },
1638 }
1639 `);
1640
1641 await loadPath(['props', 'nestedObject', 'a']);
1642
1643 expect(inspectedElement.props).toMatchInlineSnapshot(`
1644 {
1645 "nestedObject": {
1646 "a": {
1647 "b": {
1648 "value": 1,
1649 },
1650 "value": 1,
1651 },
1652 "c": Dehydrated {
1653 "preview_short": {…},
1654 "preview_long": {d: {…}, value: 1},
1655 },
1656 },
1657 }
1658 `);
1659
1660 await TestRendererAct(async () => {
1661 await TestUtilsAct(async () => {
1662 render(
1663 <Example
1664 nestedObject={{
1665 a: {
1666 value: 2,
1667 b: {
1668 value: 2,
1669 },
1670 },
1671 c: {
1672 value: 2,
1673 d: {
1674 value: 2,
1675 e: {
1676 value: 2,
1677 },
1678 },
1679 },
1680 }}
1681 />,
1682 );
1683 });
1684 });
1685
1686 await loadPath(['props', 'nestedObject', 'c']);
1687
1688 expect(inspectedElement.props).toMatchInlineSnapshot(`
1689 {
1690 "nestedObject": {
1691 "a": {
1692 "b": {
1693 "value": 2,
1694 },
1695 "value": 2,
1696 },
1697 "c": {
1698 "d": {
1699 "e": Dehydrated {
1700 "preview_short": {…},
1701 "preview_long": {value: 2},
1702 },
1703 "value": 2,
1704 },
1705 "value": 2,
1706 },
1707 },
1708 }
1709 `);
1710 });
1711
1712 it('should not tear if hydration is requested after an update', async () => {
1713 const Example = () => null;
1714
1715 await utils.actAsync(() =>
1716 render(
1717 <Example
1718 nestedObject={{
1719 value: 1,
1720 a: {
1721 value: 1,
1722 b: {
1723 value: 1,
1724 },
1725 },
1726 }}
1727 />,
1728 ),
1729 );
1730
1731 let inspectedElement = null;
1732 let inspectElementPath = null;
1733
1734 // Render once to get a handle on inspectElementPath()
1735 inspectedElement = await inspectElementAtIndex(0, () => {
1736 inspectElementPath = useInspectElementPath();
1737 });
1738
1739 async function loadPath(path) {
1740 await TestUtilsAct(async () => {
1741 await TestRendererAct(() => {
1742 inspectElementPath(path);
1743 });
1744 });
1745
1746 inspectedElement = await inspectElementAtIndex(0);
1747 }
1748
1749 expect(inspectedElement.props).toMatchInlineSnapshot(`
1750 {
1751 "nestedObject": {
1752 "a": Dehydrated {
1753 "preview_short": {…},
1754 "preview_long": {b: {…}, value: 1},
1755 },
1756 "value": 1,
1757 },
1758 }
1759 `);
1760
1761 await TestUtilsAct(async () => {
1762 render(
1763 <Example
1764 nestedObject={{
1765 value: 2,
1766 a: {
1767 value: 2,
1768 b: {
1769 value: 2,
1770 },
1771 },
1772 }}
1773 />,
1774 );
1775 });
1776
1777 await loadPath(['props', 'nestedObject', 'a']);
1778
1779 expect(inspectedElement.props).toMatchInlineSnapshot(`
1780 {
1781 "nestedObject": {
1782 "a": {
1783 "b": {
1784 "value": 2,
1785 },
1786 "value": 2,
1787 },
1788 "value": 2,
1789 },
1790 }
1791 `);
1792 });
1793
1794 // TODO(hoxyq): Enable this test for versions ~18, currently broken
1795 // @reactVersion <= 18.2
1796 // eslint-disable-next-line jest/no-disabled-tests
1797 it.skip('should inspect hooks for components that only use context (legacy render)', async () => {
1798 const Context = React.createContext(true);
1799 const Example = () => {
1800 const value = React.useContext(Context);
1801 return value;
1802 };
1803
1804 await utils.actAsync(() => legacyRender(<Example a={1} b="abc" />));
1805
1806 const inspectedElement = await inspectElementAtIndex(0);
1807 expect(inspectedElement).toMatchInlineSnapshot(`
1808 {
1809 "context": null,
1810 "events": undefined,
1811 "hooks": [
1812 {
1813 "debugInfo": null,
1814 "hookSource": {
1815 "columnNumber": "removed by Jest serializer",
1816 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
1817 "functionName": "Example",
1818 "lineNumber": "removed by Jest serializer",
1819 },
1820 "id": null,
1821 "isStateEditable": false,
1822 "name": "Context",
1823 "subHooks": [],
1824 "value": true,
1825 },
1826 ],
1827 "id": 2,
1828 "owners": null,
1829 "props": {
1830 "a": 1,
1831 "b": "abc",
1832 },
1833 "rootType": "render()",
1834 "state": null,
1835 }
1836 `);
1837 });
1838
1839 it('should inspect hooks for components that only use context (createRoot)', async () => {
1840 const Context = React.createContext(true);
1841 const Example = () => {
1842 const value = React.useContext(Context);
1843 return value;
1844 };
1845
1846 await utils.actAsync(() => modernRender(<Example a={1} b="abc" />));
1847
1848 const inspectedElement = await inspectElementAtIndex(0);
1849 expect(inspectedElement).toMatchInlineSnapshot(`
1850 {
1851 "context": null,
1852 "events": undefined,
1853 "hooks": [
1854 {
1855 "debugInfo": null,
1856 "hookSource": {
1857 "columnNumber": "removed by Jest serializer",
1858 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
1859 "functionName": "Example",
1860 "lineNumber": "removed by Jest serializer",
1861 },
1862 "id": null,
1863 "isStateEditable": false,
1864 "name": "Context",
1865 "subHooks": [],
1866 "value": true,
1867 },
1868 ],
1869 "id": 2,
1870 "owners": null,
1871 "props": {
1872 "a": 1,
1873 "b": "abc",
1874 },
1875 "rootType": "createRoot()",
1876 "state": null,
1877 }
1878 `);
1879 });
1880
1881 it('should enable inspected values to be stored as global variables', async () => {
1882 const Example = () => null;
1883
1884 const nestedObject = {
1885 a: {
1886 value: 1,
1887 b: {
1888 value: 1,
1889 c: {
1890 value: 1,
1891 },
1892 },
1893 },
1894 };
1895
1896 await utils.actAsync(() => render(<Example nestedObject={nestedObject} />));
1897
1898 let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal);
1899
1900 const id = ((store.getElementIDAtIndex(0): any): number);
1901 await inspectElementAtIndex(0, () => {
1902 storeAsGlobal = (path: Array<string | number>) => {
1903 const rendererID = store.getRendererIDForElement(id);
1904 if (rendererID !== null) {
1905 const {
1906 storeAsGlobal: storeAsGlobalAPI,
1907 } = require('react-devtools-shared/src/backendAPI');
1908 storeAsGlobalAPI({
1909 bridge,
1910 id,
1911 path,
1912 rendererID,
1913 });
1914 }
1915 };
1916 });
1917
1918 jest.spyOn(console, 'log').mockImplementation(() => {});
1919
1920 // Should store the whole value (not just the hydrated parts)
1921 storeAsGlobal(['props', 'nestedObject']);
1922 jest.runOnlyPendingTimers();
1923 expect(console.log).toHaveBeenCalledWith('$reactTemp0');
1924 expect(global.$reactTemp0).toBe(nestedObject);
1925
1926 console.log.mockReset();
1927
1928 // Should store the nested property specified (not just the outer value)
1929 storeAsGlobal(['props', 'nestedObject', 'a', 'b']);
1930 jest.runOnlyPendingTimers();
1931 expect(console.log).toHaveBeenCalledWith('$reactTemp1');
1932 expect(global.$reactTemp1).toBe(nestedObject.a.b);
1933 });
1934
1935 it('should enable inspected values to be copied to the clipboard', async () => {
1936 const Example = () => null;
1937
1938 const nestedObject = {
1939 a: {
1940 value: 1,
1941 b: {
1942 value: 1,
1943 c: {
1944 value: 1,
1945 },
1946 },
1947 },
1948 };
1949
1950 await utils.actAsync(() => render(<Example nestedObject={nestedObject} />));
1951
1952 let copyPath: CopyInspectedElementPath =
1953 ((null: any): CopyInspectedElementPath);
1954
1955 const id = ((store.getElementIDAtIndex(0): any): number);
1956 await inspectElementAtIndex(0, () => {
1957 copyPath = (path: Array<string | number>) => {
1958 const rendererID = store.getRendererIDForElement(id);
1959 if (rendererID !== null) {
1960 const {
1961 copyInspectedElementPath,
1962 } = require('react-devtools-shared/src/backendAPI');
1963 copyInspectedElementPath({
1964 bridge,
1965 id,
1966 path,
1967 rendererID,
1968 });
1969 }
1970 };
1971 });
1972
1973 // Should copy the whole value (not just the hydrated parts)
1974 copyPath(['props', 'nestedObject']);
1975 jest.runOnlyPendingTimers();
1976 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
1977 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
1978 JSON.stringify(nestedObject, undefined, 2),
1979 );
1980
1981 global.mockClipboardCopy.mockReset();
1982
1983 // Should copy the nested property specified (not just the outer value)
1984 copyPath(['props', 'nestedObject', 'a', 'b']);
1985 jest.runOnlyPendingTimers();
1986 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
1987 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
1988 JSON.stringify(nestedObject.a.b, undefined, 2),
1989 );
1990 });
1991
1992 it('should enable complex values to be copied to the clipboard', async () => {
1993 const Immutable = require('immutable');
1994
1995 const Example = () => null;
1996
1997 const set = new Set(['abc', 123]);
1998 const map = new Map([
1999 ['name', 'Brian'],
2000 ['food', 'sushi'],
2001 ]);
2002 const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
2003 const mapOfMaps = new Map([
2004 ['first', map],
2005 ['second', map],
2006 ]);
2007 const typedArray = Int8Array.from([100, -100, 0]);
2008 const arrayBuffer = typedArray.buffer;
2009 const dataView = new DataView(arrayBuffer);
2010 const immutable = Immutable.fromJS({
2011 a: [{hello: 'there'}, 'fixed', true],
2012 b: 123,
2013 c: {
2014 '1': 'xyz',
2015 xyz: 1,
2016 },
2017 });
2018 const bigInt = BigInt(123);
2019
2020 await utils.actAsync(() =>
2021 render(
2022 <Example
2023 arrayBuffer={arrayBuffer}
2024 dataView={dataView}
2025 map={map}
2026 set={set}
2027 mapOfMaps={mapOfMaps}
2028 setOfSets={setOfSets}
2029 typedArray={typedArray}
2030 immutable={immutable}
2031 bigInt={bigInt}
2032 />,
2033 ),
2034 );
2035
2036 const id = ((store.getElementIDAtIndex(0): any): number);
2037
2038 let copyPath: CopyInspectedElementPath =
2039 ((null: any): CopyInspectedElementPath);
2040
2041 await inspectElementAtIndex(0, () => {
2042 copyPath = (path: Array<string | number>) => {
2043 const rendererID = store.getRendererIDForElement(id);
2044 if (rendererID !== null) {
2045 const {
2046 copyInspectedElementPath,
2047 } = require('react-devtools-shared/src/backendAPI');
2048 copyInspectedElementPath({
2049 bridge,
2050 id,
2051 path,
2052 rendererID,
2053 });
2054 }
2055 };
2056 });
2057
2058 // Should copy the whole value (not just the hydrated parts)
2059 copyPath(['props']);
2060 jest.runOnlyPendingTimers();
2061 // Should not error despite lots of unserialized values.
2062
2063 global.mockClipboardCopy.mockReset();
2064
2065 // Should copy the nested property specified (not just the outer value)
2066 copyPath(['props', 'bigInt']);
2067 jest.runOnlyPendingTimers();
2068 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
2069 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
2070 JSON.stringify('123n', undefined, 2),
2071 );
2072
2073 global.mockClipboardCopy.mockReset();
2074
2075 // Should copy the nested property specified (not just the outer value)
2076 copyPath(['props', 'typedArray']);
2077 jest.runOnlyPendingTimers();
2078 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
2079 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
2080 JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
2081 );
2082 });
2083
2084 it('should display complex values of useDebugValue', async () => {
2085 function useDebuggableHook() {
2086 React.useDebugValue({foo: 2});
2087 React.useState(1);
2088 return 1;
2089 }
2090 function DisplayedComplexValue() {
2091 useDebuggableHook();
2092 return null;
2093 }
2094
2095 await utils.actAsync(() => render(<DisplayedComplexValue />));
2096
2097 const {hooks} = await inspectElementAtIndex(0);
2098 expect(hooks).toMatchInlineSnapshot(`
2099 [
2100 {
2101 "debugInfo": null,
2102 "hookSource": {
2103 "columnNumber": "removed by Jest serializer",
2104 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
2105 "functionName": "DisplayedComplexValue",
2106 "lineNumber": "removed by Jest serializer",
2107 },
2108 "id": null,
2109 "isStateEditable": false,
2110 "name": "DebuggableHook",
2111 "subHooks": [
2112 {
2113 "debugInfo": null,
2114 "hookSource": {
2115 "columnNumber": "removed by Jest serializer",
2116 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
2117 "functionName": "useDebuggableHook",
2118 "lineNumber": "removed by Jest serializer",
2119 },
2120 "id": 0,
2121 "isStateEditable": true,
2122 "name": "State",
2123 "subHooks": [],
2124 "value": 1,
2125 },
2126 ],
2127 "value": {
2128 "foo": 2,
2129 },
2130 },
2131 ]
2132 `);
2133 });
2134
2135 // See github.com/facebook/react/issues/21654
2136 it('should support Proxies that dont return an iterator', async () => {
2137 const Example = () => null;
2138 const proxy = new Proxy(
2139 {},
2140 {
2141 get: (target, prop, receiver) => {
2142 target[prop] = value => {};
2143 return target[prop];
2144 },
2145 },
2146 );
2147
2148 await utils.actAsync(() => render(<Example proxy={proxy} />));
2149
2150 const inspectedElement = await inspectElementAtIndex(0);
2151
2152 expect(inspectedElement.props).toMatchInlineSnapshot(`
2153 {
2154 "proxy": {
2155 "$$typeof": Dehydrated {
2156 "preview_short": () => {},
2157 "preview_long": () => {},
2158 },
2159 "Symbol(Symbol.iterator)": Dehydrated {
2160 "preview_short": () => {},
2161 "preview_long": () => {},
2162 },
2163 "constructor": Dehydrated {
2164 "preview_short": () => {},
2165 "preview_long": () => {},
2166 },
2167 },
2168 }
2169 `);
2170 });
2171
2172 // TODO(hoxyq): Enable this test for versions ~18, currently broken
2173 // Regression test for github.com/facebook/react/issues/22099
2174 // @reactVersion <= 18.2
2175 // eslint-disable-next-line jest/no-disabled-tests
2176 it.skip('should not error when an unchanged component is re-inspected after component filters changed (legacy render)', async () => {
2177 const Example = () => <div />;
2178
2179 await utils.actAsync(() => legacyRender(<Example />));
2180
2181 // Select/inspect element
2182 let inspectedElement = await inspectElementAtIndex(0);
2183 expect(inspectedElement).toMatchInlineSnapshot(`
2184 {
2185 "context": null,
2186 "events": undefined,
2187 "hooks": null,
2188 "id": 2,
2189 "owners": null,
2190 "props": {},
2191 "rootType": "render()",
2192 "state": null,
2193 }
2194 `);
2195
2196 await utils.actAsync(async () => {
2197 // Ignore transient warning this causes
2198 withErrorsOrWarningsIgnored(['No element found with id'], () => {
2199 store.componentFilters = [];
2200
2201 // Flush events to the renderer.
2202 jest.runOnlyPendingTimers();
2203 });
2204 }, false);
2205
2206 // HACK: Recreate TestRenderer instance because we rely on default state values
2207 // from props like defaultInspectedElementID and it's easier to reset here than
2208 // to read the TreeDispatcherContext and update the selected ID that way.
2209 // We're testing the inspected values here, not the context wiring, so that's ok.
2210 withErrorsOrWarningsIgnored(
2211 ['An update to %s inside a test was not wrapped in act'],
2212 () => {
2213 testRendererInstance = TestRenderer.create(null, {
2214 unstable_isConcurrent: true,
2215 });
2216 },
2217 );
2218
2219 // Select/inspect the same element again
2220 inspectedElement = await inspectElementAtIndex(0);
2221 expect(inspectedElement).toMatchInlineSnapshot(`
2222 {
2223 "context": null,
2224 "events": undefined,
2225 "hooks": null,
2226 "id": 2,
2227 "owners": null,
2228 "props": {},
2229 "rootType": "render()",
2230 "state": null,
2231 }
2232 `);
2233 });
2234
2235 // Regression test for github.com/facebook/react/issues/22099
2236 it('should not error when an unchanged component is re-inspected after component filters changed (createRoot)', async () => {
2237 const Example = () => <div />;
2238
2239 await utils.actAsync(() => modernRender(<Example />));
2240
2241 // Select/inspect element
2242 let inspectedElement = await inspectElementAtIndex(0);
2243 expect(inspectedElement).toMatchInlineSnapshot(`
2244 {
2245 "context": null,
2246 "events": undefined,
2247 "hooks": null,
2248 "id": 2,
2249 "owners": null,
2250 "props": {},
2251 "rootType": "createRoot()",
2252 "state": null,
2253 }
2254 `);
2255
2256 await utils.actAsync(async () => {
2257 // Ignore transient warning this causes
2258 withErrorsOrWarningsIgnored(['No element found with id'], () => {
2259 store.componentFilters = [];
2260
2261 // Flush events to the renderer.
2262 jest.runOnlyPendingTimers();
2263 });
2264 }, false);
2265
2266 // HACK: Recreate TestRenderer instance because we rely on default state values
2267 // from props like defaultInspectedElementID and it's easier to reset here than
2268 // to read the TreeDispatcherContext and update the selected ID that way.
2269 // We're testing the inspected values here, not the context wiring, so that's ok.
2270 withErrorsOrWarningsIgnored(
2271 ['An update to %s inside a test was not wrapped in act'],
2272 () => {
2273 testRendererInstance = TestRenderer.create(null, {
2274 unstable_isConcurrent: true,
2275 });
2276 },
2277 );
2278
2279 // Select/inspect the same element again
2280 inspectedElement = await inspectElementAtIndex(0);
2281 expect(inspectedElement).toMatchInlineSnapshot(`
2282 {
2283 "context": null,
2284 "events": undefined,
2285 "hooks": null,
2286 "id": 4,
2287 "owners": null,
2288 "props": {},
2289 "rootType": "createRoot()",
2290 "state": null,
2291 }
2292 `);
2293 });
2294
2295 // TODO(hoxyq): Enable this test for versions ~18, currently broken
2296 // @reactVersion <= 18.2
2297 // eslint-disable-next-line jest/no-disabled-tests
2298 it.skip('should display the root type for ReactDOM.hydrate', async () => {
2299 const Example = () => <div />;
2300
2301 await utils.actAsync(() => {
2302 const container = document.createElement('div');
2303 container.innerHTML = '<div></div>';
2304 withErrorsOrWarningsIgnored(
2305 ['ReactDOM.hydrate has not been supported since React 18'],
2306 () => {
2307 ReactDOM.hydrate(<Example />, container);
2308 },
2309 );
2310 }, false);
2311
2312 const inspectedElement = await inspectElementAtIndex(0);
2313 expect(inspectedElement.rootType).toMatchInlineSnapshot(`"hydrate()"`);
2314 });
2315
2316 // TODO(hoxyq): Enable this test for versions ~18, currently broken
2317 // @reactVersion <= 18.2
2318 // eslint-disable-next-line jest/no-disabled-tests
2319 it.skip('should display the root type for ReactDOM.render', async () => {
2320 const Example = () => <div />;
2321
2322 await utils.actAsync(() => {
2323 legacyRender(<Example />);
2324 }, false);
2325
2326 const inspectedElement = await inspectElementAtIndex(0);
2327 expect(inspectedElement.rootType).toMatchInlineSnapshot(`"render()"`);
2328 });
2329
2330 it('should display the root type for ReactDOMClient.hydrateRoot', async () => {
2331 const Example = () => <div />;
2332
2333 await utils.actAsync(() => {
2334 const container = document.createElement('div');
2335 container.innerHTML = '<div></div>';
2336 ReactDOMClient.hydrateRoot(container, <Example />);
2337 }, false);
2338
2339 const inspectedElement = await inspectElementAtIndex(0);
2340 expect(inspectedElement.rootType).toMatchInlineSnapshot(`"hydrateRoot()"`);
2341 });
2342
2343 it('should display the root type for ReactDOMClient.createRoot', async () => {
2344 const Example = () => <div />;
2345
2346 await utils.actAsync(() => {
2347 const container = document.createElement('div');
2348 ReactDOMClient.createRoot(container).render(<Example />);
2349 }, false);
2350
2351 const inspectedElement = await inspectElementAtIndex(0);
2352 expect(inspectedElement.rootType).toMatchInlineSnapshot(`"createRoot()"`);
2353 });
2354
2355 it('should gracefully surface backend errors on the frontend rather than timing out', async () => {
2356 jest.spyOn(console, 'error').mockImplementation(() => {});
2357
2358 let shouldThrow = false;
2359
2360 const Example = () => {
2361 const [count] = React.useState(0);
2362
2363 if (shouldThrow) {
2364 throw Error('Expected');
2365 } else {
2366 return count;
2367 }
2368 };
2369
2370 await utils.actAsync(() => {
2371 render(<Example />);
2372 }, false);
2373
2374 shouldThrow = true;
2375
2376 const value = await inspectElementAtIndex(0, noop, true);
2377
2378 expect(value).toBe(null);
2379
2380 const error = errorBoundaryInstance.state.error;
2381 expect(error.message).toBe('Expected');
2382 expect(error.stack).toContain('inspectHooksOfFiber');
2383 });
2384
2385 describe('$r', () => {
2386 it('should support function components', async () => {
2387 const Example = () => {
2388 const [count] = React.useState(1);
2389 return count;
2390 };
2391
2392 await utils.actAsync(() => render(<Example a={1} b="abc" />));
2393
2394 await inspectElementAtIndex(0);
2395
2396 expect(global.$r).toMatchInlineSnapshot(`
2397 {
2398 "hooks": [
2399 {
2400 "debugInfo": null,
2401 "hookSource": {
2402 "columnNumber": "removed by Jest serializer",
2403 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
2404 "functionName": "Example",
2405 "lineNumber": "removed by Jest serializer",
2406 },
2407 "id": 0,
2408 "isStateEditable": true,
2409 "name": "State",
2410 "subHooks": [],
2411 "value": 1,
2412 },
2413 ],
2414 "props": {
2415 "a": 1,
2416 "b": "abc",
2417 },
2418 "type": [Function],
2419 }
2420 `);
2421 });
2422
2423 it('should support memoized function components', async () => {
2424 const Example = React.memo(function Example(props) {
2425 const [count] = React.useState(1);
2426 return count;
2427 });
2428
2429 await utils.actAsync(() => render(<Example a={1} b="abc" />));
2430
2431 await inspectElementAtIndex(0);
2432
2433 expect(global.$r).toMatchInlineSnapshot(`
2434 {
2435 "hooks": [
2436 {
2437 "debugInfo": null,
2438 "hookSource": {
2439 "columnNumber": "removed by Jest serializer",
2440 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
2441 "functionName": "Example",
2442 "lineNumber": "removed by Jest serializer",
2443 },
2444 "id": 0,
2445 "isStateEditable": true,
2446 "name": "State",
2447 "subHooks": [],
2448 "value": 1,
2449 },
2450 ],
2451 "props": {
2452 "a": 1,
2453 "b": "abc",
2454 },
2455 "type": [Function],
2456 }
2457 `);
2458 });
2459
2460 it('should support forward refs', async () => {
2461 const Example = React.forwardRef(function Example(props, ref) {
2462 const [count] = React.useState(1);
2463 return count;
2464 });
2465
2466 await utils.actAsync(() => render(<Example a={1} b="abc" />));
2467
2468 await inspectElementAtIndex(0);
2469
2470 expect(global.$r).toMatchInlineSnapshot(`
2471 {
2472 "hooks": [
2473 {
2474 "debugInfo": null,
2475 "hookSource": {
2476 "columnNumber": "removed by Jest serializer",
2477 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
2478 "functionName": "Example",
2479 "lineNumber": "removed by Jest serializer",
2480 },
2481 "id": 0,
2482 "isStateEditable": true,
2483 "name": "State",
2484 "subHooks": [],
2485 "value": 1,
2486 },
2487 ],
2488 "props": {
2489 "a": 1,
2490 "b": "abc",
2491 },
2492 "type": [Function],
2493 }
2494 `);
2495 });
2496
2497 it('should support class components', async () => {
2498 class Example extends React.Component {
2499 state = {
2500 count: 0,
2501 };
2502 render() {
2503 return null;
2504 }
2505 }
2506
2507 await utils.actAsync(() => render(<Example a={1} b="abc" />));
2508
2509 await inspectElementAtIndex(0);
2510
2511 expect(global.$r.props).toMatchInlineSnapshot(`
2512 {
2513 "a": 1,
2514 "b": "abc",
2515 }
2516 `);
2517 expect(global.$r.state).toMatchInlineSnapshot(`
2518 {
2519 "count": 0,
2520 }
2521 `);
2522 });
2523 });
2524
2525 describe('inline errors and warnings', () => {
2526 async function getErrorsAndWarningsForElementAtIndex(index) {
2527 const id = ((store.getElementIDAtIndex(index): any): number);
2528 if (id == null) {
2529 throw Error(`Element at index "${index}"" not found in store`);
2530 }
2531
2532 let errors = null;
2533 let warnings = null;
2534
2535 function Suspender({target}) {
2536 const inspectedElement = useInspectedElement();
2537 errors = inspectedElement.errors;
2538 warnings = inspectedElement.warnings;
2539 return null;
2540 }
2541
2542 let root;
2543 await utils.actAsync(() => {
2544 root = TestRenderer.create(
2545 <Contexts
2546 defaultInspectedElementID={id}
2547 defaultInspectedElementIndex={index}>
2548 <React.Suspense fallback={null}>
2549 <Suspender target={id} />
2550 </React.Suspense>
2551 </Contexts>,
2552 {unstable_isConcurrent: true},
2553 );
2554 }, false);
2555 await utils.actAsync(() => {
2556 root.unmount();
2557 }, false);
2558
2559 return {errors, warnings};
2560 }
2561
2562 it('during render get recorded', async () => {
2563 const Example = () => {
2564 console.error('test-only: render error');
2565 console.warn('test-only: render warning');
2566 return null;
2567 };
2568
2569 await withErrorsOrWarningsIgnored(['test-only: '], async () => {
2570 await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
2571 });
2572
2573 const data = await getErrorsAndWarningsForElementAtIndex(0);
2574 expect(data).toMatchInlineSnapshot(`
2575 {
2576 "errors": [
2577 [
2578 "test-only: render error",
2579 1,
2580 ],
2581 ],
2582 "warnings": [
2583 [
2584 "test-only: render warning",
2585 1,
2586 ],
2587 ],
2588 }
2589 `);
2590 });
2591
2592 it('during render get deduped', async () => {
2593 const Example = () => {
2594 console.error('test-only: render error');
2595 console.error('test-only: render error');
2596 console.warn('test-only: render warning');
2597 console.warn('test-only: render warning');
2598 console.warn('test-only: render warning');
2599 return null;
2600 };
2601
2602 await withErrorsOrWarningsIgnored(['test-only:'], async () => {
2603 await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
2604 });
2605 const data = await getErrorsAndWarningsForElementAtIndex(0);
2606 expect(data).toMatchInlineSnapshot(`
2607 {
2608 "errors": [
2609 [
2610 "test-only: render error",
2611 2,
2612 ],
2613 ],
2614 "warnings": [
2615 [
2616 "test-only: render warning",
2617 3,
2618 ],
2619 ],
2620 }
2621 `);
2622 });
2623
2624 it('during layout (mount) get recorded', async () => {
2625 const Example = () => {
2626 // Note we only test mount because once the component unmounts,
2627 // it is no longer in the store and warnings are ignored.
2628 React.useLayoutEffect(() => {
2629 console.error('test-only: useLayoutEffect error');
2630 console.warn('test-only: useLayoutEffect warning');
2631 }, []);
2632 return null;
2633 };
2634
2635 await withErrorsOrWarningsIgnored(['test-only:'], async () => {
2636 await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
2637 });
2638
2639 const data = await getErrorsAndWarningsForElementAtIndex(0);
2640 expect(data).toMatchInlineSnapshot(`
2641 {
2642 "errors": [
2643 [
2644 "test-only: useLayoutEffect error",
2645 1,
2646 ],
2647 ],
2648 "warnings": [
2649 [
2650 "test-only: useLayoutEffect warning",
2651 1,
2652 ],
2653 ],
2654 }
2655 `);
2656 });
2657
2658 it('during passive (mount) get recorded', async () => {
2659 const Example = () => {
2660 // Note we only test mount because once the component unmounts,
2661 // it is no longer in the store and warnings are ignored.
2662 React.useEffect(() => {
2663 console.error('test-only: useEffect error');
2664 console.warn('test-only: useEffect warning');
2665 }, []);
2666 return null;
2667 };
2668
2669 await withErrorsOrWarningsIgnored(['test-only:'], async () => {
2670 await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
2671 });
2672
2673 const data = await getErrorsAndWarningsForElementAtIndex(0);
2674 expect(data).toMatchInlineSnapshot(`
2675 {
2676 "errors": [
2677 [
2678 "test-only: useEffect error",
2679 1,
2680 ],
2681 ],
2682 "warnings": [
2683 [
2684 "test-only: useEffect warning",
2685 1,
2686 ],
2687 ],
2688 }
2689 `);
2690 });
2691
2692 it('from react get recorded without a component stack', async () => {
2693 const Example = () => {
2694 return [<div />];
2695 };
2696
2697 await withErrorsOrWarningsIgnored(
2698 ['Each child in a list should have a unique "key" prop.'],
2699 async () => {
2700 await utils.actAsync(() =>
2701 render(<Example repeatWarningCount={1} />),
2702 );
2703 },
2704 );
2705
2706 const data = await getErrorsAndWarningsForElementAtIndex(0);
2707 expect(data).toMatchInlineSnapshot(`
2708 {
2709 "errors": [],
2710 "warnings": [],
2711 }
2712 `);
2713 });
2714
2715 it('can be cleared for the whole app', async () => {
2716 const Example = () => {
2717 console.error('test-only: render error');
2718 console.warn('test-only: render warning');
2719 return null;
2720 };
2721
2722 await withErrorsOrWarningsIgnored(['test-only:'], async () => {
2723 await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
2724 });
2725
2726 const {
2727 clearErrorsAndWarnings,
2728 } = require('react-devtools-shared/src/backendAPI');
2729 clearErrorsAndWarnings({bridge, store});
2730
2731 // Flush events to the renderer.
2732 jest.runOnlyPendingTimers();
2733
2734 const data = await getErrorsAndWarningsForElementAtIndex(0);
2735 expect(data).toMatchInlineSnapshot(`
2736 {
2737 "errors": [],
2738 "warnings": [],
2739 }
2740 `);
2741 });
2742
2743 it('can be cleared for a particular Fiber (only warnings)', async () => {
2744 const Example = ({id}) => {
2745 console.error(`test-only: render error #${id}`);
2746 console.warn(`test-only: render warning #${id}`);
2747 return null;
2748 };
2749
2750 await withErrorsOrWarningsIgnored(['test-only:'], async () => {
2751 await utils.actAsync(() =>
2752 render(
2753 <React.Fragment>
2754 <Example id={1} />
2755 <Example id={2} />
2756 </React.Fragment>,
2757 ),
2758 );
2759 });
2760
2761 let id = ((store.getElementIDAtIndex(1): any): number);
2762 const rendererID = store.getRendererIDForElement(id);
2763
2764 const {
2765 clearWarningsForElement,
2766 } = require('react-devtools-shared/src/backendAPI');
2767 clearWarningsForElement({bridge, id, rendererID});
2768
2769 // Flush events to the renderer.
2770 jest.runOnlyPendingTimers();
2771
2772 let data = [
2773 await getErrorsAndWarningsForElementAtIndex(0),
2774 await getErrorsAndWarningsForElementAtIndex(1),
2775 ];
2776 expect(data).toMatchInlineSnapshot(`
2777 [
2778 {
2779 "errors": [
2780 [
2781 "test-only: render error #1",
2782 1,
2783 ],
2784 ],
2785 "warnings": [
2786 [
2787 "test-only: render warning #1",
2788 1,
2789 ],
2790 ],
2791 },
2792 {
2793 "errors": [
2794 [
2795 "test-only: render error #2",
2796 1,
2797 ],
2798 ],
2799 "warnings": [],
2800 },
2801 ]
2802 `);
2803
2804 id = ((store.getElementIDAtIndex(0): any): number);
2805 clearWarningsForElement({bridge, id, rendererID});
2806
2807 // Flush events to the renderer.
2808 jest.runOnlyPendingTimers();
2809
2810 data = [
2811 await getErrorsAndWarningsForElementAtIndex(0),
2812 await getErrorsAndWarningsForElementAtIndex(1),
2813 ];
2814 expect(data).toMatchInlineSnapshot(`
2815 [
2816 {
2817 "errors": [
2818 [
2819 "test-only: render error #1",
2820 1,
2821 ],
2822 ],
2823 "warnings": [],
2824 },
2825 {
2826 "errors": [
2827 [
2828 "test-only: render error #2",
2829 1,
2830 ],
2831 ],
2832 "warnings": [],
2833 },
2834 ]
2835 `);
2836 });
2837
2838 it('can be cleared for a particular Fiber (only errors)', async () => {
2839 const Example = ({id}) => {
2840 console.error(`test-only: render error #${id}`);
2841 console.warn(`test-only: render warning #${id}`);
2842 return null;
2843 };
2844
2845 await withErrorsOrWarningsIgnored(['test-only:'], async () => {
2846 await utils.actAsync(() =>
2847 render(
2848 <React.Fragment>
2849 <Example id={1} />
2850 <Example id={2} />
2851 </React.Fragment>,
2852 ),
2853 );
2854 });
2855
2856 let id = ((store.getElementIDAtIndex(1): any): number);
2857 const rendererID = store.getRendererIDForElement(id);
2858
2859 const {
2860 clearErrorsForElement,
2861 } = require('react-devtools-shared/src/backendAPI');
2862 clearErrorsForElement({bridge, id, rendererID});
2863
2864 // Flush events to the renderer.
2865 jest.runOnlyPendingTimers();
2866
2867 let data = [
2868 await getErrorsAndWarningsForElementAtIndex(0),
2869 await getErrorsAndWarningsForElementAtIndex(1),
2870 ];
2871 expect(data).toMatchInlineSnapshot(`
2872 [
2873 {
2874 "errors": [
2875 [
2876 "test-only: render error #1",
2877 1,
2878 ],
2879 ],
2880 "warnings": [
2881 [
2882 "test-only: render warning #1",
2883 1,
2884 ],
2885 ],
2886 },
2887 {
2888 "errors": [],
2889 "warnings": [
2890 [
2891 "test-only: render warning #2",
2892 1,
2893 ],
2894 ],
2895 },
2896 ]
2897 `);
2898
2899 id = ((store.getElementIDAtIndex(0): any): number);
2900 clearErrorsForElement({bridge, id, rendererID});
2901
2902 // Flush events to the renderer.
2903 jest.runOnlyPendingTimers();
2904
2905 data = [
2906 await getErrorsAndWarningsForElementAtIndex(0),
2907 await getErrorsAndWarningsForElementAtIndex(1),
2908 ];
2909 expect(data).toMatchInlineSnapshot(`
2910 [
2911 {
2912 "errors": [],
2913 "warnings": [
2914 [
2915 "test-only: render warning #1",
2916 1,
2917 ],
2918 ],
2919 },
2920 {
2921 "errors": [],
2922 "warnings": [
2923 [
2924 "test-only: render warning #2",
2925 1,
2926 ],
2927 ],
2928 },
2929 ]
2930 `);
2931 });
2932 });
2933
2934 // TODO(hoxyq): Enable this test for versions ~18, currently broken
2935 // @reactVersion <= 18.2
2936 // eslint-disable-next-line jest/no-disabled-tests
2937 it.skip('inspecting nested renderers should not throw (legacy render)', async () => {
2938 // Ignoring react art warnings
2939 jest.spyOn(console, 'error').mockImplementation(() => {});
2940 const ReactArt = require('react-art');
2941 const ArtSVGMode = require('art/modes/svg');
2942 const ARTCurrentMode = require('art/modes/current');
2943 store.componentFilters = [];
2944
2945 ARTCurrentMode.setCurrent(ArtSVGMode);
2946 const {Surface, Group} = ReactArt;
2947
2948 function Child() {
2949 return (
2950 <Surface width={1} height={1}>
2951 <Group />
2952 </Surface>
2953 );
2954 }
2955 function App() {
2956 return <Child />;
2957 }
2958
2959 await utils.actAsync(() => {
2960 legacyRender(<App />);
2961 });
2962 expect(store).toMatchInlineSnapshot(`
2963 [root]
2964 ▾ <App>
2965 ▾ <Child>
2966 ▾ <Surface>
2967 <svg>
2968 [root]
2969 <Group>
2970 `);
2971
2972 const inspectedElement = await inspectElementAtIndex(4);
2973 expect(inspectedElement.owners).toMatchInlineSnapshot(`
2974 [
2975 {
2976 "compiledWithForget": false,
2977 "displayName": "Child",
2978 "hocDisplayNames": null,
2979 "id": 3,
2980 "key": null,
2981 "type": 5,
2982 },
2983 {
2984 "compiledWithForget": false,
2985 "displayName": "App",
2986 "hocDisplayNames": null,
2987 "id": 2,
2988 "key": null,
2989 "type": 5,
2990 },
2991 ]
2992 `);
2993 });
2994
2995 it('inspecting nested renderers should not throw (createRoot)', async () => {
2996 // Ignoring react art warnings
2997 jest.spyOn(console, 'error').mockImplementation(() => {});
2998 const ReactArt = require('react-art');
2999 const ArtSVGMode = require('art/modes/svg');
3000 const ARTCurrentMode = require('art/modes/current');
3001 store.componentFilters = [];
3002
3003 ARTCurrentMode.setCurrent(ArtSVGMode);
3004 const {Surface, Group} = ReactArt;
3005
3006 function Child() {
3007 return (
3008 <Surface width={1} height={1}>
3009 <Group />
3010 </Surface>
3011 );
3012 }
3013 function App() {
3014 return <Child />;
3015 }
3016
3017 await utils.actAsync(() => {
3018 modernRender(<App />);
3019 });
3020 expect(store).toMatchInlineSnapshot(`
3021 [root]
3022 ▾ <App>
3023 ▾ <Child>
3024 ▾ <Surface>
3025 <svg>
3026 [root]
3027 <Group>
3028 `);
3029
3030 const inspectedElement = await inspectElementAtIndex(4);
3031 // TODO: Ideally this should match the owners of the Group but those are
3032 // part of a different parent tree. Ideally the Group would be parent of
3033 // that parent tree though which would fix this issue.
3034 //
3035 // [
3036 // {
3037 // "compiledWithForget": false,
3038 // "displayName": "Child",
3039 // "hocDisplayNames": null,
3040 // "id": 8,
3041 // "key": null,
3042 // "type": 5,
3043 // },
3044 // {
3045 // "compiledWithForget": false,
3046 // "displayName": "App",
3047 // "hocDisplayNames": null,
3048 // "id": 7,
3049 // "key": null,
3050 // "type": 5,
3051 // },
3052 // ]
3053 expect(inspectedElement.owners).toMatchInlineSnapshot(`[]`);
3054 });
3055
3056 describe('error boundary', () => {
3057 it('can toggle error', async () => {
3058 class LocalErrorBoundary extends React.Component<any> {
3059 state = {hasError: false};
3060 static getDerivedStateFromError(error) {
3061 return {hasError: true};
3062 }
3063 render() {
3064 const {hasError} = this.state;
3065 return hasError ? 'has-error' : this.props.children;
3066 }
3067 }
3068
3069 const Example = () => 'example';
3070
3071 await utils.actAsync(() =>
3072 render(
3073 <LocalErrorBoundary>
3074 <Example />
3075 </LocalErrorBoundary>,
3076 ),
3077 );
3078
3079 const targetErrorBoundaryID = ((store.getElementIDAtIndex(
3080 0,
3081 ): any): number);
3082 const inspect = index => {
3083 // HACK: Recreate TestRenderer instance so we can inspect different elements
3084 withErrorsOrWarningsIgnored(
3085 ['An update to %s inside a test was not wrapped in act'],
3086 () => {
3087 testRendererInstance = TestRenderer.create(null, {
3088 unstable_isConcurrent: true,
3089 });
3090 },
3091 );
3092 return inspectElementAtIndex(index);
3093 };
3094 const toggleError = async forceError => {
3095 await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
3096 await TestUtilsAct(async () => {
3097 bridge.send('overrideError', {
3098 id: targetErrorBoundaryID,
3099 rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
3100 forceError,
3101 });
3102 });
3103 });
3104
3105 await TestUtilsAct(async () => {
3106 jest.runOnlyPendingTimers();
3107 });
3108 };
3109
3110 // Inspect <ErrorBoundary /> and see that we cannot toggle error state
3111 // on error boundary itself
3112 let inspectedElement = await inspect(0);
3113 expect(inspectedElement.canToggleError).toBe(true);
3114
3115 // Inspect <Example />
3116 inspectedElement = await inspect(1);
3117 expect(inspectedElement.canToggleError).toBe(true);
3118 expect(inspectedElement.isErrored).toBe(false);
3119
3120 // Suppress expected error and warning.
3121 const consoleErrorMock = jest
3122 .spyOn(console, 'error')
3123 .mockImplementation(() => {});
3124 const consoleWarnMock = jest
3125 .spyOn(console, 'warn')
3126 .mockImplementation(() => {});
3127
3128 // now force error state on <Example />
3129 await toggleError(true);
3130
3131 consoleErrorMock.mockRestore();
3132 consoleWarnMock.mockRestore();
3133
3134 // we are in error state now, <Example /> won't show up
3135 withErrorsOrWarningsIgnored(['Invalid index'], () => {
3136 expect(store.getElementIDAtIndex(1)).toBe(null);
3137 });
3138
3139 // Inpsect <ErrorBoundary /> to toggle off the error state
3140 inspectedElement = await inspect(0);
3141 expect(inspectedElement.canToggleError).toBe(true);
3142 expect(inspectedElement.isErrored).toBe(true);
3143
3144 await toggleError(false);
3145
3146 // We can now inspect <Example /> with ability to toggle again
3147 inspectedElement = await inspect(1);
3148 expect(inspectedElement.canToggleError).toBe(true);
3149 expect(inspectedElement.isErrored).toBe(false);
3150 });
3151 });
3152
3153 it('should properly handle when components filters are updated', async () => {
3154 const Wrapper = ({children}) => children;
3155
3156 let state;
3157 let dispatch;
3158 const Capture = () => {
3159 dispatch = React.useContext(TreeDispatcherContext);
3160 state = React.useContext(TreeStateContext);
3161 return null;
3162 };
3163
3164 function Child({logError = false, logWarning = false}) {
3165 if (logError === true) {
3166 console.error('test-only: error');
3167 }
3168 if (logWarning === true) {
3169 console.warn('test-only: warning');
3170 }
3171 return null;
3172 }
3173
3174 async function selectNextErrorOrWarning() {
3175 await utils.actAsync(
3176 () =>
3177 dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'}),
3178 false,
3179 );
3180 }
3181
3182 async function selectPreviousErrorOrWarning() {
3183 await utils.actAsync(
3184 () =>
3185 dispatch({
3186 type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
3187 }),
3188 false,
3189 );
3190 }
3191
3192 withErrorsOrWarningsIgnored(['test-only:'], () =>
3193 utils.act(() =>
3194 render(
3195 <React.Fragment>
3196 <Wrapper>
3197 <Child logWarning={true} />
3198 </Wrapper>
3199 <Wrapper>
3200 <Wrapper>
3201 <Child logWarning={true} />
3202 </Wrapper>
3203 </Wrapper>
3204 </React.Fragment>,
3205 ),
3206 ),
3207 );
3208
3209 utils.act(() =>
3210 TestRenderer.create(
3211 <Contexts>
3212 <Capture />
3213 </Contexts>,
3214 ),
3215 );
3216 expect(state).toMatchInlineSnapshot(`
3217 ✕ 0, ⚠ 2
3218 [root]
3219 ▾ <Wrapper>
3220 <Child> ⚠
3221 ▾ <Wrapper>
3222 ▾ <Wrapper>
3223 <Child> ⚠
3224 `);
3225
3226 await selectNextErrorOrWarning();
3227 expect(state).toMatchInlineSnapshot(`
3228 ✕ 0, ⚠ 2
3229 [root]
3230 ▾ <Wrapper>
3231 → <Child> ⚠
3232 ▾ <Wrapper>
3233 ▾ <Wrapper>
3234 <Child> ⚠
3235 `);
3236
3237 await utils.actAsync(() => {
3238 store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
3239 jest.runOnlyPendingTimers();
3240 }, false);
3241
3242 expect(state).toMatchInlineSnapshot(`
3243 ✕ 0, ⚠ 2
3244 [root]
3245 → <Child> ⚠
3246 <Child> ⚠
3247 `);
3248
3249 await selectNextErrorOrWarning();
3250 expect(state).toMatchInlineSnapshot(`
3251 ✕ 0, ⚠ 2
3252 [root]
3253 <Child> ⚠
3254 → <Child> ⚠
3255 `);
3256
3257 await utils.actAsync(() => {
3258 store.componentFilters = [];
3259 jest.runOnlyPendingTimers();
3260 }, false);
3261 expect(state).toMatchInlineSnapshot(`
3262 ✕ 0, ⚠ 2
3263 [root]
3264 ▾ <Wrapper>
3265 <Child> ⚠
3266 ▾ <Wrapper>
3267 ▾ <Wrapper>
3268 → <Child> ⚠
3269 `);
3270
3271 await selectPreviousErrorOrWarning();
3272 expect(state).toMatchInlineSnapshot(`
3273 ✕ 0, ⚠ 2
3274 [root]
3275 ▾ <Wrapper>
3276 → <Child> ⚠
3277 ▾ <Wrapper>
3278 ▾ <Wrapper>
3279 <Child> ⚠
3280 `);
3281 });
3282
3283 // @reactVersion > 18.2
3284 it('should inspect server components', async () => {
3285 const ChildPromise = Promise.resolve(<div />);
3286 ChildPromise._debugInfo = [
3287 {
3288 name: 'ServerComponent',
3289 env: 'Server',
3290 owner: null,
3291 },
3292 ];
3293 const Parent = () => ChildPromise;
3294
3295 await utils.actAsync(() => {
3296 modernRender(<Parent />);
3297 });
3298
3299 const inspectedElement = await inspectElementAtIndex(1);
3300 expect(inspectedElement).toMatchInlineSnapshot(`
3301 {
3302 "context": null,
3303 "events": undefined,
3304 "hooks": null,
3305 "id": 3,
3306 "owners": null,
3307 "props": null,
3308 "rootType": "createRoot()",
3309 "state": null,
3310 }
3311 `);
3312 });
3313 });