@samitouri / QOS-React-2 / commits / af16f755dc

Update DevTools to use getCacheForType API (#20548)

DevTools was built with a fork of an early idea for how Suspense cache might work. This idea is incompatible with newer APIs like `useTransition` which unfortunately prevented me from making certain UX improvements. This PR swaps out the primary usage of this cache (there are a few) in favor of the newer `unstable_getCacheForType` and `unstable_useCacheRefresh` APIs. We can go back and update the others in follow up PRs. ### Messaging changes I've refactored the way the frontend loads component props/state/etc to hopefully make it better match the Suspense+cache model. Doing this gave up some of the small optimizations I'd added but hopefully the actual performance impact of that is minor and the overall ergonomic improvements of working with the cache API make this worth it. The backend no longer remembers inspected paths. Instead, the frontend sends them every time and the backend sends a response with those paths. I've also added a new "force" parameter that the frontend can use to tell the backend to send a response even if the component hasn't rendered since the last time it asked. (This is used to get data for newly inspected paths.) _Initial inspection..._ ``` front | | back | -- "inspect" (id:1, paths:[], force:true) ---------> | | <------------------------ "inspected" (full-data) -- | ``` _1 second passes with no updates..._ ``` | -- "inspect" (id:1, paths:[], force:false) --------> | | <------------------------ "inspected" (no-change) -- | ``` _User clicks to expand a path, aka hydrate..._ ``` | -- "inspect" (id:1, paths:['foo'], force:true) ----> | | <------------------------ "inspected" (full-data) -- | ``` _1 second passes during which there is an update..._ ``` | -- "inspect" (id:1, paths:['foo'], force:false) ---> | | <----------------- "inspectedElement" (full-data) -- | ``` ### Clear errors/warnings transition Previously this meant there would be a delay after clicking the "clear" button. The UX after this change is much improved. ### Hydrating paths transition I also added a transition to hydration (expanding "dehyrated" paths). ### Better error boundaries I also added a lower-level error boundary in case the new suspense operation ever failed. It provides a better "retry" mechanism (select a new element) so DevTools doesn't become entirely useful. Here I'm intentionally causing an error every time I select an element. ### Improved snapshot tests I also migrated several of the existing snapshot tests to use inline snapshots and added a new serializer for dehydrated props. Inline snapshots are easier to verify and maintain and the new serializer means dehydrated props will be formatted in a way that makes sense rather than being empty (in external snapshots) or super verbose (default inline snapshot format).

Brian Vaughn committed Jan 19, 2021 at 06:51 UTC af16f755dc7e0d6e2b4bf79b86c434f4ce0497fe
39 files changed +2054 -2072
package.json
+1 -1
@@ -107,7 +107,7 @@
107 "scripts": {
108 "build": "node ./scripts/rollup/build.js",
109 "build-combined": "node ./scripts/rollup/build-all-release-channels.js",
110 - "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh",
110 + "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build-combined react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh",
111 "build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
112 "build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
113 "linc": "node ./scripts/tasks/linc.js",
packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap deleted
-670
@@ -1,670 +0,0 @@
1 -// Jest Snapshot v1, https://goo.gl/fbAQLP
2 -
3 -exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
4 -{
5 - "id": 2,
6 - "owners": null,
7 - "context": null,
8 - "hooks": null,
9 - "props": {
10 - "set_of_sets": {
11 - "0": {},
12 - "1": {}
13 - }
14 - },
15 - "state": null
16 -}
17 -`;
18 -
19 -exports[`InspectedElementContext should dehydrate complex nested values when requested: 2: Inspect props.set_of_sets.0 1`] = `
20 -{
21 - "id": 2,
22 - "owners": null,
23 - "context": null,
24 - "hooks": null,
25 - "props": {
26 - "set_of_sets": {
27 - "0": {
28 - "0": 1,
29 - "1": 2,
30 - "2": 3
31 - },
32 - "1": {}
33 - }
34 - },
35 - "state": null
36 -}
37 -`;
38 -
39 -exports[`InspectedElementContext should display complex values of useDebugValue: DisplayedComplexValue 1`] = `
40 -{
41 - "id": 2,
42 - "owners": null,
43 - "context": null,
44 - "hooks": [
45 - {
46 - "id": null,
47 - "isStateEditable": false,
48 - "name": "DebuggableHook",
49 - "value": {
50 - "foo": 2
51 - },
52 - "subHooks": [
53 - {
54 - "id": 0,
55 - "isStateEditable": true,
56 - "name": "State",
57 - "value": 1,
58 - "subHooks": []
59 - }
60 - ]
61 - }
62 - ],
63 - "props": {},
64 - "state": null
65 -}
66 -`;
67 -
68 -exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
69 -{
70 - "id": 2,
71 - "owners": null,
72 - "context": null,
73 - "hooks": null,
74 - "props": {
75 - "nestedObject": {
76 - "a": {},
77 - "c": {}
78 - }
79 - },
80 - "state": null
81 -}
82 -`;
83 -
84 -exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 2: Inspect props.nestedObject.a 1`] = `
85 -{
86 - "id": 2,
87 - "owners": null,
88 - "context": null,
89 - "hooks": null,
90 - "props": {
91 - "nestedObject": {
92 - "a": {
93 - "value": 1,
94 - "b": {
95 - "value": 1
96 - }
97 - },
98 - "c": {}
99 - }
100 - },
101 - "state": null
102 -}
103 -`;
104 -
105 -exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 3: Inspect props.nestedObject.c 1`] = `
106 -{
107 - "id": 2,
108 - "owners": null,
109 - "context": null,
110 - "hooks": null,
111 - "props": {
112 - "nestedObject": {
113 - "a": {
114 - "value": 1,
115 - "b": {
116 - "value": 1
117 - }
118 - },
119 - "c": {
120 - "value": 1,
121 - "d": {
122 - "value": 1,
123 - "e": {}
124 - }
125 - }
126 - }
127 - },
128 - "state": null
129 -}
130 -`;
131 -
132 -exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 4: update inspected element 1`] = `
133 -{
134 - "id": 2,
135 - "owners": null,
136 - "context": null,
137 - "hooks": null,
138 - "props": {
139 - "nestedObject": {
140 - "a": {
141 - "value": 2,
142 - "b": {
143 - "value": 2
144 - }
145 - },
146 - "c": {
147 - "value": 2,
148 - "d": {
149 - "value": 2,
150 - "e": {}
151 - }
152 - }
153 - }
154 - },
155 - "state": null
156 -}
157 -`;
158 -
159 -exports[`InspectedElementContext should inspect hooks for components that only use context: 1: Inspected element 2 1`] = `
160 -{
161 - "id": 2,
162 - "owners": null,
163 - "context": null,
164 - "hooks": [
165 - {
166 - "id": null,
167 - "isStateEditable": false,
168 - "name": "Context",
169 - "value": true,
170 - "subHooks": []
171 - }
172 - ],
173 - "props": {
174 - "a": 1,
175 - "b": "abc"
176 - },
177 - "state": null
178 -}
179 -`;
180 -
181 -exports[`InspectedElementContext should inspect the currently selected element: 1: Inspected element 2 1`] = `
182 -{
183 - "id": 2,
184 - "owners": null,
185 - "context": null,
186 - "hooks": [
187 - {
188 - "id": 0,
189 - "isStateEditable": true,
190 - "name": "State",
191 - "value": 1,
192 - "subHooks": []
193 - }
194 - ],
195 - "props": {
196 - "a": 1,
197 - "b": "abc"
198 - },
199 - "state": null
200 -}
201 -`;
202 -
203 -exports[`InspectedElementContext should not consume iterables while inspecting: 1: Inspected element 2 1`] = `
204 -{
205 - "id": 2,
206 - "owners": null,
207 - "context": null,
208 - "hooks": null,
209 - "props": {
210 - "prop": {}
211 - },
212 - "state": null
213 -}
214 -`;
215 -
216 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
217 -{
218 - "id": 2,
219 - "owners": null,
220 - "context": null,
221 - "hooks": [
222 - {
223 - "id": 0,
224 - "isStateEditable": true,
225 - "name": "State",
226 - "value": {
227 - "foo": {}
228 - },
229 - "subHooks": []
230 - }
231 - ],
232 - "props": {
233 - "nestedObject": {
234 - "a": {}
235 - }
236 - },
237 - "state": null
238 -}
239 -`;
240 -
241 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
242 -{
243 - "id": 2,
244 - "owners": null,
245 - "context": null,
246 - "hooks": [
247 - {
248 - "id": 0,
249 - "isStateEditable": true,
250 - "name": "State",
251 - "value": {
252 - "foo": {}
253 - },
254 - "subHooks": []
255 - }
256 - ],
257 - "props": {
258 - "nestedObject": {
259 - "a": {
260 - "b": {
261 - "c": {}
262 - }
263 - }
264 - }
265 - },
266 - "state": null
267 -}
268 -`;
269 -
270 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
271 -{
272 - "id": 2,
273 - "owners": null,
274 - "context": null,
275 - "hooks": [
276 - {
277 - "id": 0,
278 - "isStateEditable": true,
279 - "name": "State",
280 - "value": {
281 - "foo": {}
282 - },
283 - "subHooks": []
284 - }
285 - ],
286 - "props": {
287 - "nestedObject": {
288 - "a": {
289 - "b": {
290 - "c": [
291 - {
292 - "d": {}
293 - }
294 - ]
295 - }
296 - }
297 - }
298 - },
299 - "state": null
300 -}
301 -`;
302 -
303 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
304 -{
305 - "id": 2,
306 - "owners": null,
307 - "context": null,
308 - "hooks": [
309 - {
310 - "id": 0,
311 - "isStateEditable": true,
312 - "name": "State",
313 - "value": {
314 - "foo": {}
315 - },
316 - "subHooks": []
317 - }
318 - ],
319 - "props": {
320 - "nestedObject": {
321 - "a": {
322 - "b": {
323 - "c": [
324 - {
325 - "d": {
326 - "e": {}
327 - }
328 - }
329 - ]
330 - }
331 - }
332 - }
333 - },
334 - "state": null
335 -}
336 -`;
337 -
338 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 5: Inspect hooks.0.value 1`] = `
339 -{
340 - "id": 2,
341 - "owners": null,
342 - "context": null,
343 - "hooks": [
344 - {
345 - "id": 0,
346 - "isStateEditable": true,
347 - "name": "State",
348 - "value": {
349 - "foo": {
350 - "bar": {}
351 - }
352 - },
353 - "subHooks": []
354 - }
355 - ],
356 - "props": {
357 - "nestedObject": {
358 - "a": {
359 - "b": {
360 - "c": [
361 - {
362 - "d": {
363 - "e": {}
364 - }
365 - }
366 - ]
367 - }
368 - }
369 - }
370 - },
371 - "state": null
372 -}
373 -`;
374 -
375 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 6: Inspect hooks.0.value.foo.bar 1`] = `
376 -{
377 - "id": 2,
378 - "owners": null,
379 - "context": null,
380 - "hooks": [
381 - {
382 - "id": 0,
383 - "isStateEditable": true,
384 - "name": "State",
385 - "value": {
386 - "foo": {
387 - "bar": {
388 - "baz": "hi"
389 - }
390 - }
391 - },
392 - "subHooks": []
393 - }
394 - ],
395 - "props": {
396 - "nestedObject": {
397 - "a": {
398 - "b": {
399 - "c": [
400 - {
401 - "d": {
402 - "e": {}
403 - }
404 - }
405 - ]
406 - }
407 - }
408 - }
409 - },
410 - "state": null
411 -}
412 -`;
413 -
414 -exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: initial render 1`] = `
415 -{
416 - "id": 3,
417 - "owners": null,
418 - "context": null,
419 - "hooks": [
420 - {
421 - "id": 0,
422 - "isStateEditable": true,
423 - "name": "State",
424 - "value": 0,
425 - "subHooks": []
426 - }
427 - ],
428 - "props": {
429 - "a": 1,
430 - "b": "abc"
431 - },
432 - "state": null
433 -}
434 -`;
435 -
436 -exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: updated state 1`] = `
437 -{
438 - "id": 3,
439 - "owners": null,
440 - "context": null,
441 - "hooks": [
442 - {
443 - "id": 0,
444 - "isStateEditable": true,
445 - "name": "State",
446 - "value": 0,
447 - "subHooks": []
448 - }
449 - ],
450 - "props": {
451 - "a": 2,
452 - "b": "def"
453 - },
454 - "state": null
455 -}
456 -`;
457 -
458 -exports[`InspectedElementContext should not tear if hydration is requested after an update: 1: Initially inspect element 1`] = `
459 -{
460 - "id": 2,
461 - "owners": null,
462 - "context": null,
463 - "hooks": null,
464 - "props": {
465 - "nestedObject": {
466 - "value": 1,
467 - "a": {}
468 - }
469 - },
470 - "state": null
471 -}
472 -`;
473 -
474 -exports[`InspectedElementContext should not tear if hydration is requested after an update: 2: Inspect props.nestedObject.a 1`] = `
475 -{
476 - "id": 2,
477 - "owners": null,
478 - "context": null,
479 - "hooks": null,
480 - "props": {
481 - "nestedObject": {
482 - "value": 2,
483 - "a": {
484 - "value": 2,
485 - "b": {
486 - "value": 2
487 - }
488 - }
489 - }
490 - },
491 - "state": null
492 -}
493 -`;
494 -
495 -exports[`InspectedElementContext should poll for updates for the currently selected element: 1: initial render 1`] = `
496 -{
497 - "id": 2,
498 - "owners": null,
499 - "context": null,
500 - "hooks": null,
501 - "props": {
502 - "a": 1,
503 - "b": "abc"
504 - },
505 - "state": null
506 -}
507 -`;
508 -
509 -exports[`InspectedElementContext should poll for updates for the currently selected element: 2: updated state 1`] = `
510 -{
511 - "id": 2,
512 - "owners": null,
513 - "context": null,
514 - "hooks": null,
515 - "props": {
516 - "a": 2,
517 - "b": "def"
518 - },
519 - "state": null
520 -}
521 -`;
522 -
523 -exports[`InspectedElementContext should support complex data types: 1: Inspected element 2 1`] = `
524 -{
525 - "id": 2,
526 - "owners": null,
527 - "context": null,
528 - "hooks": null,
529 - "props": {
530 - "anonymous_fn": {},
531 - "array_buffer": {},
532 - "array_of_arrays": [
533 - {}
534 - ],
535 - "big_int": {},
536 - "bound_fn": {},
537 - "data_view": {},
538 - "date": {},
539 - "fn": {},
540 - "html_element": {},
541 - "immutable": {
542 - "0": {},
543 - "1": {},
544 - "2": {}
545 - },
546 - "map": {
547 - "0": {},
548 - "1": {}
549 - },
550 - "map_of_maps": {
551 - "0": {},
552 - "1": {}
553 - },
554 - "object_of_objects": {
555 - "inner": {}
556 - },
557 - "object_with_symbol": {
558 - "Symbol(name)": "hello"
559 - },
560 - "proxy": {},
561 - "react_element": {},
562 - "regexp": {},
563 - "set": {
564 - "0": "abc",
565 - "1": 123
566 - },
567 - "set_of_sets": {
568 - "0": {},
569 - "1": {}
570 - },
571 - "symbol": {},
572 - "typed_array": {
573 - "0": 100,
574 - "1": -100,
575 - "2": 0
576 - }
577 - },
578 - "state": null
579 -}
580 -`;
581 -
582 -exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Inspected element 2 1`] = `
583 -{
584 - "id": 2,
585 - "owners": null,
586 - "context": null,
587 - "hooks": null,
588 - "props": {
589 - "data": {
590 - "_number": 42,
591 - "number": 42
592 - }
593 - },
594 - "state": null
595 -}
596 -`;
597 -
598 -exports[`InspectedElementContext should support objects with no prototype: 1: Inspected element 2 1`] = `
599 -{
600 - "id": 2,
601 - "owners": null,
602 - "context": null,
603 - "hooks": null,
604 - "props": {
605 - "object": {
606 - "string": "abc",
607 - "number": 123,
608 - "boolean": true
609 - }
610 - },
611 - "state": null
612 -}
613 -`;
614 -
615 -exports[`InspectedElementContext should support objects with overridden hasOwnProperty: 1: Inspected element 2 1`] = `
616 -{
617 - "id": 2,
618 - "owners": null,
619 - "context": null,
620 - "hooks": null,
621 - "props": {
622 - "object": {
623 - "name": "blah",
624 - "hasOwnProperty": true
625 - }
626 - },
627 - "state": null
628 -}
629 -`;
630 -
631 -exports[`InspectedElementContext should support objects with with inherited keys: 1: Inspected element 2 1`] = `
632 -{
633 - "id": 2,
634 - "owners": null,
635 - "context": null,
636 - "hooks": null,
637 - "props": {
638 - "object": {
639 - "123": 3,
640 - "enumerableString": 2,
641 - "Symbol(enumerableSymbol)": 3,
642 - "enumerableStringBase": 1,
643 - "Symbol(enumerableSymbolBase)": 1
644 - }
645 - },
646 - "state": null
647 -}
648 -`;
649 -
650 -exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
651 -{
652 - "id": 2,
653 - "owners": null,
654 - "context": null,
655 - "hooks": null,
656 - "props": {
657 - "boolean_false": false,
658 - "boolean_true": true,
659 - "infinity": null,
660 - "integer_zero": 0,
661 - "integer_one": 1,
662 - "float": 1.23,
663 - "string": "abc",
664 - "string_empty": "",
665 - "nan": null,
666 - "value_null": null
667 - },
668 - "state": null
669 -}
670 -`;
packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js new
+39
@@ -0,0 +1,39 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 +// test() is part of Jest's serializer API
11 +export function test(maybeDehydratedValue) {
12 + const {meta} = require('react-devtools-shared/src/hydration');
13 + return (
14 + maybeDehydratedValue !== null &&
15 + typeof maybeDehydratedValue === 'object' &&
16 + maybeDehydratedValue.hasOwnProperty(meta.inspectable) &&
17 + maybeDehydratedValue[meta.inspected] !== true
18 + );
19 +}
20 +
21 +// print() is part of Jest's serializer API
22 +export function print(dehydratedValue, serialize, indent) {
23 + const {meta} = require('react-devtools-shared/src/hydration');
24 + const indentation = Math.max(indent('.').indexOf('.') - 2, 0);
25 + const paddingLeft = ' '.repeat(indentation);
26 + return (
27 + 'Dehydrated {\n' +
28 + paddingLeft +
29 + ' "preview_short": ' +
30 + dehydratedValue[meta.preview_short] +
31 + ',\n' +
32 + paddingLeft +
33 + ' "preview_long": ' +
34 + dehydratedValue[meta.preview_long] +
35 + ',\n' +
36 + paddingLeft +
37 + '}'
38 + );
39 +}
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js renamed
+639 -281
@@ -8,16 +8,12 @@
8 */
9
10 import typeof ReactTestRenderer from 'react-test-renderer';
11 -import type {
12 - CopyInspectedElementPath,
13 - GetInspectedElementPath,
14 - StoreAsGlobal,
15 -} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
11 +import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';
12 +
13 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
14 import type Store from 'react-devtools-shared/src/devtools/store';
18 -import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';
15
20 -describe('InspectedElementContext', () => {
16 +describe('InspectedElement', () => {
17 let React;
18 let ReactDOM;
19 let PropTypes;
@@ -81,14 +77,26 @@ describe('InspectedElementContext', () => {
77 <TreeContextController
78 defaultSelectedElementID={defaultSelectedElementID}
79 defaultSelectedElementIndex={defaultSelectedElementIndex}>
84 - <InspectedElementContextController>
85 - {children}
86 - </InspectedElementContextController>
80 + <React.Suspense fallback="Loading...">
81 + <InspectedElementContextController>
82 + {children}
83 + </InspectedElementContextController>
84 + </React.Suspense>
85 </TreeContextController>
86 </StoreContext.Provider>
87 </BridgeContext.Provider>
88 );
89
90 + function useInspectedElement(id: number) {
91 + const {inspectedElement} = React.useContext(InspectedElementContext);
92 + return inspectedElement;
93 + }
94 +
95 + function useInspectElementPath(id: number) {
96 + const {inspectPaths} = React.useContext(InspectedElementContext);
97 + return inspectPaths;
98 + }
99 +
100 it('should inspect the currently selected element', async done => {
101 const Example = () => {
102 const [count] = React.useState(1);
@@ -105,9 +113,29 @@ describe('InspectedElementContext', () => {
113 let didFinish = false;
114
115 function Suspender({target}) {
108 - const {getInspectedElement} = React.useContext(InspectedElementContext);
109 - const inspectedElement = getInspectedElement(id);
110 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
116 + const inspectedElement = useInspectedElement(id);
117 + expect(inspectedElement).toMatchInlineSnapshot(`
118 + Object {
119 + "context": null,
120 + "events": undefined,
121 + "hooks": Array [
122 + Object {
123 + "id": 0,
124 + "isStateEditable": true,
125 + "name": "State",
126 + "subHooks": Array [],
127 + "value": 1,
128 + },
129 + ],
130 + "id": 2,
131 + "owners": null,
132 + "props": Object {
133 + "a": 1,
134 + "b": "abc",
135 + },
136 + "state": null,
137 + }
138 + `);
139 didFinish = true;
140 return null;
141 }
@@ -211,8 +239,7 @@ describe('InspectedElementContext', () => {
239 ];
240
241 function Suspender({target, shouldHaveLegacyContext}) {
214 - const {getInspectedElement} = React.useContext(InspectedElementContext);
215 - const inspectedElement = getInspectedElement(target);
242 + const inspectedElement = useInspectedElement(target);
243
244 expect(inspectedElement.context).not.toBe(null);
245 expect(inspectedElement.hasLegacyContext).toBe(shouldHaveLegacyContext);
@@ -257,8 +284,7 @@ describe('InspectedElementContext', () => {
284 let inspectedElement = null;
285
286 function Suspender({target}) {
260 - const {getInspectedElement} = React.useContext(InspectedElementContext);
261 - inspectedElement = getInspectedElement(id);
287 + inspectedElement = useInspectedElement(id);
288 return null;
289 }
290
@@ -273,14 +299,28 @@ describe('InspectedElementContext', () => {
299 </Contexts>,
300 );
301 }, false);
276 - expect(inspectedElement).toMatchSnapshot('1: initial render');
302 + expect(inspectedElement.props).toMatchInlineSnapshot(`
303 + Object {
304 + "a": 1,
305 + "b": "abc",
306 + }
307 + `);
308
309 await utils.actAsync(
310 () => ReactDOM.render(<Example a={2} b="def" />, container),
311 false,
312 );
313
283 - inspectedElement = null;
314 + // TODO (cache)
315 + // This test only passes if both the check-for-updates poll AND the test renderer.update() call are included below.
316 + // It seems like either one of the two should be sufficient but:
317 + // 1. Running only check-for-updates schedules a transition that React never renders.
318 + // 2. Running only renderer.update() loads stale data (first props)
319 +
320 + // Wait for our check-for-updates poll to get the new data.
321 + jest.runOnlyPendingTimers();
322 + await Promise.resolve();
323 +
324 await utils.actAsync(
325 () =>
326 renderer.update(
@@ -294,7 +334,12 @@ describe('InspectedElementContext', () => {
334 ),
335 false,
336 );
297 - expect(inspectedElement).toMatchSnapshot('2: updated state');
337 + expect(inspectedElement.props).toMatchInlineSnapshot(`
338 + Object {
339 + "a": 2,
340 + "b": "def",
341 + }
342 + `);
343
344 done();
345 });
@@ -305,6 +350,7 @@ describe('InspectedElementContext', () => {
350 const Wrapper = ({children}) => children;
351 const Target = React.memo(props => {
352 targetRenderCount++;
353 + // Even though his hook isn't referenced, it's used to observe backend rendering.
354 React.useState(0);
355 return null;
356 });
@@ -324,8 +370,7 @@ describe('InspectedElementContext', () => {
370 let inspectedElement = null;
371
372 function Suspender({target}) {
327 - const {getInspectedElement} = React.useContext(InspectedElementContext);
328 - inspectedElement = getInspectedElement(target);
373 + inspectedElement = useInspectedElement(target);
374 return null;
375 }
376
@@ -346,7 +391,12 @@ describe('InspectedElementContext', () => {
391 false,
392 );
393 expect(targetRenderCount).toBe(1);
349 - expect(inspectedElement).toMatchSnapshot('1: initial render');
394 + expect(inspectedElement.props).toMatchInlineSnapshot(`
395 + Object {
396 + "a": 1,
397 + "b": "abc",
398 + }
399 + `);
400
401 const initialInspectedElement = inspectedElement;
402
@@ -369,6 +419,7 @@ describe('InspectedElementContext', () => {
419 expect(inspectedElement).toEqual(initialInspectedElement);
420
421 targetRenderCount = 0;
422 + inspectedElement = null;
423
424 await utils.actAsync(
425 () =>
@@ -382,8 +433,26 @@ describe('InspectedElementContext', () => {
433 );
434
435 // Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
436 + await utils.actAsync(
437 + () =>
438 + renderer.update(
439 + <Contexts
440 + defaultSelectedElementID={id}
441 + defaultSelectedElementIndex={1}>
442 + <React.Suspense fallback={null}>
443 + <Suspender target={id} />
444 + </React.Suspense>
445 + </Contexts>,
446 + ),
447 + false,
448 + );
449 expect(targetRenderCount).toBe(2);
386 - expect(inspectedElement).toMatchSnapshot('2: updated state');
450 + expect(inspectedElement.props).toMatchInlineSnapshot(`
451 + Object {
452 + "a": 2,
453 + "b": "def",
454 + }
455 + `);
456
457 done();
458 });
@@ -426,8 +495,7 @@ describe('InspectedElementContext', () => {
495 let inspectedElement = null;
496
497 function Suspender({target}) {
429 - const {getInspectedElement} = React.useContext(InspectedElementContext);
430 - inspectedElement = getInspectedElement(target);
498 + inspectedElement = useInspectedElement(target);
499 return null;
500 }
501
@@ -482,8 +550,7 @@ describe('InspectedElementContext', () => {
550 let inspectedElement = null;
551
552 function Suspender({target}) {
485 - const {getInspectedElement} = React.useContext(InspectedElementContext);
486 - inspectedElement = getInspectedElement(id);
553 + inspectedElement = useInspectedElement(id);
554 return null;
555 }
556
@@ -501,9 +568,6 @@ describe('InspectedElementContext', () => {
568 false,
569 );
570
504 - expect(inspectedElement).not.toBeNull();
505 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
506 -
571 const {props} = (inspectedElement: any);
572 expect(props.boolean_false).toBe(false);
573 expect(props.boolean_true).toBe(true);
@@ -606,8 +670,7 @@ describe('InspectedElementContext', () => {
670 let inspectedElement = null;
671
672 function Suspender({target}) {
609 - const {getInspectedElement} = React.useContext(InspectedElementContext);
610 - inspectedElement = getInspectedElement(id);
673 + inspectedElement = useInspectedElement(id);
674 return null;
675 }
676
@@ -625,9 +688,6 @@ describe('InspectedElementContext', () => {
688 false,
689 );
690
628 - expect(inspectedElement).not.toBeNull();
629 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
630 -
691 const {
692 anonymous_fn,
693 array_buffer,
@@ -820,8 +880,7 @@ describe('InspectedElementContext', () => {
880 let inspectedElement = null;
881
882 function Suspender({target}) {
823 - const {getInspectedElement} = React.useContext(InspectedElementContext);
824 - inspectedElement = getInspectedElement(id);
883 + inspectedElement = useInspectedElement(id);
884 return null;
885 }
886
@@ -839,9 +898,6 @@ describe('InspectedElementContext', () => {
898 false,
899 );
900
842 - expect(inspectedElement).not.toBeNull();
843 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
844 -
901 const {prop} = (inspectedElement: any).props;
902 expect(prop[meta.inspectable]).toBe(false);
903 expect(prop[meta.name]).toBe('Generator');
@@ -870,8 +926,7 @@ describe('InspectedElementContext', () => {
926 let inspectedElement = null;
927
928 function Suspender({target}) {
873 - const {getInspectedElement} = React.useContext(InspectedElementContext);
874 - inspectedElement = getInspectedElement(id);
929 + inspectedElement = useInspectedElement(id);
930 return null;
931 }
932
@@ -889,13 +944,15 @@ describe('InspectedElementContext', () => {
944 false,
945 );
946
892 - expect(inspectedElement).not.toBeNull();
893 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
894 - expect(inspectedElement.props.object).toEqual({
895 - boolean: true,
896 - number: 123,
897 - string: 'abc',
898 - });
947 + expect(inspectedElement.props).toMatchInlineSnapshot(`
948 + Object {
949 + "object": Object {
950 + "boolean": true,
951 + "number": 123,
952 + "string": "abc",
953 + },
954 + }
955 + `);
956
957 done();
958 });
@@ -918,8 +975,7 @@ describe('InspectedElementContext', () => {
975 let inspectedElement = null;
976
977 function Suspender({target}) {
921 - const {getInspectedElement} = React.useContext(InspectedElementContext);
922 - inspectedElement = getInspectedElement(id);
978 + inspectedElement = useInspectedElement(id);
979 return null;
980 }
981
@@ -937,12 +993,10 @@ describe('InspectedElementContext', () => {
993 false,
994 );
995
940 - expect(inspectedElement).not.toBeNull();
941 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
942 - expect(inspectedElement.props.object).toEqual({
943 - name: 'blah',
944 - hasOwnProperty: true,
945 - });
996 + // TRICKY: Don't use toMatchInlineSnapshot() for this test!
997 + // Our snapshot serializer relies on hasOwnProperty() for feature detection.
998 + expect(inspectedElement.props.object.name).toBe('blah');
999 + expect(inspectedElement.props.object.hasOwnProperty).toBe(true);
1000
1001 done();
1002 });
@@ -977,9 +1031,15 @@ describe('InspectedElementContext', () => {
1031 let didFinish = false;
1032
1033 function Suspender({target}) {
980 - const {getInspectedElement} = React.useContext(InspectedElementContext);
981 - const inspectedElement = getInspectedElement(id);
982 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
1034 + const inspectedElement = useInspectedElement(id);
1035 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1036 + Object {
1037 + "data": Object {
1038 + "_number": 42,
1039 + "number": 42,
1040 + },
1041 + }
1042 + `);
1043 didFinish = true;
1044 return null;
1045 }
@@ -1075,8 +1135,7 @@ describe('InspectedElementContext', () => {
1135 let inspectedElement = null;
1136
1137 function Suspender({target}) {
1078 - const {getInspectedElement} = React.useContext(InspectedElementContext);
1079 - inspectedElement = getInspectedElement(id);
1138 + inspectedElement = useInspectedElement(id);
1139 return null;
1140 }
1141
@@ -1094,15 +1153,17 @@ describe('InspectedElementContext', () => {
1153 false,
1154 );
1155
1097 - expect(inspectedElement).not.toBeNull();
1098 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
1099 - expect(inspectedElement.props.object).toEqual({
1100 - 123: 3,
1101 - 'Symbol(enumerableSymbol)': 3,
1102 - 'Symbol(enumerableSymbolBase)': 1,
1103 - enumerableString: 2,
1104 - enumerableStringBase: 1,
1105 - });
1156 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1157 + Object {
1158 + "object": Object {
1159 + "123": 3,
1160 + "Symbol(enumerableSymbol)": 3,
1161 + "Symbol(enumerableSymbolBase)": 1,
1162 + "enumerableString": 2,
1163 + "enumerableStringBase": 1,
1164 + },
1165 + }
1166 + `);
1167
1168 done();
1169 });
@@ -1144,96 +1205,155 @@ describe('InspectedElementContext', () => {
1205
1206 const id = ((store.getElementIDAtIndex(0): any): number);
1207
1147 - let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
1208 let inspectedElement = null;
1209 + let inspectElementPath = null;
1210
1150 - function Suspender({target}) {
1151 - const context = React.useContext(InspectedElementContext);
1152 - getInspectedElementPath = context.getInspectedElementPath;
1153 - inspectedElement = context.getInspectedElement(target);
1211 + function Suspender({path, target}) {
1212 + inspectedElement = useInspectedElement(id);
1213 + inspectElementPath = useInspectElementPath(id);
1214 return null;
1215 }
1216
1157 - await utils.actAsync(
1158 - () =>
1159 - TestRenderer.create(
1160 - <Contexts
1161 - defaultSelectedElementID={id}
1162 - defaultSelectedElementIndex={0}>
1163 - <React.Suspense fallback={null}>
1164 - <Suspender target={id} />
1165 - </React.Suspense>
1166 - </Contexts>,
1167 - ),
1168 - false,
1169 - );
1170 - expect(getInspectedElementPath).not.toBeNull();
1171 - expect(inspectedElement).not.toBeNull();
1172 - expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1217 + const renderer = TestRenderer.create(null);
1218
1174 - inspectedElement = null;
1175 - TestUtilsAct(() => {
1176 - TestRendererAct(() => {
1177 - getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
1178 - jest.runOnlyPendingTimers();
1179 - });
1180 - });
1181 - expect(inspectedElement).not.toBeNull();
1182 - expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
1219 + async function getInspectedElement() {
1220 + await utils.actAsync(
1221 + () =>
1222 + renderer.update(
1223 + <Contexts
1224 + defaultSelectedElementID={id}
1225 + defaultSelectedElementIndex={0}>
1226 + <React.Suspense fallback={null}>
1227 + <Suspender target={id} />
1228 + </React.Suspense>
1229 + </Contexts>,
1230 + ),
1231 + false,
1232 + );
1233 + }
1234
1184 - inspectedElement = null;
1185 - TestUtilsAct(() => {
1186 - TestRendererAct(() => {
1187 - getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
1188 - jest.runOnlyPendingTimers();
1189 - });
1190 - });
1191 - expect(inspectedElement).not.toBeNull();
1192 - expect(inspectedElement).toMatchSnapshot(
1193 - '3: Inspect props.nestedObject.a.b.c',
1194 - );
1235 + // Render once to get a handle on inspectElementPath()
1236 + await getInspectedElement();
1237
1196 - inspectedElement = null;
1197 - TestUtilsAct(() => {
1198 - TestRendererAct(() => {
1199 - getInspectedElementPath(id, [
1200 - 'props',
1201 - 'nestedObject',
1202 - 'a',
1203 - 'b',
1204 - 'c',
1205 - 0,
1206 - 'd',
1207 - ]);
1208 - jest.runOnlyPendingTimers();
1238 + async function loadPath(path) {
1239 + TestUtilsAct(() => {
1240 + TestRendererAct(() => {
1241 + inspectElementPath(path);
1242 + jest.runOnlyPendingTimers();
1243 + });
1244 });
1210 - });
1211 - expect(inspectedElement).not.toBeNull();
1212 - expect(inspectedElement).toMatchSnapshot(
1213 - '4: Inspect props.nestedObject.a.b.c.0.d',
1214 - );
1245 + await getInspectedElement();
1246 + }
1247
1216 - inspectedElement = null;
1217 - TestUtilsAct(() => {
1218 - TestRendererAct(() => {
1219 - getInspectedElementPath(id, ['hooks', 0, 'value']);
1220 - jest.runOnlyPendingTimers();
1221 - });
1222 - });
1223 - expect(inspectedElement).not.toBeNull();
1224 - expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
1248 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1249 + Object {
1250 + "nestedObject": Object {
1251 + "a": Dehydrated {
1252 + "preview_short": {…},
1253 + "preview_long": {b: {…}},
1254 + },
1255 + },
1256 + }
1257 + `);
1258 +
1259 + await loadPath(['props', 'nestedObject', 'a']);
1260 +
1261 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1262 + Object {
1263 + "nestedObject": Object {
1264 + "a": Object {
1265 + "b": Object {
1266 + "c": Dehydrated {
1267 + "preview_short": Array(1),
1268 + "preview_long": [{…}],
1269 + },
1270 + },
1271 + },
1272 + },
1273 + }
1274 + `);
1275 +
1276 + await loadPath(['props', 'nestedObject', 'a', 'b', 'c']);
1277 +
1278 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1279 + Object {
1280 + "nestedObject": Object {
1281 + "a": Object {
1282 + "b": Object {
1283 + "c": Array [
1284 + Object {
1285 + "d": Dehydrated {
1286 + "preview_short": {…},
1287 + "preview_long": {e: {…}},
1288 + },
1289 + },
1290 + ],
1291 + },
1292 + },
1293 + },
1294 + }
1295 + `);
1296 +
1297 + await loadPath(['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);
1298 +
1299 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1300 + Object {
1301 + "nestedObject": Object {
1302 + "a": Object {
1303 + "b": Object {
1304 + "c": Array [
1305 + Object {
1306 + "d": Object {
1307 + "e": Object {},
1308 + },
1309 + },
1310 + ],
1311 + },
1312 + },
1313 + },
1314 + }
1315 + `);
1316
1226 - inspectedElement = null;
1227 - TestUtilsAct(() => {
1228 - TestRendererAct(() => {
1229 - getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
1230 - jest.runOnlyPendingTimers();
1231 - });
1232 - });
1233 - expect(inspectedElement).not.toBeNull();
1234 - expect(inspectedElement).toMatchSnapshot(
1235 - '6: Inspect hooks.0.value.foo.bar',
1236 - );
1317 + await loadPath(['hooks', 0, 'value']);
1318 +
1319 + expect(inspectedElement.hooks).toMatchInlineSnapshot(`
1320 + Array [
1321 + Object {
1322 + "id": 0,
1323 + "isStateEditable": true,
1324 + "name": "State",
1325 + "subHooks": Array [],
1326 + "value": Object {
1327 + "foo": Object {
1328 + "bar": Dehydrated {
1329 + "preview_short": {…},
1330 + "preview_long": {baz: "hi"},
1331 + },
1332 + },
1333 + },
1334 + },
1335 + ]
1336 + `);
1337 +
1338 + await loadPath(['hooks', 0, 'value', 'foo', 'bar']);
1339 +
1340 + expect(inspectedElement.hooks).toMatchInlineSnapshot(`
1341 + Array [
1342 + Object {
1343 + "id": 0,
1344 + "isStateEditable": true,
1345 + "name": "State",
1346 + "subHooks": Array [],
1347 + "value": Object {
1348 + "foo": Object {
1349 + "bar": Object {
1350 + "baz": "hi",
1351 + },
1352 + },
1353 + },
1354 + },
1355 + ]
1356 + `);
1357
1358 done();
1359 });
@@ -1253,42 +1373,79 @@ describe('InspectedElementContext', () => {
1373
1374 const id = ((store.getElementIDAtIndex(0): any): number);
1375
1256 - let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
1376 let inspectedElement = null;
1377 + let inspectElementPath = null;
1378
1259 - function Suspender({target}) {
1260 - const context = React.useContext(InspectedElementContext);
1261 - getInspectedElementPath = context.getInspectedElementPath;
1262 - inspectedElement = context.getInspectedElement(target);
1379 + function Suspender({path, target}) {
1380 + inspectedElement = useInspectedElement(id);
1381 + inspectElementPath = useInspectElementPath(id);
1382 return null;
1383 }
1384
1266 - await utils.actAsync(
1267 - () =>
1268 - TestRenderer.create(
1269 - <Contexts
1270 - defaultSelectedElementID={id}
1271 - defaultSelectedElementIndex={0}>
1272 - <React.Suspense fallback={null}>
1273 - <Suspender target={id} />
1274 - </React.Suspense>
1275 - </Contexts>,
1276 - ),
1277 - false,
1278 - );
1279 - expect(getInspectedElementPath).not.toBeNull();
1280 - expect(inspectedElement).not.toBeNull();
1281 - expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1385 + const renderer = TestRenderer.create(null);
1386
1283 - inspectedElement = null;
1284 - TestUtilsAct(() => {
1285 - TestRendererAct(() => {
1286 - getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
1287 - jest.runOnlyPendingTimers();
1387 + async function getInspectedElement() {
1388 + await utils.actAsync(
1389 + () =>
1390 + renderer.update(
1391 + <Contexts
1392 + defaultSelectedElementID={id}
1393 + defaultSelectedElementIndex={0}>
1394 + <React.Suspense fallback={null}>
1395 + <Suspender target={id} />
1396 + </React.Suspense>
1397 + </Contexts>,
1398 + ),
1399 + false,
1400 + );
1401 + }
1402 +
1403 + // Render once to get a handle on inspectElementPath()
1404 + await getInspectedElement();
1405 +
1406 + async function loadPath(path) {
1407 + TestUtilsAct(() => {
1408 + TestRendererAct(() => {
1409 + inspectElementPath(path);
1410 + jest.runOnlyPendingTimers();
1411 + });
1412 });
1289 - });
1290 - expect(inspectedElement).not.toBeNull();
1291 - expect(inspectedElement).toMatchSnapshot('2: Inspect props.set_of_sets.0');
1413 + await getInspectedElement();
1414 + }
1415 +
1416 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1417 + Object {
1418 + "set_of_sets": Object {
1419 + "0": Dehydrated {
1420 + "preview_short": Set(3),
1421 + "preview_long": Set(3) {1, 2, 3},
1422 + },
1423 + "1": Dehydrated {
1424 + "preview_short": Set(3),
1425 + "preview_long": Set(3) {"a", "b", "c"},
1426 + },
1427 + },
1428 + }
1429 + `);
1430 +
1431 + await loadPath(['props', 'set_of_sets', 0]);
1432 +
1433 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1434 + Object {
1435 + "set_of_sets": Object {
1436 + "0": Object {
1437 + "0": 1,
1438 + "1": 2,
1439 + "2": 3,
1440 + },
1441 + "1": Object {
1442 + "0": "a",
1443 + "1": "b",
1444 + "2": "c",
1445 + },
1446 + },
1447 + }
1448 + `);
1449
1450 done();
1451 });
@@ -1324,48 +1481,107 @@ describe('InspectedElementContext', () => {
1481
1482 const id = ((store.getElementIDAtIndex(0): any): number);
1483
1327 - let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
1484 let inspectedElement = null;
1485 + let inspectElementPath = null;
1486
1330 - function Suspender({target}) {
1331 - const context = React.useContext(InspectedElementContext);
1332 - getInspectedElementPath = context.getInspectedElementPath;
1333 - inspectedElement = context.getInspectedElement(id);
1487 + function Suspender({path, target}) {
1488 + inspectedElement = useInspectedElement(id);
1489 + inspectElementPath = useInspectElementPath(id);
1490 return null;
1491 }
1492
1337 - await utils.actAsync(
1338 - () =>
1339 - TestRenderer.create(
1340 - <Contexts
1341 - defaultSelectedElementID={id}
1342 - defaultSelectedElementIndex={0}>
1343 - <React.Suspense fallback={null}>
1344 - <Suspender target={id} />
1345 - </React.Suspense>
1346 - </Contexts>,
1347 - ),
1348 - false,
1349 - );
1350 - expect(getInspectedElementPath).not.toBeNull();
1351 - expect(inspectedElement).not.toBeNull();
1352 - expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1493 + const renderer = TestRenderer.create(null);
1494
1354 - inspectedElement = null;
1355 - TestRendererAct(() => {
1356 - getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
1357 - jest.runOnlyPendingTimers();
1358 - });
1359 - expect(inspectedElement).not.toBeNull();
1360 - expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
1495 + async function getInspectedElement() {
1496 + await utils.actAsync(
1497 + () =>
1498 + renderer.update(
1499 + <Contexts
1500 + defaultSelectedElementID={id}
1501 + defaultSelectedElementIndex={0}>
1502 + <React.Suspense fallback={null}>
1503 + <Suspender target={id} />
1504 + </React.Suspense>
1505 + </Contexts>,
1506 + ),
1507 + false,
1508 + );
1509 + }
1510
1362 - inspectedElement = null;
1363 - TestRendererAct(() => {
1364 - getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
1365 - jest.runOnlyPendingTimers();
1366 - });
1367 - expect(inspectedElement).not.toBeNull();
1368 - expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
1511 + // Render once to get a handle on inspectElementPath()
1512 + await getInspectedElement();
1513 +
1514 + async function loadPath(path) {
1515 + TestUtilsAct(() => {
1516 + TestRendererAct(() => {
1517 + inspectElementPath(path);
1518 + jest.runOnlyPendingTimers();
1519 + });
1520 + });
1521 + await getInspectedElement();
1522 + }
1523 +
1524 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1525 + Object {
1526 + "nestedObject": Object {
1527 + "a": Dehydrated {
1528 + "preview_short": {…},
1529 + "preview_long": {b: {…}, value: 1},
1530 + },
1531 + "c": Dehydrated {
1532 + "preview_short": {…},
1533 + "preview_long": {d: {…}, value: 1},
1534 + },
1535 + },
1536 + }
1537 + `);
1538 +
1539 + await loadPath(['props', 'nestedObject', 'a']);
1540 +
1541 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1542 + Object {
1543 + "nestedObject": Object {
1544 + "a": Object {
1545 + "b": Object {
1546 + "value": 1,
1547 + },
1548 + "value": 1,
1549 + },
1550 + "c": Object {
1551 + "d": Dehydrated {
1552 + "preview_short": {…},
1553 + "preview_long": {e: {…}, value: 1},
1554 + },
1555 + "value": 1,
1556 + },
1557 + },
1558 + }
1559 + `);
1560 +
1561 + await loadPath(['props', 'nestedObject', 'c']);
1562 +
1563 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1564 + Object {
1565 + "nestedObject": Object {
1566 + "a": Object {
1567 + "b": Object {
1568 + "value": 1,
1569 + },
1570 + "value": 1,
1571 + },
1572 + "c": Object {
1573 + "d": Object {
1574 + "e": Dehydrated {
1575 + "preview_short": {…},
1576 + "preview_long": {value: 1},
1577 + },
1578 + "value": 1,
1579 + },
1580 + "value": 1,
1581 + },
1582 + },
1583 + }
1584 + `);
1585
1586 TestRendererAct(() => {
1587 TestUtilsAct(() => {
@@ -1394,12 +1610,33 @@ describe('InspectedElementContext', () => {
1610 });
1611 });
1612
1397 - TestRendererAct(() => {
1398 - inspectedElement = null;
1399 - jest.advanceTimersByTime(1000);
1400 - });
1401 - expect(inspectedElement).not.toBeNull();
1402 - expect(inspectedElement).toMatchSnapshot('4: update inspected element');
1613 + // Wait for pending poll-for-update and then update inspected element data.
1614 + jest.runOnlyPendingTimers();
1615 + await Promise.resolve();
1616 + await getInspectedElement();
1617 +
1618 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1619 + Object {
1620 + "nestedObject": Object {
1621 + "a": Object {
1622 + "b": Object {
1623 + "value": 2,
1624 + },
1625 + "value": 2,
1626 + },
1627 + "c": Object {
1628 + "d": Object {
1629 + "e": Dehydrated {
1630 + "preview_short": {…},
1631 + "preview_long": {value: 2},
1632 + },
1633 + "value": 2,
1634 + },
1635 + "value": 2,
1636 + },
1637 + },
1638 + }
1639 + `);
1640
1641 done();
1642 });
@@ -1427,32 +1664,57 @@ describe('InspectedElementContext', () => {
1664
1665 const id = ((store.getElementIDAtIndex(0): any): number);
1666
1430 - let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
1667 let inspectedElement = null;
1668 + let inspectElementPath = null;
1669
1433 - function Suspender({target}) {
1434 - const context = React.useContext(InspectedElementContext);
1435 - getInspectedElementPath = context.getInspectedElementPath;
1436 - inspectedElement = context.getInspectedElement(id);
1670 + function Suspender({path, target}) {
1671 + inspectedElement = useInspectedElement(id);
1672 + inspectElementPath = useInspectElementPath(id);
1673 return null;
1674 }
1675
1440 - await utils.actAsync(
1441 - () =>
1442 - TestRenderer.create(
1443 - <Contexts
1444 - defaultSelectedElementID={id}
1445 - defaultSelectedElementIndex={0}>
1446 - <React.Suspense fallback={null}>
1447 - <Suspender target={id} />
1448 - </React.Suspense>
1449 - </Contexts>,
1450 - ),
1451 - false,
1452 - );
1453 - expect(getInspectedElementPath).not.toBeNull();
1454 - expect(inspectedElement).not.toBeNull();
1455 - expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1676 + const renderer = TestRenderer.create(null);
1677 +
1678 + async function getInspectedElement() {
1679 + await utils.actAsync(
1680 + () =>
1681 + renderer.update(
1682 + <Contexts
1683 + defaultSelectedElementID={id}
1684 + defaultSelectedElementIndex={0}>
1685 + <React.Suspense fallback={null}>
1686 + <Suspender target={id} />
1687 + </React.Suspense>
1688 + </Contexts>,
1689 + ),
1690 + false,
1691 + );
1692 + }
1693 +
1694 + // Render once to get a handle on inspectElementPath()
1695 + await getInspectedElement();
1696 +
1697 + async function loadPath(path) {
1698 + TestUtilsAct(() => {
1699 + TestRendererAct(() => {
1700 + inspectElementPath(path);
1701 + jest.runOnlyPendingTimers();
1702 + });
1703 + });
1704 + await getInspectedElement();
1705 + }
1706 +
1707 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1708 + Object {
1709 + "nestedObject": Object {
1710 + "a": Dehydrated {
1711 + "preview_short": {…},
1712 + "preview_long": {b: {…}, value: 1},
1713 + },
1714 + "value": 1,
1715 + },
1716 + }
1717 + `);
1718
1719 TestUtilsAct(() => {
1720 ReactDOM.render(
@@ -1471,16 +1733,21 @@ describe('InspectedElementContext', () => {
1733 );
1734 });
1735
1474 - inspectedElement = null;
1736 + await loadPath(['props', 'nestedObject', 'a']);
1737
1476 - TestRendererAct(() => {
1477 - TestUtilsAct(() => {
1478 - getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
1479 - jest.runOnlyPendingTimers();
1480 - });
1481 - });
1482 - expect(inspectedElement).not.toBeNull();
1483 - expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
1738 + expect(inspectedElement.props).toMatchInlineSnapshot(`
1739 + Object {
1740 + "nestedObject": Object {
1741 + "a": Object {
1742 + "b": Object {
1743 + "value": 2,
1744 + },
1745 + "value": 2,
1746 + },
1747 + "value": 2,
1748 + },
1749 + }
1750 + `);
1751
1752 done();
1753 });
@@ -1502,9 +1769,29 @@ describe('InspectedElementContext', () => {
1769 let didFinish = false;
1770
1771 function Suspender({target}) {
1505 - const {getInspectedElement} = React.useContext(InspectedElementContext);
1506 - const inspectedElement = getInspectedElement(id);
1507 - expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
1772 + const inspectedElement = useInspectedElement(id);
1773 + expect(inspectedElement).toMatchInlineSnapshot(`
1774 + Object {
1775 + "context": null,
1776 + "events": undefined,
1777 + "hooks": Array [
1778 + Object {
1779 + "id": null,
1780 + "isStateEditable": false,
1781 + "name": "Context",
1782 + "subHooks": Array [],
1783 + "value": true,
1784 + },
1785 + ],
1786 + "id": 2,
1787 + "owners": null,
1788 + "props": Object {
1789 + "a": 1,
1790 + "b": "abc",
1791 + },
1792 + "state": null,
1793 + }
1794 + `);
1795 didFinish = true;
1796 return null;
1797 }
@@ -1554,8 +1841,20 @@ describe('InspectedElementContext', () => {
1841 let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal);
1842
1843 function Suspender({target}) {
1557 - const context = React.useContext(InspectedElementContext);
1558 - storeAsGlobal = context.storeAsGlobal;
1844 + storeAsGlobal = (elementID: number, path: Array<string | number>) => {
1845 + const rendererID = store.getRendererIDForElement(elementID);
1846 + if (rendererID !== null) {
1847 + const {
1848 + storeAsGlobal: storeAsGlobalAPI,
1849 + } = require('react-devtools-shared/src/backendAPI');
1850 + storeAsGlobalAPI({
1851 + bridge,
1852 + id: elementID,
1853 + path,
1854 + rendererID,
1855 + });
1856 + }
1857 + };
1858 return null;
1859 }
1860
@@ -1572,23 +1871,22 @@ describe('InspectedElementContext', () => {
1871 ),
1872 false,
1873 );
1575 - expect(storeAsGlobal).not.toBeNull();
1874
1875 jest.spyOn(console, 'log').mockImplementation(() => {});
1876
1877 // Should store the whole value (not just the hydrated parts)
1878 storeAsGlobal(id, ['props', 'nestedObject']);
1879 jest.runOnlyPendingTimers();
1582 - expect(console.log).toHaveBeenCalledWith('$reactTemp1');
1583 - expect(global.$reactTemp1).toBe(nestedObject);
1880 + expect(console.log).toHaveBeenCalledWith('$reactTemp0');
1881 + expect(global.$reactTemp0).toBe(nestedObject);
1882
1883 console.log.mockReset();
1884
1885 // Should store the nested property specified (not just the outer value)
1886 storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
1887 jest.runOnlyPendingTimers();
1590 - expect(console.log).toHaveBeenCalledWith('$reactTemp2');
1591 - expect(global.$reactTemp2).toBe(nestedObject.a.b);
1888 + expect(console.log).toHaveBeenCalledWith('$reactTemp1');
1889 + expect(global.$reactTemp1).toBe(nestedObject.a.b);
1890
1891 done();
1892 });
@@ -1620,8 +1918,20 @@ describe('InspectedElementContext', () => {
1918 let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath);
1919
1920 function Suspender({target}) {
1623 - const context = React.useContext(InspectedElementContext);
1624 - copyPath = context.copyInspectedElementPath;
1921 + copyPath = (elementID: number, path: Array<string | number>) => {
1922 + const rendererID = store.getRendererIDForElement(elementID);
1923 + if (rendererID !== null) {
1924 + const {
1925 + copyInspectedElementPath,
1926 + } = require('react-devtools-shared/src/backendAPI');
1927 + copyInspectedElementPath({
1928 + bridge,
1929 + id: elementID,
1930 + path,
1931 + rendererID,
1932 + });
1933 + }
1934 + };
1935 return null;
1936 }
1937
@@ -1712,8 +2022,20 @@ describe('InspectedElementContext', () => {
2022 let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath);
2023
2024 function Suspender({target}) {
1715 - const context = React.useContext(InspectedElementContext);
1716 - copyPath = context.copyInspectedElementPath;
2025 + copyPath = (elementID: number, path: Array<string | number>) => {
2026 + const rendererID = store.getRendererIDForElement(elementID);
2027 + if (rendererID !== null) {
2028 + const {
2029 + copyInspectedElementPath,
2030 + } = require('react-devtools-shared/src/backendAPI');
2031 + copyInspectedElementPath({
2032 + bridge,
2033 + id: elementID,
2034 + path,
2035 + rendererID,
2036 + });
2037 + }
2038 + };
2039 return null;
2040 }
2041
@@ -1761,12 +2083,9 @@ describe('InspectedElementContext', () => {
2083 });
2084
2085 it('should display complex values of useDebugValue', async done => {
1764 - let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
2086 let inspectedElement = null;
2087 function Suspender({target}) {
1767 - const context = React.useContext(InspectedElementContext);
1768 - getInspectedElementPath = context.getInspectedElementPath;
1769 - inspectedElement = context.getInspectedElement(target);
2088 + inspectedElement = useInspectedElement(target);
2089 return null;
2090 }
2091
@@ -1803,9 +2122,27 @@ describe('InspectedElementContext', () => {
2122 ),
2123 false,
2124 );
1806 - expect(getInspectedElementPath).not.toBeNull();
1807 - expect(inspectedElement).not.toBeNull();
1808 - expect(inspectedElement).toMatchSnapshot('DisplayedComplexValue');
2125 + expect(inspectedElement.hooks).toMatchInlineSnapshot(`
2126 + Array [
2127 + Object {
2128 + "id": null,
2129 + "isStateEditable": false,
2130 + "name": "DebuggableHook",
2131 + "subHooks": Array [
2132 + Object {
2133 + "id": 0,
2134 + "isStateEditable": true,
2135 + "name": "State",
2136 + "subHooks": Array [],
2137 + "value": 1,
2138 + },
2139 + ],
2140 + "value": Object {
2141 + "foo": 2,
2142 + },
2143 + },
2144 + ]
2145 + `);
2146
2147 done();
2148 });
@@ -1825,8 +2162,7 @@ describe('InspectedElementContext', () => {
2162 let warnings = null;
2163
2164 function Suspender({target}) {
1828 - const {getInspectedElement} = React.useContext(InspectedElementContext);
1829 - const inspectedElement = getInspectedElement(id);
2165 + const inspectedElement = useInspectedElement(id);
2166 errors = inspectedElement.errors;
2167 warnings = inspectedElement.warnings;
2168 return null;
@@ -2038,7 +2374,11 @@ describe('InspectedElementContext', () => {
2374 );
2375 });
2376
2041 - store.clearErrorsAndWarnings();
2377 + const {
2378 + clearErrorsAndWarnings,
2379 + } = require('react-devtools-shared/src/backendAPI');
2380 + clearErrorsAndWarnings({bridge, store});
2381 +
2382 // Flush events to the renderer.
2383 jest.runOnlyPendingTimers();
2384
@@ -2051,7 +2391,7 @@ describe('InspectedElementContext', () => {
2391 `);
2392 });
2393
2054 - it('can be cleared for a particular Fiber (only errors)', async () => {
2394 + it('can be cleared for a particular Fiber (only warnings)', async () => {
2395 const Example = ({id}) => {
2396 console.error(`test-only: render error #${id}`);
2397 console.warn(`test-only: render warning #${id}`);
@@ -2071,7 +2411,14 @@ describe('InspectedElementContext', () => {
2411 );
2412 });
2413
2074 - store.clearWarningsForElement(2);
2414 + let id = ((store.getElementIDAtIndex(1): any): number);
2415 + const rendererID = store.getRendererIDForElement(id);
2416 +
2417 + const {
2418 + clearWarningsForElement,
2419 + } = require('react-devtools-shared/src/backendAPI');
2420 + clearWarningsForElement({bridge, id, rendererID});
2421 +
2422 // Flush events to the renderer.
2423 jest.runOnlyPendingTimers();
2424
@@ -2107,7 +2454,9 @@ describe('InspectedElementContext', () => {
2454 ]
2455 `);
2456
2110 - store.clearWarningsForElement(1);
2457 + id = ((store.getElementIDAtIndex(0): any): number);
2458 + clearWarningsForElement({bridge, id, rendererID});
2459 +
2460 // Flush events to the renderer.
2461 jest.runOnlyPendingTimers();
2462
@@ -2139,7 +2488,7 @@ describe('InspectedElementContext', () => {
2488 `);
2489 });
2490
2142 - it('can be cleared for a particular Fiber (only warnings)', async () => {
2491 + it('can be cleared for a particular Fiber (only errors)', async () => {
2492 const Example = ({id}) => {
2493 console.error(`test-only: render error #${id}`);
2494 console.warn(`test-only: render warning #${id}`);
@@ -2159,7 +2508,14 @@ describe('InspectedElementContext', () => {
2508 );
2509 });
2510
2162 - store.clearErrorsForElement(2);
2511 + let id = ((store.getElementIDAtIndex(1): any): number);
2512 + const rendererID = store.getRendererIDForElement(id);
2513 +
2514 + const {
2515 + clearErrorsForElement,
2516 + } = require('react-devtools-shared/src/backendAPI');
2517 + clearErrorsForElement({bridge, id, rendererID});
2518 +
2519 // Flush events to the renderer.
2520 jest.runOnlyPendingTimers();
2521
@@ -2195,7 +2551,9 @@ describe('InspectedElementContext', () => {
2551 ]
2552 `);
2553
2198 - store.clearErrorsForElement(1);
2554 + id = ((store.getElementIDAtIndex(0): any): number);
2555 + clearErrorsForElement({bridge, id, rendererID});
2556 +
2557 // Flush events to the renderer.
2558 jest.runOnlyPendingTimers();
2559
packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js
+10 -13
@@ -12,17 +12,14 @@ export function test(maybeInspectedElement) {
12
13 // print() is part of Jest's serializer API
14 export function print(inspectedElement, serialize, indent) {
15 - return JSON.stringify(
16 - {
17 - id: inspectedElement.id,
18 - owners: inspectedElement.owners,
19 - context: inspectedElement.context,
20 - events: inspectedElement.events,
21 - hooks: inspectedElement.hooks,
22 - props: inspectedElement.props,
23 - state: inspectedElement.state,
24 - },
25 - null,
26 - 2,
27 - );
15 + // Don't stringify this object; that would break nested serializers.
16 + return serialize({
17 + context: inspectedElement.context,
18 + events: inspectedElement.events,
19 + hooks: inspectedElement.hooks,
20 + id: inspectedElement.id,
21 + owners: inspectedElement.owners,
22 + props: inspectedElement.props,
23 + state: inspectedElement.state,
24 + });
25 }
packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap deleted
-303
@@ -1,303 +0,0 @@
1 -// Jest Snapshot v1, https://goo.gl/fbAQLP
2 -
3 -exports[`InspectedElementContext should inspect the currently selected element: 1: Initial inspection 1`] = `
4 -Object {
5 - "id": 2,
6 - "type": "full-data",
7 - "value": {
8 - "id": 2,
9 - "owners": null,
10 - "context": {},
11 - "hooks": null,
12 - "props": {
13 - "a": 1,
14 - "b": "abc"
15 - },
16 - "state": null
17 -},
18 -}
19 -`;
20 -
21 -exports[`InspectedElementContext should not consume iterables while inspecting: 1: Initial inspection 1`] = `
22 -Object {
23 - "id": 2,
24 - "type": "full-data",
25 - "value": {
26 - "id": 2,
27 - "owners": null,
28 - "context": {},
29 - "hooks": null,
30 - "props": {
31 - "iteratable": {}
32 - },
33 - "state": null
34 -},
35 -}
36 -`;
37 -
38 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
39 -Object {
40 - "id": 2,
41 - "type": "full-data",
42 - "value": {
43 - "id": 2,
44 - "owners": null,
45 - "context": {},
46 - "hooks": null,
47 - "props": {
48 - "nestedObject": {
49 - "a": {}
50 - }
51 - },
52 - "state": null
53 -},
54 -}
55 -`;
56 -
57 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
58 -Object {
59 - "id": 2,
60 - "type": "full-data",
61 - "value": {
62 - "id": 2,
63 - "owners": null,
64 - "context": {},
65 - "hooks": null,
66 - "props": {
67 - "nestedObject": {
68 - "a": {
69 - "b": {
70 - "c": {}
71 - }
72 - }
73 - }
74 - },
75 - "state": null
76 -},
77 -}
78 -`;
79 -
80 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
81 -Object {
82 - "id": 2,
83 - "type": "full-data",
84 - "value": {
85 - "id": 2,
86 - "owners": null,
87 - "context": {},
88 - "hooks": null,
89 - "props": {
90 - "nestedObject": {
91 - "a": {
92 - "b": {
93 - "c": [
94 - {
95 - "d": {}
96 - }
97 - ]
98 - }
99 - }
100 - }
101 - },
102 - "state": null
103 -},
104 -}
105 -`;
106 -
107 -exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
108 -Object {
109 - "id": 2,
110 - "type": "full-data",
111 - "value": {
112 - "id": 2,
113 - "owners": null,
114 - "context": {},
115 - "hooks": null,
116 - "props": {
117 - "nestedObject": {
118 - "a": {
119 - "b": {
120 - "c": [
121 - {
122 - "d": {
123 - "e": {}
124 - }
125 - }
126 - ]
127 - }
128 - }
129 - }
130 - },
131 - "state": null
132 -},
133 -}
134 -`;
135 -
136 -exports[`InspectedElementContext should support complex data types: 1: Initial inspection 1`] = `
137 -Object {
138 - "id": 2,
139 - "type": "full-data",
140 - "value": {
141 - "id": 2,
142 - "owners": null,
143 - "context": {},
144 - "hooks": null,
145 - "props": {
146 - "anonymous_fn": {},
147 - "array_buffer": {},
148 - "array_of_arrays": [
149 - {}
150 - ],
151 - "big_int": {},
152 - "bound_fn": {},
153 - "data_view": {},
154 - "date": {},
155 - "fn": {},
156 - "html_element": {},
157 - "immutable": {
158 - "0": {},
159 - "1": {},
160 - "2": {}
161 - },
162 - "map": {
163 - "0": {},
164 - "1": {}
165 - },
166 - "map_of_maps": {
167 - "0": {},
168 - "1": {}
169 - },
170 - "object_of_objects": {
171 - "inner": {}
172 - },
173 - "react_element": {},
174 - "regexp": {},
175 - "set": {
176 - "0": "abc",
177 - "1": 123
178 - },
179 - "set_of_sets": {
180 - "0": {},
181 - "1": {}
182 - },
183 - "symbol": {},
184 - "typed_array": {
185 - "0": 100,
186 - "1": -100,
187 - "2": 0
188 - }
189 - },
190 - "state": null
191 -},
192 -}
193 -`;
194 -
195 -exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Initial inspection 1`] = `
196 -Object {
197 - "id": 2,
198 - "type": "full-data",
199 - "value": {
200 - "id": 2,
201 - "owners": null,
202 - "context": {},
203 - "hooks": null,
204 - "props": {
205 - "data": {
206 - "_number": 42,
207 - "number": 42
208 - }
209 - },
210 - "state": null
211 -},
212 -}
213 -`;
214 -
215 -exports[`InspectedElementContext should support objects with no prototype: 1: Initial inspection 1`] = `
216 -Object {
217 - "id": 2,
218 - "type": "full-data",
219 - "value": {
220 - "id": 2,
221 - "owners": null,
222 - "context": {},
223 - "hooks": null,
224 - "props": {
225 - "object": {
226 - "string": "abc",
227 - "number": 123,
228 - "boolean": true
229 - }
230 - },
231 - "state": null
232 -},
233 -}
234 -`;
235 -
236 -exports[`InspectedElementContext should support objects with overridden hasOwnProperty: 1: Initial inspection 1`] = `
237 -Object {
238 - "id": 2,
239 - "type": "full-data",
240 - "value": {
241 - "id": 2,
242 - "owners": null,
243 - "context": {},
244 - "hooks": null,
245 - "props": {
246 - "object": {
247 - "name": "blah",
248 - "hasOwnProperty": true
249 - }
250 - },
251 - "state": null
252 -},
253 -}
254 -`;
255 -
256 -exports[`InspectedElementContext should support objects with with inherited keys: 1: Initial inspection 1`] = `
257 -Object {
258 - "id": 2,
259 - "type": "full-data",
260 - "value": {
261 - "id": 2,
262 - "owners": null,
263 - "context": {},
264 - "hooks": null,
265 - "props": {
266 - "data": {
267 - "123": 3,
268 - "enumerableString": 2,
269 - "Symbol(enumerableSymbol)": 3,
270 - "enumerableStringBase": 1,
271 - "Symbol(enumerableSymbolBase)": 1
272 - }
273 - },
274 - "state": null
275 -},
276 -}
277 -`;
278 -
279 -exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
280 -Object {
281 - "id": 2,
282 - "type": "full-data",
283 - "value": {
284 - "id": 2,
285 - "owners": null,
286 - "context": {},
287 - "hooks": null,
288 - "props": {
289 - "boolean_false": false,
290 - "boolean_true": true,
291 - "infinity": null,
292 - "integer_zero": 0,
293 - "integer_one": 1,
294 - "float": 1.23,
295 - "string": "abc",
296 - "string_empty": "",
297 - "nan": null,
298 - "value_null": null
299 - },
300 - "state": null
301 -},
302 -}
303 -`;
packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
+211 -104
@@ -7,71 +7,50 @@
7 * @flow
8 */
9
10 -import type {InspectedElementPayload} from 'react-devtools-shared/src/backend/types';
11 -import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
10 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
11 import type Store from 'react-devtools-shared/src/devtools/store';
12
13 describe('InspectedElementContext', () => {
14 let React;
15 let ReactDOM;
18 - let hydrate;
19 - let meta;
16 let bridge: FrontendBridge;
17 let store: Store;
18
19 + let backendAPI;
20 +
21 const act = (callback: Function) => {
22 callback();
23
24 jest.runAllTimers(); // Flush Bridge operations
25 };
26
29 - function dehydrateHelper(
30 - dehydratedData: DehydratedData | null,
31 - ): Object | null {
32 - if (dehydratedData !== null) {
33 - return hydrate(
34 - dehydratedData.data,
35 - dehydratedData.cleaned,
36 - dehydratedData.unserializable,
37 - );
38 - } else {
39 - return null;
40 - }
41 - }
42 -
27 async function read(
28 id: number,
45 - path?: Array<string | number>,
29 + inspectedPaths?: Object = {},
30 ): Promise<Object> {
47 - return new Promise((resolve, reject) => {
48 - const rendererID = ((store.getRendererIDForElement(id): any): number);
49 -
50 - const onInspectedElement = (payload: InspectedElementPayload) => {
51 - bridge.removeListener('inspectedElement', onInspectedElement);
52 -
53 - if (payload.type === 'full-data' && payload.value !== null) {
54 - payload.value.context = dehydrateHelper(payload.value.context);
55 - payload.value.props = dehydrateHelper(payload.value.props);
56 - payload.value.state = dehydrateHelper(payload.value.state);
57 - }
58 -
59 - resolve(payload);
60 - };
31 + const rendererID = ((store.getRendererIDForElement(id): any): number);
32 + const promise = backendAPI
33 + .inspectElement({
34 + bridge,
35 + forceUpdate: true,
36 + id,
37 + inspectedPaths,
38 + rendererID,
39 + })
40 + .then(data =>
41 + backendAPI.convertInspectedElementBackendToFrontend(data.value),
42 + );
43
62 - bridge.addListener('inspectedElement', onInspectedElement);
63 - bridge.send('inspectElement', {id, path, rendererID});
44 + jest.runOnlyPendingTimers();
45
65 - jest.runOnlyPendingTimers();
66 - });
46 + return promise;
47 }
48
49 beforeEach(() => {
50 bridge = global.bridge;
51 store = global.store;
52
73 - hydrate = require('react-devtools-shared/src/hydration').hydrate;
74 - meta = require('react-devtools-shared/src/hydration').meta;
53 + backendAPI = require('react-devtools-shared/src/backendAPI');
54
55 // Redirect all React/ReactDOM requires to the v15 UMD.
56 // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
@@ -94,7 +73,20 @@ describe('InspectedElementContext', () => {
73 const id = ((store.getElementIDAtIndex(0): any): number);
74 const inspectedElement = await read(id);
75
97 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
76 + expect(inspectedElement).toMatchInlineSnapshot(`
77 + Object {
78 + "context": Object {},
79 + "events": undefined,
80 + "hooks": null,
81 + "id": 2,
82 + "owners": null,
83 + "props": Object {
84 + "a": 1,
85 + "b": "abc",
86 + },
87 + "state": null,
88 + }
89 + `);
90
91 done();
92 });
@@ -124,20 +116,29 @@ describe('InspectedElementContext', () => {
116 const id = ((store.getElementIDAtIndex(0): any): number);
117 const inspectedElement = await read(id);
118
127 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
128 -
129 - const {props} = inspectedElement.value;
130 - expect(props.boolean_false).toBe(false);
131 - expect(props.boolean_true).toBe(true);
132 - expect(Number.isFinite(props.infinity)).toBe(false);
133 - expect(props.integer_zero).toEqual(0);
134 - expect(props.integer_one).toEqual(1);
135 - expect(props.float).toEqual(1.23);
136 - expect(props.string).toEqual('abc');
137 - expect(props.string_empty).toEqual('');
138 - expect(props.nan).toBeNaN();
139 - expect(props.value_null).toBeNull();
140 - expect(props.value_undefined).toBeUndefined();
119 + expect(inspectedElement).toMatchInlineSnapshot(`
120 + Object {
121 + "context": Object {},
122 + "events": undefined,
123 + "hooks": null,
124 + "id": 2,
125 + "owners": null,
126 + "props": Object {
127 + "boolean_false": false,
128 + "boolean_true": true,
129 + "float": 1.23,
130 + "infinity": Infinity,
131 + "integer_one": 1,
132 + "integer_zero": 0,
133 + "nan": NaN,
134 + "string": "abc",
135 + "string_empty": "",
136 + "value_null": null,
137 + "value_undefined": undefined,
138 + },
139 + "state": null,
140 + }
141 + `);
142
143 done();
144 });
@@ -211,8 +212,6 @@ describe('InspectedElementContext', () => {
212 const id = ((store.getElementIDAtIndex(0): any): number);
213 const inspectedElement = await read(id);
214
214 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
215 -
215 const {
216 anonymous_fn,
217 array_buffer,
@@ -233,7 +232,9 @@ describe('InspectedElementContext', () => {
232 set_of_sets,
233 symbol,
234 typed_array,
236 - } = inspectedElement.value.props;
235 + } = inspectedElement.props;
236 +
237 + const {meta} = require('react-devtools-shared/src/hydration');
238
239 expect(anonymous_fn[meta.inspectable]).toBe(false);
240 expect(anonymous_fn[meta.name]).toBe('function');
@@ -360,12 +361,15 @@ describe('InspectedElementContext', () => {
361 const id = ((store.getElementIDAtIndex(0): any): number);
362 const inspectedElement = await read(id);
363
363 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
364 - expect(inspectedElement.value.props.object).toEqual({
365 - boolean: true,
366 - number: 123,
367 - string: 'abc',
368 - });
364 + expect(inspectedElement.props).toMatchInlineSnapshot(`
365 + Object {
366 + "object": Object {
367 + "boolean": true,
368 + "number": 123,
369 + "string": "abc",
370 + },
371 + }
372 + `);
373
374 done();
375 });
@@ -388,11 +392,10 @@ describe('InspectedElementContext', () => {
392 const id = ((store.getElementIDAtIndex(0): any): number);
393 const inspectedElement = await read(id);
394
391 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
392 - expect(inspectedElement.value.props.object).toEqual({
393 - name: 'blah',
394 - hasOwnProperty: true,
395 - });
395 + // TRICKY: Don't use toMatchInlineSnapshot() for this test!
396 + // Our snapshot serializer relies on hasOwnProperty() for feature detection.
397 + expect(inspectedElement.props.object.name).toBe('blah');
398 + expect(inspectedElement.props.object.hasOwnProperty).toBe(true);
399
400 done();
401 });
@@ -417,7 +420,22 @@ describe('InspectedElementContext', () => {
420 const id = ((store.getElementIDAtIndex(0): any): number);
421 const inspectedElement = await read(id);
422
420 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
423 + expect(inspectedElement).toMatchInlineSnapshot(`
424 + Object {
425 + "context": Object {},
426 + "events": undefined,
427 + "hooks": null,
428 + "id": 2,
429 + "owners": null,
430 + "props": Object {
431 + "iteratable": Dehydrated {
432 + "preview_short": Generator,
433 + "preview_long": Generator,
434 + },
435 + },
436 + "state": null,
437 + }
438 + `);
439
440 // Inspecting should not consume the iterable.
441 expect(iteratable.next().value).toEqual(1);
@@ -457,7 +475,22 @@ describe('InspectedElementContext', () => {
475 const id = ((store.getElementIDAtIndex(0): any): number);
476 const inspectedElement = await read(id);
477
460 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
478 + expect(inspectedElement).toMatchInlineSnapshot(`
479 + Object {
480 + "context": Object {},
481 + "events": undefined,
482 + "hooks": null,
483 + "id": 2,
484 + "owners": null,
485 + "props": Object {
486 + "data": Object {
487 + "_number": 42,
488 + "number": 42,
489 + },
490 + },
491 + "state": null,
492 + }
493 + `);
494
495 done();
496 });
@@ -532,7 +565,25 @@ describe('InspectedElementContext', () => {
565 const id = ((store.getElementIDAtIndex(0): any): number);
566 const inspectedElement = await read(id);
567
535 - expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
568 + expect(inspectedElement).toMatchInlineSnapshot(`
569 + Object {
570 + "context": Object {},
571 + "events": undefined,
572 + "hooks": null,
573 + "id": 2,
574 + "owners": null,
575 + "props": Object {
576 + "data": Object {
577 + "123": 3,
578 + "Symbol(enumerableSymbol)": 3,
579 + "Symbol(enumerableSymbolBase)": 1,
580 + "enumerableString": 2,
581 + "enumerableStringBase": 1,
582 + },
583 + },
584 + "state": null,
585 + }
586 + `);
587
588 done();
589 });
@@ -564,28 +615,75 @@ describe('InspectedElementContext', () => {
615 const id = ((store.getElementIDAtIndex(0): any): number);
616
617 let inspectedElement = await read(id);
567 - expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
568 -
569 - inspectedElement = await read(id, ['props', 'nestedObject', 'a']);
570 - expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
618 + expect(inspectedElement.props).toMatchInlineSnapshot(`
619 + Object {
620 + "nestedObject": Object {
621 + "a": Dehydrated {
622 + "preview_short": {…},
623 + "preview_long": {b: {…}},
624 + },
625 + },
626 + }
627 + `);
628 +
629 + inspectedElement = await read(id, {props: {nestedObject: {a: {}}}});
630 + expect(inspectedElement.props).toMatchInlineSnapshot(`
631 + Object {
632 + "nestedObject": Object {
633 + "a": Object {
634 + "b": Object {
635 + "c": Dehydrated {
636 + "preview_short": Array(1),
637 + "preview_long": [{…}],
638 + },
639 + },
640 + },
641 + },
642 + }
643 + `);
644
572 - inspectedElement = await read(id, ['props', 'nestedObject', 'a', 'b', 'c']);
573 - expect(inspectedElement).toMatchSnapshot(
574 - '3: Inspect props.nestedObject.a.b.c',
575 - );
645 + inspectedElement = await read(id, {
646 + props: {nestedObject: {a: {b: {c: {}}}}},
647 + });
648 + expect(inspectedElement.props).toMatchInlineSnapshot(`
649 + Object {
650 + "nestedObject": Object {
651 + "a": Object {
652 + "b": Object {
653 + "c": Array [
654 + Object {
655 + "d": Dehydrated {
656 + "preview_short": {…},
657 + "preview_long": {e: {…}},
658 + },
659 + },
660 + ],
661 + },
662 + },
663 + },
664 + }
665 + `);
666
577 - inspectedElement = await read(id, [
578 - 'props',
579 - 'nestedObject',
580 - 'a',
581 - 'b',
582 - 'c',
583 - 0,
584 - 'd',
585 - ]);
586 - expect(inspectedElement).toMatchSnapshot(
587 - '4: Inspect props.nestedObject.a.b.c.0.d',
588 - );
667 + inspectedElement = await read(id, {
668 + props: {nestedObject: {a: {b: {c: {0: {d: {}}}}}}},
669 + });
670 + expect(inspectedElement.props).toMatchInlineSnapshot(`
671 + Object {
672 + "nestedObject": Object {
673 + "a": Object {
674 + "b": Object {
675 + "c": Array [
676 + Object {
677 + "d": Object {
678 + "e": Object {},
679 + },
680 + },
681 + ],
682 + },
683 + },
684 + },
685 + }
686 + `);
687
688 done();
689 });
@@ -619,28 +717,30 @@ describe('InspectedElementContext', () => {
717 spyOn(console, 'log').and.callFake(logSpy);
718
719 // Should store the whole value (not just the hydrated parts)
622 - bridge.send('storeAsGlobal', {
623 - count: 1,
720 + backendAPI.storeAsGlobal({
721 + bridge,
722 id,
723 path: ['props', 'nestedObject'],
724 rendererID,
725 });
726 +
727 jest.runOnlyPendingTimers();
629 - expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
630 - expect(global.$reactTemp1).toBe(nestedObject);
728 + expect(logSpy).toHaveBeenCalledWith('$reactTemp0');
729 + expect(global.$reactTemp0).toBe(nestedObject);
730
731 logSpy.mockReset();
732
733 // Should store the nested property specified (not just the outer value)
635 - bridge.send('storeAsGlobal', {
636 - count: 2,
734 + backendAPI.storeAsGlobal({
735 + bridge,
736 id,
737 path: ['props', 'nestedObject', 'a', 'b'],
738 rendererID,
739 });
740 +
741 jest.runOnlyPendingTimers();
642 - expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
643 - expect(global.$reactTemp2).toBe(nestedObject.a.b);
742 + expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
743 + expect(global.$reactTemp1).toBe(nestedObject.a.b);
744 });
745
746 it('should enable inspected values to be copied to the clipboard', () => {
@@ -669,11 +769,13 @@ describe('InspectedElementContext', () => {
769 const rendererID = ((store.getRendererIDForElement(id): any): number);
770
771 // Should copy the whole value (not just the hydrated parts)
672 - bridge.send('copyElementPath', {
772 + backendAPI.copyInspectedElementPath({
773 + bridge,
774 id,
775 path: ['props', 'nestedObject'],
776 rendererID,
777 });
778 +
779 jest.runOnlyPendingTimers();
780 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
781 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
@@ -683,11 +785,13 @@ describe('InspectedElementContext', () => {
785 global.mockClipboardCopy.mockReset();
786
787 // Should copy the nested property specified (not just the outer value)
686 - bridge.send('copyElementPath', {
788 + backendAPI.copyInspectedElementPath({
789 + bridge,
790 id,
791 path: ['props', 'nestedObject', 'a', 'b'],
792 rendererID,
793 });
794 +
795 jest.runOnlyPendingTimers();
796 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
797 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
@@ -745,7 +849,8 @@ describe('InspectedElementContext', () => {
849 const rendererID = ((store.getRendererIDForElement(id): any): number);
850
851 // Should copy the whole value (not just the hydrated parts)
748 - bridge.send('copyElementPath', {
852 + backendAPI.copyInspectedElementPath({
853 + bridge,
854 id,
855 path: ['props'],
856 rendererID,
@@ -756,7 +861,8 @@ describe('InspectedElementContext', () => {
861 global.mockClipboardCopy.mockReset();
862
863 // Should copy the nested property specified (not just the outer value)
759 - bridge.send('copyElementPath', {
864 + backendAPI.copyInspectedElementPath({
865 + bridge,
866 id,
867 path: ['props', 'bigInt'],
868 rendererID,
@@ -770,7 +876,8 @@ describe('InspectedElementContext', () => {
876 global.mockClipboardCopy.mockReset();
877
878 // Should copy the nested property specified (not just the outer value)
773 - bridge.send('copyElementPath', {
879 + backendAPI.copyInspectedElementPath({
880 + bridge,
881 id,
882 path: ['props', 'typedArray'],
883 rendererID,
packages/react-devtools-shared/src/__tests__/store-test.js
+23 -3
@@ -12,12 +12,14 @@ describe('Store', () => {
12 let ReactDOM;
13 let agent;
14 let act;
15 + let bridge;
16 let getRendererID;
17 let store;
18 let withErrorsOrWarningsIgnored;
19
20 beforeEach(() => {
21 agent = global.agent;
22 + bridge = global.bridge;
23 store = global.store;
24
25 React = require('react');
@@ -1159,7 +1161,11 @@ describe('Store', () => {
1161 <Example> ✕⚠
1162 `);
1163
1162 - store.clearErrorsAndWarnings();
1164 + const {
1165 + clearErrorsAndWarnings,
1166 + } = require('react-devtools-shared/src/backendAPI');
1167 + clearErrorsAndWarnings({bridge, store});
1168 +
1169 // flush events to the renderer
1170 jest.runAllTimers();
1171
@@ -1196,7 +1202,14 @@ describe('Store', () => {
1202 <Example> ✕⚠
1203 `);
1204
1199 - store.clearWarningsForElement(2);
1205 + const id = ((store.getElementIDAtIndex(1): any): number);
1206 + const rendererID = store.getRendererIDForElement(id);
1207 +
1208 + const {
1209 + clearWarningsForElement,
1210 + } = require('react-devtools-shared/src/backendAPI');
1211 + clearWarningsForElement({bridge, id, rendererID});
1212 +
1213 // Flush events to the renderer.
1214 jest.runAllTimers();
1215
@@ -1234,7 +1247,14 @@ describe('Store', () => {
1247 <Example> ✕⚠
1248 `);
1249
1237 - store.clearErrorsForElement(2);
1250 + const id = ((store.getElementIDAtIndex(1): any): number);
1251 + const rendererID = store.getRendererIDForElement(id);
1252 +
1253 + const {
1254 + clearErrorsForElement,
1255 + } = require('react-devtools-shared/src/backendAPI');
1256 + clearErrorsForElement({bridge, id, rendererID});
1257 +
1258 // Flush events to the renderer.
1259 jest.runAllTimers();
1260
packages/react-devtools-shared/src/__tests__/treeContext-test.js
+11 -3
@@ -1441,20 +1441,28 @@ describe('TreeListContext', () => {
1441 });
1442
1443 describe('inline errors/warnings state', () => {
1444 + const {
1445 + clearErrorsAndWarnings: clearErrorsAndWarningsAPI,
1446 + clearErrorsForElement: clearErrorsForElementAPI,
1447 + clearWarningsForElement: clearWarningsForElementAPI,
1448 + } = require('react-devtools-shared/src/backendAPI');
1449 +
1450 function clearAllErrors() {
1445 - utils.act(() => store.clearErrorsAndWarnings());
1451 + utils.act(() => clearErrorsAndWarningsAPI({bridge, store}));
1452 // flush events to the renderer
1453 jest.runAllTimers();
1454 }
1455
1456 function clearErrorsForElement(id) {
1451 - utils.act(() => store.clearErrorsForElement(id));
1457 + const rendererID = store.getRendererIDForElement(id);
1458 + utils.act(() => clearErrorsForElementAPI({bridge, id, rendererID}));
1459 // flush events to the renderer
1460 jest.runAllTimers();
1461 }
1462
1463 function clearWarningsForElement(id) {
1457 - utils.act(() => store.clearWarningsForElement(id));
1464 + const rendererID = store.getRendererIDForElement(id);
1465 + utils.act(() => clearWarningsForElementAPI({bridge, id, rendererID}));
1466 // flush events to the renderer
1467 jest.runAllTimers();
1468 }
packages/react-devtools-shared/src/backend/agent.js
+14 -3
@@ -70,8 +70,10 @@ type CopyElementParams = {|
70
71 type InspectElementParams = {|
72 id: number,
73 - path?: Array<string | number>,
73 + inspectedPaths: Object,
74 + forceUpdate: boolean,
75 rendererID: number,
76 + requestID: number,
77 |};
78
79 type OverrideHookParams = {|
@@ -328,12 +330,21 @@ export default class Agent extends EventEmitter<{|
330 }
331 };
332
331 - inspectElement = ({id, path, rendererID}: InspectElementParams) => {
333 + inspectElement = ({
334 + id,
335 + inspectedPaths,
336 + forceUpdate,
337 + rendererID,
338 + requestID,
339 + }: InspectElementParams) => {
340 const renderer = this._rendererInterfaces[rendererID];
341 if (renderer == null) {
342 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
343 } else {
336 - this._bridge.send('inspectedElement', renderer.inspectElement(id, path));
344 + this._bridge.send(
345 + 'inspectedElement',
346 + renderer.inspectElement(requestID, id, inspectedPaths, forceUpdate),
347 + );
348
349 // When user selects an element, stop trying to restore the selection,
350 // and instead remember the current selection for the next reload.
packages/react-devtools-shared/src/backend/legacy/renderer.js
+9 -24
@@ -584,25 +584,12 @@ export function attach(
584 }
585
586 let currentlyInspectedElementID: number | null = null;
587 - let currentlyInspectedPaths: Object = {};
588 -
589 - // Track the intersection of currently inspected paths,
590 - // so that we can send their data along if the element is re-rendered.
591 - function mergeInspectedPaths(path: Array<string | number>) {
592 - let current = currentlyInspectedPaths;
593 - path.forEach(key => {
594 - if (!current[key]) {
595 - current[key] = {};
596 - }
597 - current = current[key];
598 - });
599 - }
587
601 - function createIsPathAllowed(key: string) {
588 + function createIsPathAllowed(key: string, inspectedPaths: Object) {
589 // This function helps prevent previously-inspected paths from being dehydrated in updates.
590 // This is important to avoid a bad user experience where expanded toggles collapse on update.
591 return function isPathAllowed(path: Array<string | number>): boolean {
605 - let current = currentlyInspectedPaths[key];
592 + let current = inspectedPaths[key];
593 if (!current) {
594 return false;
595 }
@@ -691,26 +678,23 @@ export function attach(
678 }
679
680 function inspectElement(
681 + requestID: number,
682 id: number,
695 - path?: Array<string | number>,
683 + inspectedPaths: Object,
684 ): InspectedElementPayload {
685 if (currentlyInspectedElementID !== id) {
686 currentlyInspectedElementID = id;
699 - currentlyInspectedPaths = {};
687 }
688
689 const inspectedElement = inspectElementRaw(id);
690 if (inspectedElement === null) {
691 return {
692 id,
693 + responseID: requestID,
694 type: 'not-found',
695 };
696 }
697
710 - if (path != null) {
711 - mergeInspectedPaths(path);
712 - }
713 -
698 // Any time an inspected element has an update,
699 // we should update the selected $r value as wel.
700 // Do this before dehyration (cleanForBridge).
@@ -718,19 +702,20 @@ export function attach(
702
703 inspectedElement.context = cleanForBridge(
704 inspectedElement.context,
721 - createIsPathAllowed('context'),
705 + createIsPathAllowed('context', inspectedPaths),
706 );
707 inspectedElement.props = cleanForBridge(
708 inspectedElement.props,
725 - createIsPathAllowed('props'),
709 + createIsPathAllowed('props', inspectedPaths),
710 );
711 inspectedElement.state = cleanForBridge(
712 inspectedElement.state,
729 - createIsPathAllowed('state'),
713 + createIsPathAllowed('state', inspectedPaths),
714 );
715
716 return {
717 id,
718 + responseID: requestID,
719 type: 'full-data',
720 value: inspectedElement,
721 };
packages/react-devtools-shared/src/backend/renderer.js
+52 -96
@@ -2718,7 +2718,6 @@ export function attach(
2718
2719 let mostRecentlyInspectedElement: InspectedElement | null = null;
2720 let hasElementUpdatedSinceLastInspected: boolean = false;
2721 - let currentlyInspectedPaths: Object = {};
2721
2722 function isMostRecentlyInspectedElementCurrent(id: number): boolean {
2723 return (
@@ -2728,21 +2727,10 @@ export function attach(
2727 );
2728 }
2729
2731 - // Track the intersection of currently inspected paths,
2732 - // so that we can send their data along if the element is re-rendered.
2733 - function mergeInspectedPaths(path: Array<string | number>) {
2734 - let current = currentlyInspectedPaths;
2735 - path.forEach(key => {
2736 - if (!current[key]) {
2737 - current[key] = {};
2738 - }
2739 - current = current[key];
2740 - });
2741 - }
2742 -
2730 function createIsPathAllowed(
2731 key: string | null,
2732 secondaryCategory: 'hooks' | null,
2733 + inspectedPaths: Object,
2734 ) {
2735 // This function helps prevent previously-inspected paths from being dehydrated in updates.
2736 // This is important to avoid a bad user experience where expanded toggles collapse on update.
@@ -2767,8 +2755,7 @@ export function attach(
2755 break;
2756 }
2757
2770 - let current =
2771 - key === null ? currentlyInspectedPaths : currentlyInspectedPaths[key];
2758 + let current = key === null ? inspectedPaths : inspectedPaths[key];
2759 if (!current) {
2760 return false;
2761 }
@@ -2863,97 +2850,66 @@ export function attach(
2850 }
2851
2852 function inspectElement(
2853 + requestID: number,
2854 id: number,
2867 - path?: Array<string | number>,
2855 + inspectedPaths: Object,
2856 + forceUpdate: boolean,
2857 ): InspectedElementPayload {
2869 - const isCurrent = isMostRecentlyInspectedElementCurrent(id);
2858 + const isCurrent = !forceUpdate && isMostRecentlyInspectedElementCurrent(id);
2859
2860 if (isCurrent) {
2872 - if (path != null) {
2873 - mergeInspectedPaths(path);
2874 -
2875 - let secondaryCategory = null;
2876 - if (path[0] === 'hooks') {
2877 - secondaryCategory = 'hooks';
2878 - }
2879 -
2880 - // If this element has not been updated since it was last inspected,
2881 - // we can just return the subset of data in the newly-inspected path.
2882 - return {
2883 - id,
2884 - type: 'hydrated-path',
2885 - path,
2886 - value: cleanForBridge(
2887 - getInObject(
2888 - ((mostRecentlyInspectedElement: any): InspectedElement),
2889 - path,
2890 - ),
2891 - createIsPathAllowed(null, secondaryCategory),
2892 - path,
2893 - ),
2894 - };
2895 - } else {
2896 - // If this element has not been updated since it was last inspected, we don't need to re-run it.
2897 - // Instead we can just return the ID to indicate that it has not changed.
2898 - return {
2899 - id,
2900 - type: 'no-change',
2901 - };
2902 - }
2903 - } else {
2904 - hasElementUpdatedSinceLastInspected = false;
2905 -
2906 - if (
2907 - mostRecentlyInspectedElement === null ||
2908 - mostRecentlyInspectedElement.id !== id
2909 - ) {
2910 - currentlyInspectedPaths = {};
2911 - }
2912 -
2913 - mostRecentlyInspectedElement = inspectElementRaw(id);
2914 - if (mostRecentlyInspectedElement === null) {
2915 - return {
2916 - id,
2917 - type: 'not-found',
2918 - };
2919 - }
2920 -
2921 - if (path != null) {
2922 - mergeInspectedPaths(path);
2923 - }
2924 -
2925 - // Any time an inspected element has an update,
2926 - // we should update the selected $r value as wel.
2927 - // Do this before dehydration (cleanForBridge).
2928 - updateSelectedElement(mostRecentlyInspectedElement);
2861 + // If this element has not been updated since it was last inspected, we don't need to return it.
2862 + // Instead we can just return the ID to indicate that it has not changed.
2863 + return {
2864 + id,
2865 + responseID: requestID,
2866 + type: 'no-change',
2867 + };
2868 + }
2869
2930 - // Clone before cleaning so that we preserve the full data.
2931 - // This will enable us to send patches without re-inspecting if hydrated paths are requested.
2932 - // (Reducing how often we shallow-render is a better DX for function components that use hooks.)
2933 - const cleanedInspectedElement = {...mostRecentlyInspectedElement};
2934 - cleanedInspectedElement.context = cleanForBridge(
2935 - cleanedInspectedElement.context,
2936 - createIsPathAllowed('context', null),
2937 - );
2938 - cleanedInspectedElement.hooks = cleanForBridge(
2939 - cleanedInspectedElement.hooks,
2940 - createIsPathAllowed('hooks', 'hooks'),
2941 - );
2942 - cleanedInspectedElement.props = cleanForBridge(
2943 - cleanedInspectedElement.props,
2944 - createIsPathAllowed('props', null),
2945 - );
2946 - cleanedInspectedElement.state = cleanForBridge(
2947 - cleanedInspectedElement.state,
2948 - createIsPathAllowed('state', null),
2949 - );
2870 + hasElementUpdatedSinceLastInspected = false;
2871
2872 + mostRecentlyInspectedElement = inspectElementRaw(id);
2873 + if (mostRecentlyInspectedElement === null) {
2874 return {
2875 id,
2953 - type: 'full-data',
2954 - value: cleanedInspectedElement,
2876 + responseID: requestID,
2877 + type: 'not-found',
2878 };
2879 }
2880 +
2881 + // Any time an inspected element has an update,
2882 + // we should update the selected $r value as wel.
2883 + // Do this before dehydration (cleanForBridge).
2884 + updateSelectedElement(mostRecentlyInspectedElement);
2885 +
2886 + // Clone before cleaning so that we preserve the full data.
2887 + // This will enable us to send patches without re-inspecting if hydrated paths are requested.
2888 + // (Reducing how often we shallow-render is a better DX for function components that use hooks.)
2889 + const cleanedInspectedElement = {...mostRecentlyInspectedElement};
2890 + cleanedInspectedElement.context = cleanForBridge(
2891 + cleanedInspectedElement.context,
2892 + createIsPathAllowed('context', null, inspectedPaths),
2893 + );
2894 + cleanedInspectedElement.hooks = cleanForBridge(
2895 + cleanedInspectedElement.hooks,
2896 + createIsPathAllowed('hooks', 'hooks', inspectedPaths),
2897 + );
2898 + cleanedInspectedElement.props = cleanForBridge(
2899 + cleanedInspectedElement.props,
2900 + createIsPathAllowed('props', null, inspectedPaths),
2901 + );
2902 + cleanedInspectedElement.state = cleanForBridge(
2903 + cleanedInspectedElement.state,
2904 + createIsPathAllowed('state', null, inspectedPaths),
2905 + );
2906 +
2907 + return {
2908 + id,
2909 + responseID: requestID,
2910 + type: 'full-data',
2911 + value: cleanedInspectedElement,
2912 + };
2913 }
2914
2915 function logElementToConsole(id) {
packages/react-devtools-shared/src/backend/types.js
+6 -10
@@ -257,34 +257,28 @@ export type InspectedElement = {|
257 export const InspectElementFullDataType = 'full-data';
258 export const InspectElementNoChangeType = 'no-change';
259 export const InspectElementNotFoundType = 'not-found';
260 -export const InspectElementHydratedPathType = 'hydrated-path';
260
261 type InspectElementFullData = {|
262 id: number,
263 + responseID: number,
264 type: 'full-data',
265 value: InspectedElement,
266 |};
267
268 -type InspectElementHydratedPath = {|
269 - id: number,
270 - type: 'hydrated-path',
271 - path: Array<string | number>,
272 - value: any,
273 -|};
274 -
268 type InspectElementNoChange = {|
269 id: number,
270 + responseID: number,
271 type: 'no-change',
272 |};
273
274 type InspectElementNotFound = {|
275 id: number,
276 + responseID: number,
277 type: 'not-found',
278 |};
279
280 export type InspectedElementPayload =
281 | InspectElementFullData
287 - | InspectElementHydratedPath
282 | InspectElementNoChange
283 | InspectElementNotFound;
284
@@ -319,8 +313,10 @@ export type RendererInterface = {
313 handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
314 handleCommitFiberUnmount: (fiber: Object) => void,
315 inspectElement: (
316 + requestID: number,
317 id: number,
323 - path?: Array<string | number>,
318 + inspectedPaths: Object,
319 + forceUpdate: boolean,
320 ) => InspectedElementPayload,
321 logElementToConsole: (id: number) => void,
322 overrideSuspense: (id: number, forceFallback: boolean) => void,
packages/react-devtools-shared/src/backendAPI.js new
+283
@@ -0,0 +1,283 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
11 +import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
12 +import Store from 'react-devtools-shared/src/devtools/store';
13 +
14 +import type {
15 + InspectedElement as InspectedElementBackend,
16 + InspectedElementPayload,
17 +} from 'react-devtools-shared/src/backend/types';
18 +import type {
19 + BackendEvents,
20 + FrontendBridge,
21 +} from 'react-devtools-shared/src/bridge';
22 +import type {
23 + DehydratedData,
24 + InspectedElement as InspectedElementFrontend,
25 +} from 'react-devtools-shared/src/devtools/views/Components/types';
26 +
27 +export function clearErrorsAndWarnings({
28 + bridge,
29 + store,
30 +}: {|
31 + bridge: FrontendBridge,
32 + store: Store,
33 +|}): void {
34 + store.rootIDToRendererID.forEach(rendererID => {
35 + bridge.send('clearErrorsAndWarnings', {rendererID});
36 + });
37 +}
38 +
39 +export function clearErrorsForElement({
40 + bridge,
41 + id,
42 + rendererID,
43 +}: {|
44 + bridge: FrontendBridge,
45 + id: number,
46 + rendererID: number,
47 +|}): void {
48 + bridge.send('clearErrorsForFiberID', {
49 + rendererID,
50 + id,
51 + });
52 +}
53 +
54 +export function clearWarningsForElement({
55 + bridge,
56 + id,
57 + rendererID,
58 +}: {|
59 + bridge: FrontendBridge,
60 + id: number,
61 + rendererID: number,
62 +|}): void {
63 + bridge.send('clearWarningsForFiberID', {
64 + rendererID,
65 + id,
66 + });
67 +}
68 +
69 +export function copyInspectedElementPath({
70 + bridge,
71 + id,
72 + path,
73 + rendererID,
74 +}: {|
75 + bridge: FrontendBridge,
76 + id: number,
77 + path: Array<string | number>,
78 + rendererID: number,
79 +|}): void {
80 + bridge.send('copyElementPath', {
81 + id,
82 + path,
83 + rendererID,
84 + });
85 +}
86 +
87 +export function inspectElement({
88 + bridge,
89 + forceUpdate,
90 + id,
91 + inspectedPaths,
92 + rendererID,
93 +}: {|
94 + bridge: FrontendBridge,
95 + forceUpdate: boolean,
96 + id: number,
97 + inspectedPaths: Object,
98 + rendererID: number,
99 +|}): Promise<InspectedElementPayload> {
100 + const requestID = requestCounter++;
101 + const promise = getPromiseForRequestID<InspectedElementPayload>(
102 + requestID,
103 + 'inspectedElement',
104 + bridge,
105 + );
106 +
107 + bridge.send('inspectElement', {
108 + forceUpdate,
109 + id,
110 + inspectedPaths,
111 + rendererID,
112 + requestID,
113 + });
114 +
115 + return promise;
116 +}
117 +
118 +let storeAsGlobalCount = 0;
119 +
120 +export function storeAsGlobal({
121 + bridge,
122 + id,
123 + path,
124 + rendererID,
125 +}: {|
126 + bridge: FrontendBridge,
127 + id: number,
128 + path: Array<string | number>,
129 + rendererID: number,
130 +|}): void {
131 + bridge.send('storeAsGlobal', {
132 + count: storeAsGlobalCount++,
133 + id,
134 + path,
135 + rendererID,
136 + });
137 +}
138 +
139 +const TIMEOUT_DELAY = 5000;
140 +
141 +let requestCounter = 0;
142 +
143 +function getPromiseForRequestID<T>(
144 + requestID: number,
145 + eventType: $Keys<BackendEvents>,
146 + bridge: FrontendBridge,
147 +): Promise<T> {
148 + return new Promise((resolve, reject) => {
149 + const cleanup = () => {
150 + bridge.removeListener(eventType, onInspectedElement);
151 +
152 + clearTimeout(timeoutID);
153 + };
154 +
155 + const onInspectedElement = (data: any) => {
156 + if (data.responseID === requestID) {
157 + cleanup();
158 + resolve((data: T));
159 + }
160 + };
161 +
162 + const onTimeout = () => {
163 + cleanup();
164 + reject();
165 + };
166 +
167 + bridge.addListener(eventType, onInspectedElement);
168 +
169 + const timeoutID = setTimeout(onTimeout, TIMEOUT_DELAY);
170 + });
171 +}
172 +
173 +export function cloneInspectedElementWithPath(
174 + inspectedElement: InspectedElementFrontend,
175 + path: Array<string | number>,
176 + value: Object,
177 +): InspectedElementFrontend {
178 + const hydratedValue = hydrateHelper(value, path);
179 + const clonedInspectedElement = {...inspectedElement};
180 +
181 + fillInPath(clonedInspectedElement, value, path, hydratedValue);
182 +
183 + return clonedInspectedElement;
184 +}
185 +
186 +export function convertInspectedElementBackendToFrontend(
187 + inspectedElementBackend: InspectedElementBackend,
188 +): InspectedElementFrontend {
189 + const {
190 + canEditFunctionProps,
191 + canEditFunctionPropsDeletePaths,
192 + canEditFunctionPropsRenamePaths,
193 + canEditHooks,
194 + canEditHooksAndDeletePaths,
195 + canEditHooksAndRenamePaths,
196 + canToggleSuspense,
197 + canViewSource,
198 + hasLegacyContext,
199 + id,
200 + source,
201 + type,
202 + owners,
203 + context,
204 + hooks,
205 + props,
206 + rendererPackageName,
207 + rendererVersion,
208 + rootType,
209 + state,
210 + key,
211 + errors,
212 + warnings,
213 + } = inspectedElementBackend;
214 +
215 + const inspectedElement: InspectedElementFrontend = {
216 + canEditFunctionProps,
217 + canEditFunctionPropsDeletePaths,
218 + canEditFunctionPropsRenamePaths,
219 + canEditHooks,
220 + canEditHooksAndDeletePaths,
221 + canEditHooksAndRenamePaths,
222 + canToggleSuspense,
223 + canViewSource,
224 + hasLegacyContext,
225 + id,
226 + key,
227 + rendererPackageName,
228 + rendererVersion,
229 + rootType,
230 + source,
231 + type,
232 + owners:
233 + owners === null
234 + ? null
235 + : owners.map(owner => {
236 + const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs(
237 + owner.displayName,
238 + owner.type,
239 + );
240 + return {
241 + ...owner,
242 + displayName,
243 + hocDisplayNames,
244 + };
245 + }),
246 + context: hydrateHelper(context),
247 + hooks: hydrateHelper(hooks),
248 + props: hydrateHelper(props),
249 + state: hydrateHelper(state),
250 + errors,
251 + warnings,
252 + };
253 +
254 + return inspectedElement;
255 +}
256 +
257 +function hydrateHelper(
258 + dehydratedData: DehydratedData | null,
259 + path?: Array<string | number>,
260 +): Object | null {
261 + if (dehydratedData !== null) {
262 + const {cleaned, data, unserializable} = dehydratedData;
263 +
264 + if (path) {
265 + const {length} = path;
266 + if (length > 0) {
267 + // Hydration helper requires full paths, but inspection dehydrates with relative paths.
268 + // In that event it's important that we adjust the "cleaned" paths to match.
269 + return hydrate(
270 + data,
271 + cleaned.map(cleanedPath => cleanedPath.slice(length)),
272 + unserializable.map(unserializablePath =>
273 + unserializablePath.slice(length),
274 + ),
275 + );
276 + }
277 + }
278 +
279 + return hydrate(data, cleaned, unserializable);
280 + } else {
281 + return null;
282 + }
283 +}
packages/react-devtools-shared/src/bridge.js
+4 -2
@@ -89,7 +89,9 @@ type ViewAttributeSourceParams = {|
89
90 type InspectElementParams = {|
91 ...ElementAndRendererID,
92 - path?: Array<string | number>,
92 + forceUpdate: boolean,
93 + inspectedPaths: Object,
94 + requestID: number,
95 |};
96
97 type StoreAsGlobalParams = {|
@@ -117,7 +119,7 @@ type UpdateConsolePatchSettingsParams = {|
119 showInlineWarningsAndErrors: boolean,
120 |};
121
120 -type BackendEvents = {|
122 +export type BackendEvents = {|
123 extensionBackendInitialized: [],
124 inspectedElement: [InspectedElementPayload],
125 isBackendStorageAPISupported: [boolean],
packages/react-devtools-shared/src/devtools/cache.js
+2
@@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes';
12 import * as React from 'react';
13 import {createContext} from 'react';
14
15 +// TODO (cache) Remove this cache; it is outdated and will not work with newer APIs like startTransition.
16 +
17 // Cache implementation was forked from the React repo:
18 // https://github.com/facebook/react/blob/master/packages/react-cache/src/ReactCache.js
19 //
packages/react-devtools-shared/src/devtools/store.js
+1 -37
@@ -101,7 +101,7 @@ export default class Store extends EventEmitter<{|
101
102 // Map of ID to (mutable) Element.
103 // Elements are mutated to avoid excessive cloning during tree updates.
104 - // The InspectedElementContext also relies on this mutability for its WeakMap usage.
104 + // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
105 _idToElement: Map<number, Element> = new Map();
106
107 // Should the React Native style editor panel be shown?
@@ -378,42 +378,6 @@ export default class Store extends EventEmitter<{|
378 return this._cachedWarningCount;
379 }
380
381 - clearErrorsAndWarnings(): void {
382 - this._rootIDToRendererID.forEach(rendererID => {
383 - this._bridge.send('clearErrorsAndWarnings', {
384 - rendererID,
385 - });
386 - });
387 - }
388 -
389 - clearErrorsForElement(id: number): void {
390 - const rendererID = this.getRendererIDForElement(id);
391 - if (rendererID === null) {
392 - console.warn(
393 - `Unable to find rendererID for element ${id} when clearing errors.`,
394 - );
395 - } else {
396 - this._bridge.send('clearErrorsForFiberID', {
397 - rendererID,
398 - id,
399 - });
400 - }
401 - }
402 -
403 - clearWarningsForElement(id: number): void {
404 - const rendererID = this.getRendererIDForElement(id);
405 - if (rendererID === null) {
406 - console.warn(
407 - `Unable to find rendererID for element ${id} when clearing warnings.`,
408 - );
409 - } else {
410 - this._bridge.send('clearWarningsForFiberID', {
411 - rendererID,
412 - id,
413 - });
414 - }
415 - }
416 -
381 containsElement(id: number): boolean {
382 return this._idToElement.get(id) != null;
383 }
packages/react-devtools-shared/src/devtools/views/Components/Components.js
+28 -25
@@ -17,7 +17,6 @@ import {
17 useRef,
18 } from 'react';
19 import Tree from './Tree';
20 -import {InspectedElementContextController} from './InspectedElementContext';
20 import {OwnersListContextController} from './OwnersListContext';
21 import portaledContent from '../portaledContent';
22 import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
@@ -25,7 +24,9 @@ import {
24 localStorageGetItem,
25 localStorageSetItem,
26 } from 'react-devtools-shared/src/storage';
27 +import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
28 import InspectedElement from './InspectedElement';
29 +import {InspectedElementContextController} from './InspectedElementContext';
30 import {ModalDialog} from '../ModalDialog';
31 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
32 import {NativeStyleContextController} from './NativeStyleEditor/context';
@@ -151,32 +152,34 @@ function Components(_: {||}) {
152 return (
153 <SettingsModalContextController>
154 <OwnersListContextController>
154 - <InspectedElementContextController>
155 - <div
156 - ref={wrapperElementRef}
157 - className={styles.Components}
158 - onMouseMove={onResize}
159 - onMouseLeave={onResizeEnd}
160 - onMouseUp={onResizeEnd}>
161 - <Fragment>
162 - <div ref={resizeElementRef} className={styles.TreeWrapper}>
163 - <Tree />
164 - </div>
165 - <div className={styles.ResizeBarWrapper}>
166 - <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
167 - </div>
168 - <div className={styles.InspectedElementWrapper}>
169 - <NativeStyleContextController>
155 + <div
156 + ref={wrapperElementRef}
157 + className={styles.Components}
158 + onMouseMove={onResize}
159 + onMouseLeave={onResizeEnd}
160 + onMouseUp={onResizeEnd}>
161 + <Fragment>
162 + <div ref={resizeElementRef} className={styles.TreeWrapper}>
163 + <Tree />
164 + </div>
165 + <div className={styles.ResizeBarWrapper}>
166 + <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
167 + </div>
168 + <div className={styles.InspectedElementWrapper}>
169 + <NativeStyleContextController>
170 + <InspectedElementErrorBoundary>
171 <Suspense fallback={<Loading />}>
171 - <InspectedElement />
172 + <InspectedElementContextController>
173 + <InspectedElement />
174 + </InspectedElementContextController>
175 </Suspense>
173 - </NativeStyleContextController>
174 - </div>
175 - <ModalDialog />
176 - <SettingsModal />
177 - </Fragment>
178 - </div>
179 - </InspectedElementContextController>
176 + </InspectedElementErrorBoundary>
177 + </NativeStyleContextController>
178 + </div>
179 + <ModalDialog />
180 + <SettingsModal />
181 + </Fragment>
182 + </div>
183 </OwnersListContextController>
184 </SettingsModalContextController>
185 );
packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js
+3
@@ -14,17 +14,20 @@ import ButtonIcon from '../ButtonIcon';
14 import styles from './ExpandCollapseToggle.css';
15
16 type ExpandCollapseToggleProps = {|
17 + disabled: boolean,
18 isOpen: boolean,
19 setIsOpen: Function,
20 |};
21
22 export default function ExpandCollapseToggle({
23 + disabled,
24 isOpen,
25 setIsOpen,
26 }: ExpandCollapseToggleProps) {
27 return (
28 <Button
29 className={styles.ExpandCollapseToggle}
30 + disabled={disabled}
31 onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
32 title={`${isOpen ? 'Collapse' : 'Expand'} prop value`}>
33 <ButtonIcon type={isOpen ? 'expanded' : 'collapsed'} />
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+2 -14
@@ -14,16 +14,15 @@ import {BridgeContext, StoreContext} from '../context';
14 import Button from '../Button';
15 import ButtonIcon from '../ButtonIcon';
16 import {ModalDialogContext} from '../ModalDialog';
17 -import {InspectedElementContext} from './InspectedElementContext';
17 import ViewElementSourceContext from './ViewElementSourceContext';
18 import Toggle from '../Toggle';
19 import {ElementTypeSuspense} from 'react-devtools-shared/src/types';
20 import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
21 import InspectedElementView from './InspectedElementView';
22 +import {InspectedElementContext} from './InspectedElementContext';
23
24 import styles from './InspectedElement.css';
25
26 -import type {InspectedElementContextType} from './InspectedElementContext';
26 import type {InspectedElement} from './types';
27
28 export type Props = {||};
@@ -38,21 +37,13 @@ export default function InspectedElementWrapper(_: Props) {
37 const store = useContext(StoreContext);
38 const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
39
41 - const {
42 - copyInspectedElementPath,
43 - getInspectedElementPath,
44 - getInspectedElement,
45 - storeAsGlobal,
46 - } = useContext<InspectedElementContextType>(InspectedElementContext);
40 + const {inspectedElement} = useContext(InspectedElementContext);
41
42 const element =
43 inspectedElementID !== null
44 ? store.getElementByID(inspectedElementID)
45 : null;
46
53 - const inspectedElement =
54 - inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
55 -
47 const highlightElement = useCallback(() => {
48 if (element !== null && inspectedElementID !== null) {
49 const rendererID = store.getRendererIDForElement(inspectedElementID);
@@ -228,11 +219,8 @@ export default function InspectedElementWrapper(_: Props) {
219 key={
220 inspectedElementID /* Force reset when selected Element changes */
221 }
231 - copyInspectedElementPath={copyInspectedElementPath}
222 element={element}
233 - getInspectedElementPath={getInspectedElementPath}
223 inspectedElement={inspectedElement}
235 - storeAsGlobal={storeAsGlobal}
224 />
225 )}
226 </div>
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
+100 -354
@@ -10,6 +10,8 @@
10 import * as React from 'react';
11 import {
12 createContext,
13 + unstable_startTransition as startTransition,
14 + unstable_useCacheRefresh as useCacheRefresh,
15 useCallback,
16 useContext,
17 useEffect,
@@ -17,360 +19,134 @@ import {
19 useRef,
20 useState,
21 } from 'react';
20 -import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
21 -import {createResource} from '../../cache';
22 -import {BridgeContext, StoreContext} from '../context';
23 -import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
22 import {TreeStateContext} from './TreeContext';
25 -import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
23 +import {BridgeContext, StoreContext} from '../context';
24 +import {
25 + checkForUpdate,
26 + inspectElement,
27 +} from 'react-devtools-shared/src/inspectedElementCache';
28
29 +import type {ReactNodeList} from 'shared/ReactTypes';
30 import type {
28 - InspectedElement as InspectedElementBackend,
29 - InspectedElementPayload,
30 -} from 'react-devtools-shared/src/backend/types';
31 -import type {
32 - DehydratedData,
31 Element,
34 - InspectedElement as InspectedElementFrontend,
32 + InspectedElement,
33 } from 'react-devtools-shared/src/devtools/views/Components/types';
36 -import type {Resource, Thenable} from '../../cache';
37 -
38 -export type StoreAsGlobal = (id: number, path: Array<string | number>) => void;
34
40 -export type CopyInspectedElementPath = (
41 - id: number,
42 - path: Array<string | number>,
43 -) => void;
35 +type Path = Array<string | number>;
36 +type InspectPathFunction = (path: Path) => void;
37
45 -export type GetInspectedElementPath = (
46 - id: number,
47 - path: Array<string | number>,
48 -) => void;
49 -
50 -export type GetInspectedElement = (
51 - id: number,
52 -) => InspectedElementFrontend | null;
53 -
54 -type RefreshInspectedElement = () => void;
55 -
56 -export type InspectedElementContextType = {|
57 - copyInspectedElementPath: CopyInspectedElementPath,
58 - getInspectedElementPath: GetInspectedElementPath,
59 - getInspectedElement: GetInspectedElement,
60 - refreshInspectedElement: RefreshInspectedElement,
61 - storeAsGlobal: StoreAsGlobal,
38 +type Context = {|
39 + inspectedElement: InspectedElement | null,
40 + inspectPaths: InspectPathFunction,
41 |};
42
64 -const InspectedElementContext = createContext<InspectedElementContextType>(
65 - ((null: any): InspectedElementContextType),
43 +export const InspectedElementContext = createContext<Context>(
44 + ((null: any): Context),
45 );
67 -InspectedElementContext.displayName = 'InspectedElementContext';
68 -
69 -type ResolveFn = (inspectedElement: InspectedElementFrontend) => void;
70 -type InProgressRequest = {|
71 - promise: Thenable<InspectedElementFrontend>,
72 - resolveFn: ResolveFn,
73 -|};
74 -
75 -const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
76 -const resource: Resource<
77 - Element,
78 - Element,
79 - InspectedElementFrontend,
80 -> = createResource(
81 - (element: Element) => {
82 - const request = inProgressRequests.get(element);
83 - if (request != null) {
84 - return request.promise;
85 - }
86 -
87 - let resolveFn = ((null: any): ResolveFn);
88 - const promise = new Promise(resolve => {
89 - resolveFn = resolve;
90 - });
91 -
92 - inProgressRequests.set(element, {promise, resolveFn});
46
94 - return promise;
95 - },
96 - (element: Element) => element,
97 - {useWeakMap: true},
98 -);
47 +const POLL_INTERVAL = 1000;
48
100 -type Props = {|
101 - children: React$Node,
49 +export type Props = {|
50 + children: ReactNodeList,
51 |};
52
104 -function InspectedElementContextController({children}: Props) {
53 +export function InspectedElementContextController({children}: Props) {
54 + const {selectedElementID} = useContext(TreeStateContext);
55 const bridge = useContext(BridgeContext);
56 const store = useContext(StoreContext);
57
108 - const storeAsGlobalCount = useRef(1);
109 -
110 - // Ask the backend to store the value at the specified path as a global variable.
111 - const storeAsGlobal = useCallback<GetInspectedElementPath>(
112 - (id: number, path: Array<string | number>) => {
113 - const rendererID = store.getRendererIDForElement(id);
114 - if (rendererID !== null) {
115 - bridge.send('storeAsGlobal', {
116 - count: storeAsGlobalCount.current++,
117 - id,
118 - path,
119 - rendererID,
120 - });
121 - }
122 - },
123 - [bridge, store],
124 - );
125 -
126 - // Ask the backend to copy the specified path to the clipboard.
127 - const copyInspectedElementPath = useCallback<GetInspectedElementPath>(
128 - (id: number, path: Array<string | number>) => {
129 - const rendererID = store.getRendererIDForElement(id);
130 - if (rendererID !== null) {
131 - bridge.send('copyElementPath', {id, path, rendererID});
132 - }
133 - },
134 - [bridge, store],
135 - );
136 -
137 - // Ask the backend to fill in a "dehydrated" path; this will result in a "inspectedElement".
138 - const getInspectedElementPath = useCallback<GetInspectedElementPath>(
139 - (id: number, path: Array<string | number>) => {
140 - const rendererID = store.getRendererIDForElement(id);
141 - if (rendererID !== null) {
142 - bridge.send('inspectElement', {id, path, rendererID});
143 - }
144 - },
145 - [bridge, store],
146 - );
147 -
148 - const getInspectedElement = useCallback<GetInspectedElement>(
149 - (id: number) => {
150 - const element = store.getElementByID(id);
151 - if (element !== null) {
152 - return resource.read(element);
153 - } else {
154 - return null;
155 - }
156 - },
157 - [store],
158 - );
159 -
160 - // It's very important that this context consumes selectedElementID and not inspectedElementID.
161 - // Otherwise the effect that sends the "inspect" message across the bridge-
162 - // would itself be blocked by the same render that suspends (waiting for the data).
163 - const {selectedElementID} = useContext(TreeStateContext);
164 -
165 - const refreshInspectedElement = useCallback<RefreshInspectedElement>(() => {
166 - if (selectedElementID !== null) {
167 - const rendererID = store.getRendererIDForElement(selectedElementID);
168 - if (rendererID !== null) {
169 - bridge.send('inspectElement', {id: selectedElementID, rendererID});
170 - }
171 - }
172 - }, [bridge, selectedElementID]);
173 -
174 - const [
175 - currentlyInspectedElement,
176 - setCurrentlyInspectedElement,
177 - ] = useState<InspectedElementFrontend | null>(null);
178 -
179 - // This effect handler invalidates the suspense cache and schedules rendering updates with React.
180 - useEffect(() => {
181 - const onInspectedElement = (data: InspectedElementPayload) => {
182 - const {id} = data;
58 + const refresh = useCacheRefresh();
59
184 - let element;
60 + // Track when insepected paths have changed; we need to force the backend to send an udpate then.
61 + const forceUpdateRef = useRef<boolean>(true);
62
186 - switch (data.type) {
187 - case 'no-change':
188 - case 'not-found':
189 - // No-op
190 - break;
191 - case 'hydrated-path':
192 - // Merge new data into previous object and invalidate cache
193 - element = store.getElementByID(id);
194 - if (element !== null) {
195 - if (currentlyInspectedElement != null) {
196 - const value = hydrateHelper(data.value, data.path);
197 - const inspectedElement = {...currentlyInspectedElement};
63 + // Track the paths insepected for the currently selected element.
64 + const [state, setState] = useState<{|
65 + element: Element | null,
66 + inspectedPaths: Object,
67 + |}>({
68 + element: null,
69 + inspectedPaths: {},
70 + });
71
199 - fillInPath(inspectedElement, data.value, data.path, value);
72 + const element =
73 + selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
74
201 - resource.write(element, inspectedElement);
75 + const elementHasChanged = element !== null && element !== state.element;
76
203 - // Schedule update with React if the currently-selected element has been invalidated.
204 - if (id === selectedElementID) {
205 - setCurrentlyInspectedElement(inspectedElement);
206 - }
207 - }
208 - }
209 - break;
210 - case 'full-data':
211 - const {
212 - canEditFunctionProps,
213 - canEditFunctionPropsDeletePaths,
214 - canEditFunctionPropsRenamePaths,
215 - canEditHooks,
216 - canEditHooksAndDeletePaths,
217 - canEditHooksAndRenamePaths,
218 - canToggleSuspense,
219 - canViewSource,
220 - hasLegacyContext,
221 - source,
222 - type,
223 - owners,
224 - context,
225 - hooks,
226 - props,
227 - rendererPackageName,
228 - rendererVersion,
229 - rootType,
230 - state,
231 - key,
232 - errors,
233 - warnings,
234 - } = ((data.value: any): InspectedElementBackend);
235 -
236 - const inspectedElement: InspectedElementFrontend = {
237 - canEditFunctionProps,
238 - canEditFunctionPropsDeletePaths,
239 - canEditFunctionPropsRenamePaths,
240 - canEditHooks,
241 - canEditHooksAndDeletePaths,
242 - canEditHooksAndRenamePaths,
243 - canToggleSuspense,
244 - canViewSource,
245 - hasLegacyContext,
246 - id,
247 - key,
248 - rendererPackageName,
249 - rendererVersion,
250 - rootType,
251 - source,
252 - type,
253 - owners:
254 - owners === null
255 - ? null
256 - : owners.map(owner => {
257 - const [
258 - displayName,
259 - hocDisplayNames,
260 - ] = separateDisplayNameAndHOCs(
261 - owner.displayName,
262 - owner.type,
263 - );
264 - return {
265 - ...owner,
266 - displayName,
267 - hocDisplayNames,
268 - };
269 - }),
270 - context: hydrateHelper(context),
271 - hooks: hydrateHelper(hooks),
272 - props: hydrateHelper(props),
273 - state: hydrateHelper(state),
274 - errors,
275 - warnings,
276 - };
277 -
278 - element = store.getElementByID(id);
279 - if (element !== null) {
280 - const request = inProgressRequests.get(element);
281 - if (request != null) {
282 - inProgressRequests.delete(element);
283 - batchedUpdates(() => {
284 - request.resolveFn(inspectedElement);
285 - setCurrentlyInspectedElement(inspectedElement);
286 - });
287 - } else {
288 - resource.write(element, inspectedElement);
77 + // Reset the cached inspected paths when a new element is selected.
78 + if (elementHasChanged) {
79 + setState({
80 + element,
81 + inspectedPaths: {},
82 + });
83 + }
84
290 - // Schedule update with React if the currently-selected element has been invalidated.
291 - if (id === selectedElementID) {
292 - setCurrentlyInspectedElement(inspectedElement);
293 - }
85 + // Don't load a stale element from the backend; it wastes bridge bandwidth.
86 + const inspectedElement =
87 + !elementHasChanged && element !== null
88 + ? inspectElement(
89 + element,
90 + state.inspectedPaths,
91 + forceUpdateRef.current,
92 + store,
93 + bridge,
94 + )
95 + : null;
96 +
97 + const inspectPaths: InspectPathFunction = useCallback<InspectPathFunction>(
98 + (path: Path) => {
99 + startTransition(() => {
100 + forceUpdateRef.current = true;
101 + setState(prevState => {
102 + const cloned = {...prevState};
103 + let current = cloned.inspectedPaths;
104 + path.forEach(key => {
105 + if (!current[key]) {
106 + current[key] = {};
107 }
295 - }
296 - break;
297 - default:
298 - break;
299 - }
300 - };
301 -
302 - bridge.addListener('inspectedElement', onInspectedElement);
303 - return () => bridge.removeListener('inspectedElement', onInspectedElement);
304 - }, [bridge, currentlyInspectedElement, selectedElementID, store]);
108 + current = current[key];
109 + });
110 + return cloned;
111 + });
112 + refresh();
113 + });
114 + },
115 + [setState],
116 + );
117
306 - // This effect handler polls for updates on the currently selected element.
118 + // Force backend update when inspected paths change.
119 useEffect(() => {
308 - if (selectedElementID === null) {
309 - return () => {};
310 - }
311 -
312 - const rendererID = store.getRendererIDForElement(selectedElementID);
313 -
314 - let timeoutID: TimeoutID | null = null;
120 + forceUpdateRef.current = false;
121 + }, [element, state]);
122
316 - const sendRequest = () => {
317 - timeoutID = null;
318 -
319 - if (rendererID !== null) {
320 - bridge.send('inspectElement', {id: selectedElementID, rendererID});
321 - }
322 - };
323 -
324 - // Send the initial inspection request.
325 - // We'll poll for an update in the response handler below.
326 - sendRequest();
327 -
328 - const onInspectedElement = (data: InspectedElementPayload) => {
329 - // If this is the element we requested, wait a little bit and then ask for another update.
330 - if (data.id === selectedElementID) {
331 - switch (data.type) {
332 - case 'no-change':
333 - case 'full-data':
334 - case 'hydrated-path':
335 - if (timeoutID !== null) {
336 - clearTimeout(timeoutID);
337 - }
338 - timeoutID = setTimeout(sendRequest, 1000);
339 - break;
340 - default:
341 - break;
342 - }
343 - }
344 - };
345 -
346 - bridge.addListener('inspectedElement', onInspectedElement);
347 -
348 - return () => {
349 - bridge.removeListener('inspectedElement', onInspectedElement);
350 -
351 - if (timeoutID !== null) {
123 + // Periodically poll the selected element for updates.
124 + useEffect(() => {
125 + if (element !== null) {
126 + const inspectedPaths = state.inspectedPaths;
127 + const checkForUpdateWrapper = () => {
128 + checkForUpdate({bridge, element, inspectedPaths, refresh, store});
129 + timeoutID = setTimeout(checkForUpdateWrapper, POLL_INTERVAL);
130 + };
131 + let timeoutID = setTimeout(checkForUpdateWrapper, POLL_INTERVAL);
132 + return () => {
133 clearTimeout(timeoutID);
353 - }
354 - };
355 - }, [bridge, selectedElementID, store]);
356 -
357 - const value = useMemo(
134 + };
135 + }
136 + }, [
137 + element,
138 + // Reset this timer any time the element we're inspecting gets a new response.
139 + // No sense to ping right away after e.g. inspecting/hydrating a path.
140 + inspectedElement,
141 + state,
142 + ]);
143 +
144 + const value = useMemo<Context>(
145 () => ({
359 - copyInspectedElementPath,
360 - getInspectedElement,
361 - getInspectedElementPath,
362 - refreshInspectedElement,
363 - storeAsGlobal,
146 + inspectedElement,
147 + inspectPaths,
148 }),
365 - // InspectedElement is used to invalidate the cache and schedule an update with React.
366 - [
367 - copyInspectedElementPath,
368 - currentlyInspectedElement,
369 - getInspectedElement,
370 - getInspectedElementPath,
371 - refreshInspectedElement,
372 - storeAsGlobal,
373 - ],
149 + [inspectedElement, inspectPaths],
150 );
151
152 return (
@@ -379,33 +155,3 @@ function InspectedElementContextController({children}: Props) {
155 </InspectedElementContext.Provider>
156 );
157 }
382 -
383 -function hydrateHelper(
384 - dehydratedData: DehydratedData | null,
385 - path?: Array<string | number>,
386 -): Object | null {
387 - if (dehydratedData !== null) {
388 - const {cleaned, data, unserializable} = dehydratedData;
389 -
390 - if (path) {
391 - const {length} = path;
392 - if (length > 0) {
393 - // Hydration helper requires full paths, but inspection dehydrates with relative paths.
394 - // In that event it's important that we adjust the "cleaned" paths to match.
395 - return hydrate(
396 - data,
397 - cleaned.map(cleanedPath => cleanedPath.slice(length)),
398 - unserializable.map(unserializablePath =>
399 - unserializablePath.slice(length),
400 - ),
401 - );
402 - }
403 - }
404 -
405 - return hydrate(data, cleaned, unserializable);
406 - } else {
407 - return null;
408 - }
409 -}
410 -
411 -export {InspectedElementContext, InspectedElementContextController};
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js
+5 -5
@@ -20,20 +20,20 @@ import {
20 ElementTypeFunction,
21 } from 'react-devtools-shared/src/types';
22
23 -import type {GetInspectedElementPath} from './InspectedElementContext';
23 import type {InspectedElement} from './types';
24 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
25 +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
26
27 type Props = {|
28 bridge: FrontendBridge,
29 - getInspectedElementPath: GetInspectedElementPath,
29 + element: Element,
30 inspectedElement: InspectedElement,
31 store: Store,
32 |};
33
34 export default function InspectedElementContextTree({
35 bridge,
36 - getInspectedElementPath,
36 + element,
37 inspectedElement,
38 store,
39 }: Props) {
@@ -81,15 +81,15 @@ export default function InspectedElementContextTree({
81 canEditValues={!isReadOnly}
82 canRenamePaths={!isReadOnly}
83 canRenamePathsAtDepth={canRenamePathsAtDepth}
84 - type="context"
84 depth={1}
86 - getInspectedElementPath={getInspectedElementPath}
85 + element={element}
86 hidden={false}
87 inspectedElement={inspectedElement}
88 name={name}
89 path={[name]}
90 pathRoot="context"
91 store={store}
92 + type="context"
93 value={value}
94 />
95 ))}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css new
+21
@@ -0,0 +1,21 @@
1 +.Error {
2 + justify-content: center;
3 + align-items: center;
4 + display: flex;
5 + flex-direction: column;
6 + height: 100%;
7 + font-size: var(--font-size-sans-large);
8 + font-weight: bold;
9 + text-align: center;
10 + background-color: var(--color-error-background);
11 + color: var(--color-error-text);
12 + border: 1px solid var(--color-error-border);
13 + padding: 1rem;
14 +}
15 +
16 +.Message {
17 + margin-bottom: 1rem;
18 +}
19 +
20 +.RetryButton {
21 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.js new
+93
@@ -0,0 +1,93 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 * as React from 'react';
11 +import {Component, useContext} from 'react';
12 +import {TreeDispatcherContext} from './TreeContext';
13 +import Button from 'react-devtools-shared/src/devtools/views/Button';
14 +import styles from './InspectedElementErrorBoundary.css';
15 +
16 +import type {DispatcherContext} from './InspectedElementErrorBoundary.css';
17 +
18 +type WrapperProps = {|
19 + children: React$Node,
20 +|};
21 +
22 +export default function InspectedElementErrorBoundaryWrapper({
23 + children,
24 +}: WrapperProps) {
25 + const dispatch = useContext(TreeDispatcherContext);
26 +
27 + return (
28 + <InspectedElementErrorBoundary children={children} dispatch={dispatch} />
29 + );
30 +}
31 +
32 +type Props = {|
33 + children: React$Node,
34 + dispatch: DispatcherContext,
35 +|};
36 +
37 +type State = {|
38 + errorMessage: string | null,
39 + hasError: boolean,
40 +|};
41 +
42 +const InitialState: State = {
43 + errorMessage: null,
44 + hasError: false,
45 +};
46 +
47 +class InspectedElementErrorBoundary extends Component<Props, State> {
48 + state: State = InitialState;
49 +
50 + static getDerivedStateFromError(error: any) {
51 + const errorMessage =
52 + typeof error === 'object' &&
53 + error !== null &&
54 + error.hasOwnProperty('message')
55 + ? error.message
56 + : error;
57 +
58 + return {
59 + errorMessage,
60 + hasError: true,
61 + };
62 + }
63 +
64 + render() {
65 + const {children} = this.props;
66 + const {errorMessage, hasError} = this.state;
67 +
68 + if (hasError) {
69 + return (
70 + <div className={styles.Error}>
71 + <div className={styles.Message}>{errorMessage || 'Error'}</div>
72 + <Button className={styles.RetryButton} onClick={this._retry}>
73 + Dismiss
74 + </Button>
75 + </div>
76 + );
77 + }
78 +
79 + return children;
80 + }
81 +
82 + _retry = () => {
83 + const {dispatch} = this.props;
84 + dispatch({
85 + type: 'SELECT_ELEMENT_BY_ID',
86 + payload: null,
87 + });
88 + this.setState({
89 + errorMessage: null,
90 + hasError: false,
91 + });
92 + };
93 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js
+55 -25
@@ -8,14 +8,21 @@
8 */
9
10 import * as React from 'react';
11 -import {useContext} from 'react';
11 +import {
12 + useContext,
13 + unstable_useCacheRefresh as useCacheRefresh,
14 + unstable_useTransition as useTransition,
15 +} from 'react';
16 import Button from '../Button';
17 import ButtonIcon from '../ButtonIcon';
18 import Store from '../../store';
19 import sharedStyles from './InspectedElementSharedStyles.css';
20 import styles from './InspectedElementErrorsAndWarningsTree.css';
21 import {SettingsContext} from '../Settings/SettingsContext';
18 -import {InspectedElementContext} from './InspectedElementContext';
22 +import {
23 + clearErrorsForElement as clearErrorsForElementAPI,
24 + clearWarningsForElement as clearWarningsForElementAPI,
25 +} from 'react-devtools-shared/src/backendAPI';
26
27 import type {InspectedElement} from './types';
28 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -31,7 +38,45 @@ export default function InspectedElementErrorsAndWarningsTree({
38 inspectedElement,
39 store,
40 }: Props) {
34 - const {refreshInspectedElement} = useContext(InspectedElementContext);
41 + const refresh = useCacheRefresh();
42 +
43 + const [
44 + startClearErrorsTransition,
45 + isErrorsTransitionPending,
46 + ] = useTransition();
47 + const clearErrorsForInspectedElement = () => {
48 + const {id} = inspectedElement;
49 + const rendererID = store.getRendererIDForElement(id);
50 + if (rendererID !== null) {
51 + startClearErrorsTransition(() => {
52 + clearErrorsForElementAPI({
53 + bridge,
54 + id,
55 + rendererID,
56 + });
57 + refresh();
58 + });
59 + }
60 + };
61 +
62 + const [
63 + startClearWarningsTransition,
64 + isWarningsTransitionPending,
65 + ] = useTransition();
66 + const clearWarningsForInspectedElement = () => {
67 + const {id} = inspectedElement;
68 + const rendererID = store.getRendererIDForElement(id);
69 + if (rendererID !== null) {
70 + startClearWarningsTransition(() => {
71 + clearWarningsForElementAPI({
72 + bridge,
73 + id,
74 + rendererID,
75 + });
76 + refresh();
77 + });
78 + }
79 + };
80
81 const {showInlineWarningsAndErrors} = useContext(SettingsContext);
82 if (!showInlineWarningsAndErrors) {
@@ -40,26 +85,6 @@ export default function InspectedElementErrorsAndWarningsTree({
85
86 const {errors, warnings} = inspectedElement;
87
43 - const clearErrors = () => {
44 - const {id} = inspectedElement;
45 - store.clearErrorsForElement(id);
46 -
47 - // Immediately poll for updated data.
48 - // This avoids a delay between clicking the clear button and refreshing errors.
49 - // Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
50 - refreshInspectedElement();
51 - };
52 -
53 - const clearWarnings = () => {
54 - const {id} = inspectedElement;
55 - store.clearWarningsForElement(id);
56 -
57 - // Immediately poll for updated data.
58 - // This avoids a delay between clicking the clear button and refreshing warnings.
59 - // Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
60 - refreshInspectedElement();
61 - };
62 -
88 return (
89 <React.Fragment>
90 {errors.length > 0 && (
@@ -67,8 +92,9 @@ export default function InspectedElementErrorsAndWarningsTree({
92 badgeClassName={styles.ErrorBadge}
93 bridge={bridge}
94 className={styles.ErrorTree}
70 - clearMessages={clearErrors}
95 + clearMessages={clearErrorsForInspectedElement}
96 entries={errors}
97 + isTransitionPending={isErrorsTransitionPending}
98 label="errors"
99 messageClassName={styles.Error}
100 />
@@ -78,8 +104,9 @@ export default function InspectedElementErrorsAndWarningsTree({
104 badgeClassName={styles.WarningBadge}
105 bridge={bridge}
106 className={styles.WarningTree}
81 - clearMessages={clearWarnings}
107 + clearMessages={clearWarningsForInspectedElement}
108 entries={warnings}
109 + isTransitionPending={isWarningsTransitionPending}
110 label="warnings"
111 messageClassName={styles.Warning}
112 />
@@ -94,6 +121,7 @@ type TreeProps = {|
121 className: string,
122 clearMessages: () => {},
123 entries: Array<[string, number]>,
124 + isTransitionPending: boolean,
125 label: string,
126 messageClassName: string,
127 |};
@@ -104,6 +132,7 @@ function Tree({
132 className,
133 clearMessages,
134 entries,
135 + isTransitionPending,
136 label,
137 messageClassName,
138 }: TreeProps) {
@@ -115,6 +144,7 @@ function Tree({
144 <div className={`${sharedStyles.HeaderRow} ${styles.HeaderRow}`}>
145 <div className={sharedStyles.Header}>{label}</div>
146 <Button
147 + disabled={isTransitionPending}
148 onClick={clearMessages}
149 title={`Clear all ${label} for this component`}>
150 <ButtonIcon type="clear" />
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
+14 -20
@@ -22,20 +22,20 @@ import useContextMenu from '../../ContextMenu/useContextMenu';
22 import {meta} from '../../../hydration';
23
24 import type {InspectedElement} from './types';
25 -import type {GetInspectedElementPath} from './InspectedElementContext';
25 import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
26 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
27 +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
28
29 type HooksTreeViewProps = {|
30 bridge: FrontendBridge,
31 - getInspectedElementPath: GetInspectedElementPath,
31 + element: Element,
32 inspectedElement: InspectedElement,
33 store: Store,
34 |};
35
36 export function InspectedElementHooksTree({
37 bridge,
38 - getInspectedElementPath,
38 + element,
39 inspectedElement,
40 store,
41 }: HooksTreeViewProps) {
@@ -57,7 +57,7 @@ export function InspectedElementHooksTree({
57 <InnerHooksTreeView
58 hooks={hooks}
59 id={id}
60 - getInspectedElementPath={getInspectedElementPath}
60 + element={element}
61 inspectedElement={inspectedElement}
62 path={[]}
63 />
@@ -67,7 +67,7 @@ export function InspectedElementHooksTree({
67 }
68
69 type InnerHooksTreeViewProps = {|
70 - getInspectedElementPath: GetInspectedElementPath,
70 + element: Element,
71 hooks: HooksTree,
72 id: number,
73 inspectedElement: InspectedElement,
@@ -75,7 +75,7 @@ type InnerHooksTreeViewProps = {|
75 |};
76
77 export function InnerHooksTreeView({
78 - getInspectedElementPath,
78 + element,
79 hooks,
80 id,
81 inspectedElement,
@@ -85,7 +85,7 @@ export function InnerHooksTreeView({
85 return hooks.map((hook, index) => (
86 <HookView
87 key={index}
88 - getInspectedElementPath={getInspectedElementPath}
88 + element={element}
89 hook={hooks[index]}
90 id={id}
91 inspectedElement={inspectedElement}
@@ -95,20 +95,14 @@ export function InnerHooksTreeView({
95 }
96
97 type HookViewProps = {|
98 - getInspectedElementPath: GetInspectedElementPath,
98 + element: Element,
99 hook: HooksNode,
100 id: number,
101 inspectedElement: InspectedElement,
102 path: Array<string | number>,
103 |};
104
105 -function HookView({
106 - getInspectedElementPath,
107 - hook,
108 - id,
109 - inspectedElement,
110 - path,
111 -}: HookViewProps) {
105 +function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
106 const {
107 canEditHooks,
108 canEditHooksAndDeletePaths,
@@ -195,7 +189,7 @@ function HookView({
189 if (isCustomHook) {
190 const subHooksView = Array.isArray(subHooks) ? (
191 <InnerHooksTreeView
198 - getInspectedElementPath={getInspectedElementPath}
192 + element={element}
193 hooks={subHooks}
194 id={id}
195 inspectedElement={inspectedElement}
@@ -210,7 +204,7 @@ function HookView({
204 canRenamePaths={canRenamePaths}
205 canRenamePathsAtDepth={canRenamePathsAtDepth}
206 depth={1}
213 - getInspectedElementPath={getInspectedElementPath}
207 + element={element}
208 hookID={hookID}
209 inspectedElement={inspectedElement}
210 name="subHooks"
@@ -244,7 +238,7 @@ function HookView({
238 canRenamePaths={canRenamePaths}
239 canRenamePathsAtDepth={canRenamePathsAtDepth}
240 depth={1}
247 - getInspectedElementPath={getInspectedElementPath}
241 + element={element}
242 hookID={hookID}
243 inspectedElement={inspectedElement}
244 name="DebugValue"
@@ -290,7 +284,7 @@ function HookView({
284 canRenamePaths={canRenamePaths}
285 canRenamePathsAtDepth={canRenamePathsAtDepth}
286 depth={1}
293 - getInspectedElementPath={getInspectedElementPath}
287 + element={element}
288 hookID={hookID}
289 inspectedElement={inspectedElement}
290 name={name}
@@ -311,7 +305,7 @@ function HookView({
305 canEditValues={canEditValues}
306 canRenamePaths={false}
307 depth={1}
314 - getInspectedElementPath={getInspectedElementPath}
308 + element={element}
309 hookID={hookID}
310 inspectedElement={inspectedElement}
311 name={name}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js
+4 -4
@@ -19,19 +19,19 @@ import styles from './InspectedElementSharedStyles.css';
19 import {ElementTypeClass} from 'react-devtools-shared/src/types';
20
21 import type {InspectedElement} from './types';
22 -import type {GetInspectedElementPath} from './InspectedElementContext';
22 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
23 +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
24
25 type Props = {|
26 bridge: FrontendBridge,
27 - getInspectedElementPath: GetInspectedElementPath,
27 + element: Element,
28 inspectedElement: InspectedElement,
29 store: Store,
30 |};
31
32 export default function InspectedElementPropsTree({
33 bridge,
34 - getInspectedElementPath,
34 + element,
35 inspectedElement,
36 store,
37 }: Props) {
@@ -78,7 +78,7 @@ export default function InspectedElementPropsTree({
78 canEditValues={canEditValues}
79 canRenamePaths={canRenamePaths}
80 depth={1}
81 - getInspectedElementPath={getInspectedElementPath}
81 + element={element}
82 hidden={false}
83 inspectedElement={inspectedElement}
84 name={name}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js
+4 -4
@@ -17,19 +17,19 @@ import Store from '../../store';
17 import styles from './InspectedElementSharedStyles.css';
18
19 import type {InspectedElement} from './types';
20 -import type {GetInspectedElementPath} from './InspectedElementContext';
20 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
21 +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
22
23 type Props = {|
24 bridge: FrontendBridge,
25 - getInspectedElementPath: GetInspectedElementPath,
25 + element: Element,
26 inspectedElement: InspectedElement,
27 store: Store,
28 |};
29
30 export default function InspectedElementStateTree({
31 bridge,
32 - getInspectedElementPath,
32 + element,
33 inspectedElement,
34 store,
35 }: Props) {
@@ -68,7 +68,7 @@ export default function InspectedElementStateTree({
68 canEditValues={true}
69 canRenamePaths={true}
70 depth={1}
71 - getInspectedElementPath={getInspectedElementPath}
71 + element={element}
72 hidden={false}
73 inspectedElement={inspectedElement}
74 name={name}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+63 -44
@@ -27,15 +27,14 @@ import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
27 import NativeStyleEditor from './NativeStyleEditor';
28 import Badge from './Badge';
29 import {useHighlightNativeElement} from '../hooks';
30 +import {
31 + copyInspectedElementPath as copyInspectedElementPathAPI,
32 + storeAsGlobal as storeAsGlobalAPI,
33 +} from 'react-devtools-shared/src/backendAPI';
34
35 import styles from './InspectedElementView.css';
36
37 import type {ContextMenuContextType} from '../context';
34 -import type {
35 - CopyInspectedElementPath,
36 - GetInspectedElementPath,
37 - StoreAsGlobal,
38 -} from './InspectedElementContext';
38 import type {Element, InspectedElement, Owner} from './types';
39 import type {ElementType} from 'react-devtools-shared/src/types';
40
@@ -43,19 +42,13 @@ export type CopyPath = (path: Array<string | number>) => void;
42 export type InspectPath = (path: Array<string | number>) => void;
43
44 type Props = {|
46 - copyInspectedElementPath: CopyInspectedElementPath,
45 element: Element,
48 - getInspectedElementPath: GetInspectedElementPath,
46 inspectedElement: InspectedElement,
50 - storeAsGlobal: StoreAsGlobal,
47 |};
48
49 export default function InspectedElementView({
54 - copyInspectedElementPath,
50 element,
56 - getInspectedElementPath,
51 inspectedElement,
58 - storeAsGlobal,
52 }: Props) {
53 const {id} = element;
54 const {
@@ -89,7 +82,7 @@ export default function InspectedElementView({
82
83 <InspectedElementPropsTree
84 bridge={bridge}
92 - getInspectedElementPath={getInspectedElementPath}
85 + element={element}
86 inspectedElement={inspectedElement}
87 store={store}
88 />
@@ -102,28 +95,28 @@ export default function InspectedElementView({
95
96 <InspectedElementStateTree
97 bridge={bridge}
105 - getInspectedElementPath={getInspectedElementPath}
98 + element={element}
99 inspectedElement={inspectedElement}
100 store={store}
101 />
102
103 <InspectedElementHooksTree
104 bridge={bridge}
112 - getInspectedElementPath={getInspectedElementPath}
105 + element={element}
106 inspectedElement={inspectedElement}
107 store={store}
108 />
109
110 <InspectedElementContextTree
111 bridge={bridge}
119 - getInspectedElementPath={getInspectedElementPath}
112 + element={element}
113 inspectedElement={inspectedElement}
114 store={store}
115 />
116
117 <InspectedElementErrorsAndWarningsTree
118 bridge={bridge}
126 - getInspectedElementPath={getInspectedElementPath}
119 + element={element}
120 inspectedElement={inspectedElement}
121 store={store}
122 />
@@ -160,34 +153,60 @@ export default function InspectedElementView({
153
154 {isContextMenuEnabledForInspectedElement && (
155 <ContextMenu id="InspectedElement">
163 - {data => (
164 - <Fragment>
165 - <ContextMenuItem
166 - onClick={() => copyInspectedElementPath(id, data.path)}
167 - title="Copy value to clipboard">
168 - <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
169 - value to clipboard
170 - </ContextMenuItem>
171 - <ContextMenuItem
172 - onClick={() => storeAsGlobal(id, data.path)}
173 - title="Store as global variable">
174 - <Icon
175 - className={styles.ContextMenuIcon}
176 - type="store-as-global-variable"
177 - />{' '}
178 - Store as global variable
179 - </ContextMenuItem>
180 - {viewAttributeSourceFunction !== null &&
181 - data.type === 'function' && (
182 - <ContextMenuItem
183 - onClick={() => viewAttributeSourceFunction(id, data.path)}
184 - title="Go to definition">
185 - <Icon className={styles.ContextMenuIcon} type="code" /> Go
186 - to definition
187 - </ContextMenuItem>
188 - )}
189 - </Fragment>
190 - )}
156 + {({path, type: pathType}) => {
157 + const copyInspectedElementPath = () => {
158 + const rendererID = store.getRendererIDForElement(id);
159 + if (rendererID !== null) {
160 + copyInspectedElementPathAPI({
161 + bridge,
162 + id,
163 + path,
164 + rendererID,
165 + });
166 + }
167 + };
168 +
169 + const storeAsGlobal = () => {
170 + const rendererID = store.getRendererIDForElement(id);
171 + if (rendererID !== null) {
172 + storeAsGlobalAPI({
173 + bridge,
174 + id,
175 + path,
176 + rendererID,
177 + });
178 + }
179 + };
180 +
181 + return (
182 + <Fragment>
183 + <ContextMenuItem
184 + onClick={copyInspectedElementPath}
185 + title="Copy value to clipboard">
186 + <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
187 + value to clipboard
188 + </ContextMenuItem>
189 + <ContextMenuItem
190 + onClick={storeAsGlobal}
191 + title="Store as global variable">
192 + <Icon
193 + className={styles.ContextMenuIcon}
194 + type="store-as-global-variable"
195 + />{' '}
196 + Store as global variable
197 + </ContextMenuItem>
198 + {viewAttributeSourceFunction !== null &&
199 + pathType === 'function' && (
200 + <ContextMenuItem
201 + onClick={() => viewAttributeSourceFunction(id, path)}
202 + title="Go to definition">
203 + <Icon className={styles.ContextMenuIcon} type="code" /> Go
204 + to definition
205 + </ContextMenuItem>
206 + )}
207 + </Fragment>
208 + );
209 + }}
210 </ContextMenu>
211 )}
212 </Fragment>
packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css
+4
@@ -2,6 +2,10 @@
2 display: flex;
3 }
4
5 +.DisabledItem {
6 + opacity: 0.5;
7 +}
8 +
9 .Name {
10 color: var(--color-attribute-name-not-editable);
11 flex: 0 0 auto;
packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+45 -19
@@ -8,11 +8,17 @@
8 */
9
10 import * as React from 'react';
11 -import {useEffect, useRef, useState} from 'react';
11 +import {
12 + unstable_useTransition as useTransition,
13 + useContext,
14 + useRef,
15 + useState,
16 +} from 'react';
17 import EditableName from './EditableName';
18 import EditableValue from './EditableValue';
19 import NewArrayValue from './NewArrayValue';
20 import NewKeyValue from './NewKeyValue';
21 +import LoadingAnimation from './LoadingAnimation';
22 import ExpandCollapseToggle from './ExpandCollapseToggle';
23 import {alphaSortEntries, getMetaValueLabel} from '../utils';
24 import {meta} from '../../../hydration';
@@ -22,11 +28,12 @@ import {parseHookPathForEdit} from './utils';
28 import styles from './KeyValue.css';
29 import Button from 'react-devtools-shared/src/devtools/views/Button';
30 import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
31 +import {InspectedElementContext} from './InspectedElementContext';
32
33 import type {InspectedElement} from './types';
27 -import type {Element} from 'react';
34 +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
35 +import type {Element as ReactElement} from 'react';
36 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
29 -import type {GetInspectedElementPath} from './InspectedElementContext';
37
38 type Type = 'props' | 'state' | 'context' | 'hooks';
39
@@ -38,9 +45,9 @@ type KeyValueProps = {|
45 canRenamePaths: boolean,
46 canRenamePathsAtDepth?: (depth: number) => boolean,
47 depth: number,
48 + element: Element,
49 hidden: boolean,
50 hookID?: ?number,
43 - getInspectedElementPath: GetInspectedElementPath,
51 inspectedElement: InspectedElement,
52 isDirectChildOfAnArray?: boolean,
53 name: string,
@@ -58,7 +65,7 @@ export default function KeyValue({
65 canRenamePaths,
66 canRenamePathsAtDepth,
67 depth,
61 - getInspectedElementPath,
68 + element,
69 inspectedElement,
70 isDirectChildOfAnArray,
71 hidden,
@@ -72,9 +79,10 @@ export default function KeyValue({
79 const {id} = inspectedElement;
80
81 const [isOpen, setIsOpen] = useState<boolean>(false);
75 - const prevIsOpenRef = useRef(isOpen);
82 const contextMenuTriggerRef = useRef(null);
83
84 + const {inspectPaths} = useContext(InspectedElementContext);
85 +
86 let isInspectable = false;
87 let isReadOnly = false;
88 if (value !== null && typeof value === 'object') {
@@ -82,14 +90,20 @@ export default function KeyValue({
90 isReadOnly = value[meta.readonly];
91 }
92
85 - useEffect(() => {
86 - if (isInspectable && isOpen && !prevIsOpenRef.current) {
87 - getInspectedElementPath(id, [pathRoot, ...path]);
88 - }
89 - prevIsOpenRef.current = isOpen;
90 - }, [getInspectedElementPath, isInspectable, isOpen, path, pathRoot]);
93 + const [startInspectPathsTransition, isInspectPathsPending] = useTransition();
94 + const toggleIsOpen = () => {
95 + if (isOpen) {
96 + setIsOpen(false);
97 + } else {
98 + setIsOpen(true);
99
92 - const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
100 + if (isInspectable) {
101 + startInspectPathsTransition(() => {
102 + inspectPaths([pathRoot, ...path]);
103 + });
104 + }
105 + }
106 + };
107
108 useContextMenu({
109 data: {
@@ -255,7 +269,7 @@ export default function KeyValue({
269 ref={contextMenuTriggerRef}
270 style={style}>
271 {isInspectable ? (
258 - <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
272 + <ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} />
273 ) : (
274 <div className={styles.ExpandCollapseToggleSpacer} />
275 )}
@@ -268,6 +282,18 @@ export default function KeyValue({
282 </span>
283 </div>
284 );
285 +
286 + if (isInspectPathsPending) {
287 + children = (
288 + <>
289 + {children}
290 + <div className={styles.Item} style={style}>
291 + <div className={styles.ExpandCollapseToggleSpacer} />
292 + <LoadingAnimation />
293 + </div>
294 + </>
295 + );
296 + }
297 } else {
298 if (Array.isArray(value)) {
299 const hasChildren = value.length > 0 || canEditValues;
@@ -283,7 +309,7 @@ export default function KeyValue({
309 canRenamePaths={canRenamePaths && !isReadOnly}
310 canRenamePathsAtDepth={canRenamePathsAtDepth}
311 depth={depth + 1}
286 - getInspectedElementPath={getInspectedElementPath}
312 + element={element}
313 hookID={hookID}
314 inspectedElement={inspectedElement}
315 isDirectChildOfAnArray={true}
@@ -305,7 +331,7 @@ export default function KeyValue({
331 hidden={hidden || !isOpen}
332 hookID={hookID}
333 index={value.length}
308 - getInspectedElementPath={getInspectedElementPath}
334 + element={element}
335 inspectedElement={inspectedElement}
336 path={path}
337 store={store}
@@ -347,7 +373,7 @@ export default function KeyValue({
373 const hasChildren = entries.length > 0 || canEditValues;
374 const displayName = getMetaValueLabel(value);
375
350 - children = entries.map<Element<any>>(([key, keyValue]) => (
376 + children = entries.map<ReactElement<any>>(([key, keyValue]) => (
377 <KeyValue
378 key={key}
379 alphaSort={alphaSort}
@@ -357,7 +383,7 @@ export default function KeyValue({
383 canRenamePaths={canRenamePaths && !isReadOnly}
384 canRenamePathsAtDepth={canRenamePathsAtDepth}
385 depth={depth + 1}
360 - getInspectedElementPath={getInspectedElementPath}
386 + element={element}
387 hookID={hookID}
388 inspectedElement={inspectedElement}
389 hidden={hidden || !isOpen}
@@ -375,7 +401,7 @@ export default function KeyValue({
401 key="NewKeyValue"
402 bridge={bridge}
403 depth={depth + 1}
378 - getInspectedElementPath={getInspectedElementPath}
404 + element={element}
405 hidden={hidden || !isOpen}
406 hookID={hookID}
407 inspectedElement={inspectedElement}
packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.css new
+5
@@ -0,0 +1,5 @@
1 +.Icon {
2 + width: 1rem;
3 + height: 1rem;
4 + fill: currentColor;
5 +}
packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.js new
+55
@@ -0,0 +1,55 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 * as React from 'react';
11 +import styles from './LoadingAnimation.css';
12 +
13 +type Props = {|
14 + className?: string,
15 +|};
16 +
17 +export default function LoadingAnimation({className = ''}: Props) {
18 + return (
19 + <svg
20 + xmlns="http://www.w3.org/2000/svg"
21 + className={`${styles.Icon} ${className}`}
22 + width="24"
23 + height="24"
24 + viewBox="0 0 100 100">
25 + <path d="M0 0h100v100H0z" fill="none" />
26 + <circle fill="currentColor" stroke="none" cx="20" cy="50" r="10">
27 + <animate
28 + attributeName="opacity"
29 + dur="1s"
30 + values="0;1;0"
31 + repeatCount="indefinite"
32 + begin="0.1"
33 + />
34 + </circle>
35 + <circle fill="currentColor" stroke="none" cx="50" cy="50" r="10">
36 + <animate
37 + attributeName="opacity"
38 + dur="1s"
39 + values="0;1;0"
40 + repeatCount="indefinite"
41 + begin="0.2"
42 + />
43 + </circle>
44 + <circle fill="currentColor" stroke="none" cx="80" cy="50" r="10">
45 + <animate
46 + attributeName="opacity"
47 + dur="1s"
48 + values="0;1;0"
49 + repeatCount="indefinite"
50 + begin="0.3"
51 + />
52 + </circle>
53 + </svg>
54 + );
55 +}
packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+6 -2
@@ -32,7 +32,7 @@ import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views
32 import SelectedTreeHighlight from './SelectedTreeHighlight';
33 import TreeFocusedContext from './TreeFocusedContext';
34 import {useHighlightNativeElement, useSubscription} from '../hooks';
35 -
35 +import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI';
36 import styles from './Tree.css';
37 import ButtonIcon from '../ButtonIcon';
38 import Button from '../Button';
@@ -327,6 +327,10 @@ export default function Tree(props: Props) {
327 );
328 const {errors, warnings} = useSubscription(errorsOrWarningsSubscription);
329
330 + const clearErrorsAndWarnings = () => {
331 + clearErrorsAndWarningsAPI({bridge, store});
332 + };
333 +
334 return (
335 <TreeFocusedContext.Provider value={treeFocused}>
336 <div className={styles.Tree} ref={treeRef}>
@@ -368,7 +372,7 @@ export default function Tree(props: Props) {
372 <ButtonIcon type="down" />
373 </Button>
374 <Button
371 - onClick={() => store.clearErrorsAndWarnings()}
375 + onClick={clearErrorsAndWarnings}
376 title="Clear all errors and warnings">
377 <ButtonIcon type="clear" />
378 </Button>
packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js
+14 -5
@@ -38,14 +38,25 @@ const InitialState: State = {
38 export default class ErrorBoundary extends Component<Props, State> {
39 state: State = InitialState;
40
41 - componentDidCatch(error: any, {componentStack}: any) {
41 + static getDerivedStateFromError(error: any) {
42 const errorMessage =
43 - typeof error === 'object' && error.hasOwnProperty('message')
43 + typeof error === 'object' &&
44 + error !== null &&
45 + error.hasOwnProperty('message')
46 ? error.message
47 : error;
48
49 + return {
50 + errorMessage,
51 + hasError: true,
52 + };
53 + }
54 +
55 + componentDidCatch(error: any, {componentStack}: any) {
56 const callStack =
48 - typeof error === 'object' && error.hasOwnProperty('stack')
57 + typeof error === 'object' &&
58 + error !== null &&
59 + error.hasOwnProperty('stack')
60 ? error.stack
61 .split('\n')
62 .slice(1)
@@ -55,8 +66,6 @@ export default class ErrorBoundary extends Component<Props, State> {
66 this.setState({
67 callStack,
68 componentStack,
58 - errorMessage,
59 - hasError: true,
69 });
70 }
71
packages/react-devtools-shared/src/inspectedElementCache.js new
+220
@@ -0,0 +1,220 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 {
11 + unstable_getCacheForType,
12 + unstable_startTransition as startTransition,
13 +} from 'react';
14 +import Store from './devtools/store';
15 +import {
16 + convertInspectedElementBackendToFrontend,
17 + inspectElement as inspectElementAPI,
18 +} from './backendAPI';
19 +
20 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
21 +import type {Wakeable} from 'shared/ReactTypes';
22 +import type {
23 + InspectedElement as InspectedElementBackend,
24 + InspectedElementPayload,
25 +} from 'react-devtools-shared/src/backend/types';
26 +import type {
27 + Element,
28 + InspectedElement as InspectedElementFrontend,
29 +} from 'react-devtools-shared/src/devtools/views/Components/types';
30 +
31 +const Pending = 0;
32 +const Resolved = 1;
33 +const Rejected = 2;
34 +
35 +type PendingRecord = {|
36 + status: 0,
37 + value: Wakeable,
38 +|};
39 +
40 +type ResolvedRecord<T> = {|
41 + status: 1,
42 + value: T,
43 +|};
44 +
45 +type RejectedRecord = {|
46 + status: 2,
47 + value: string,
48 +|};
49 +
50 +type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
51 +
52 +function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
53 + if (record.status === Resolved) {
54 + // This is just a type refinement.
55 + return record;
56 + } else {
57 + throw record.value;
58 + }
59 +}
60 +
61 +type InspectedElementMap = WeakMap<Element, Record<InspectedElementFrontend>>;
62 +type CacheSeedKey = () => InspectedElementMap;
63 +
64 +function createMap(): InspectedElementMap {
65 + return new WeakMap();
66 +}
67 +
68 +function getRecordMap(): WeakMap<Element, Record<InspectedElementFrontend>> {
69 + return unstable_getCacheForType(createMap);
70 +}
71 +
72 +function createCacheSeed(
73 + element: Element,
74 + inspectedElement: InspectedElementFrontend,
75 +): [CacheSeedKey, InspectedElementMap] {
76 + const newRecord: Record<InspectedElementFrontend> = {
77 + status: Resolved,
78 + value: inspectedElement,
79 + };
80 + const map = createMap();
81 + map.set(element, newRecord);
82 + return [createMap, map];
83 +}
84 +
85 +/**
86 + * Fetches element props and state from the backend for inspection.
87 + * This method should be called during render; it will suspend if data has not yet been fetched.
88 + */
89 +export function inspectElement(
90 + element: Element,
91 + inspectedPaths: Object,
92 + forceUpdate: boolean,
93 + store: Store,
94 + bridge: FrontendBridge,
95 +): InspectedElementFrontend | null {
96 + const map = getRecordMap();
97 + let record = map.get(element);
98 + if (!record) {
99 + const callbacks = new Set();
100 + const wakeable: Wakeable = {
101 + then(callback) {
102 + callbacks.add(callback);
103 + },
104 + };
105 + const wake = () => {
106 + // This assumes they won't throw.
107 + callbacks.forEach(callback => callback());
108 + callbacks.clear();
109 + };
110 + const newRecord: Record<InspectedElementFrontend> = (record = {
111 + status: Pending,
112 + value: wakeable,
113 + });
114 +
115 + const rendererID = store.getRendererIDForElement(element.id);
116 + if (rendererID == null) {
117 + const rejectedRecord = ((newRecord: any): RejectedRecord);
118 + rejectedRecord.status = Rejected;
119 + rejectedRecord.value = 'Inspected element not found.';
120 + return null;
121 + }
122 +
123 + inspectElementAPI({
124 + bridge,
125 + forceUpdate: true,
126 + id: element.id,
127 + inspectedPaths,
128 + rendererID: ((rendererID: any): number),
129 + }).then(
130 + (data: InspectedElementPayload) => {
131 + if (newRecord.status === Pending) {
132 + switch (data.type) {
133 + case 'no-change':
134 + // This response type should never be received.
135 + // We always send forceUpdate:true when we have a cache miss.
136 + break;
137 +
138 + case 'not-found':
139 + const notFoundRecord = ((newRecord: any): RejectedRecord);
140 + notFoundRecord.status = Rejected;
141 + notFoundRecord.value = 'Inspected element not found.';
142 + wake();
143 + break;
144 +
145 + case 'full-data':
146 + const resolvedRecord = ((newRecord: any): ResolvedRecord<InspectedElementFrontend>);
147 + resolvedRecord.status = Resolved;
148 + resolvedRecord.value = convertInspectedElementBackendToFrontend(
149 + ((data.value: any): InspectedElementBackend),
150 + );
151 + wake();
152 + break;
153 + }
154 + }
155 + },
156 +
157 + () => {
158 + // Timed out without receiving a response.
159 + if (newRecord.status === Pending) {
160 + const timedOutRecord = ((newRecord: any): RejectedRecord);
161 + timedOutRecord.status = Rejected;
162 + timedOutRecord.value = 'Inspected element timed out.';
163 + wake();
164 + }
165 + },
166 + );
167 + map.set(element, record);
168 + }
169 +
170 + const response = readRecord(record).value;
171 + return response;
172 +}
173 +
174 +type RefreshFunction = (
175 + seedKey: CacheSeedKey,
176 + cacheMap: InspectedElementMap,
177 +) => void;
178 +
179 +/**
180 + * Asks the backend for updated props and state from an expected element.
181 + * This method should never be called during render; call it from an effect or event handler.
182 + * This method will schedule an update if updated information is returned.
183 + */
184 +export function checkForUpdate({
185 + bridge,
186 + element,
187 + inspectedPaths,
188 + refresh,
189 + store,
190 +}: {
191 + bridge: FrontendBridge,
192 + element: Element,
193 + inspectedPaths: Object,
194 + refresh: RefreshFunction,
195 + store: Store,
196 +}): void {
197 + const {id} = element;
198 + const rendererID = store.getRendererIDForElement(id);
199 + if (rendererID != null) {
200 + inspectElementAPI({
201 + bridge,
202 + forceUpdate: false,
203 + id,
204 + inspectedPaths,
205 + rendererID,
206 + }).then((data: InspectedElementPayload) => {
207 + switch (data.type) {
208 + case 'full-data':
209 + const inspectedElement = convertInspectedElementBackendToFrontend(
210 + ((data.value: any): InspectedElementBackend),
211 + );
212 + startTransition(() => {
213 + const [key, value] = createCacheSeed(element, inspectedElement);
214 + refresh(key, value);
215 + });
216 + break;
217 + }
218 + });
219 + }
220 +}
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1 -1
@@ -23,7 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
25 export const enableLazyElements = false;
26 -export const enableCache = false;
26 +export const enableCache = __EXPERIMENTAL__;
27 export const disableJavaScriptURLs = false;
28 export const disableInputAttributeSyncing = false;
29 export const enableSchedulerDebugging = false;
scripts/jest/config.build-devtools.js
+3
@@ -58,6 +58,9 @@ module.exports = Object.assign({}, baseConfig, {
58 transformIgnorePatterns: ['/node_modules/', '<rootDir>/build2/'],
59 testRegex: 'packages/react-devtools-shared/src/__tests__/[^]+.test.js$',
60 snapshotSerializers: [
61 + require.resolve(
62 + '../../packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js'
63 + ),
64 require.resolve(
65 '../../packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js'
66 ),
scripts/jest/preprocessor.js
+4
@@ -59,6 +59,10 @@ const babelOptions = {
59
60 module.exports = {
61 process: function(src, filePath) {
62 + if (filePath.match(/\.css$/)) {
63 + // Don't try to parse CSS modules; they aren't needed for tests anyway.
64 + return '';
65 + }
66 if (filePath.match(/\.coffee$/)) {
67 return coffee.compile(src, {bare: true});
68 }