@samitouri / QOS-React-2 / commits / 09a2c363a5

Expose DEV-mode warnings in devtools UI (#20463)

Co-authored-by: Brian Vaughn <bvaughn@fb.com>

Sebastian Silbermann committed Dec 22, 2020 at 17:09 UTC 09a2c363a5175291ecdcbf7f39b0d165bc7da8ec
50 files changed +4613 -1794
packages/react-devtools-core/src/standalone.js
+4
@@ -20,6 +20,7 @@ import {
20 getAppendComponentStack,
21 getBreakOnConsoleErrors,
22 getSavedComponentFilters,
23 + getShowInlineWarningsAndErrors,
24 } from 'react-devtools-shared/src/utils';
25 import {Server} from 'ws';
26 import {join} from 'path';
@@ -303,6 +304,9 @@ function startServer(
304 )};
305 window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
306 getSavedComponentFilters(),
307 + )};
308 + window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
309 + getShowInlineWarningsAndErrors(),
310 )};`;
311
312 response.end(
packages/react-devtools-extensions/src/main.js
+7 -6
@@ -10,6 +10,7 @@ import {
10 getAppendComponentStack,
11 getBreakOnConsoleErrors,
12 getSavedComponentFilters,
13 + getShowInlineWarningsAndErrors,
14 } from 'react-devtools-shared/src/utils';
15 import {
16 localStorageGetItem,
@@ -29,18 +30,18 @@ let panelCreated = false;
30 // because they are stored in localStorage within the context of the extension.
31 // Instead it relies on the extension to pass filters through.
32 function syncSavedPreferences() {
32 - const appendComponentStack = getAppendComponentStack();
33 - const breakOnConsoleErrors = getBreakOnConsoleErrors();
34 - const componentFilters = getSavedComponentFilters();
33 chrome.devtools.inspectedWindow.eval(
34 `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
37 - appendComponentStack,
35 + getAppendComponentStack(),
36 )};
37 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
40 - breakOnConsoleErrors,
38 + getBreakOnConsoleErrors(),
39 )};
40 window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
43 - componentFilters,
41 + getSavedComponentFilters(),
42 + )};
43 + window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
44 + getShowInlineWarningsAndErrors(),
45 )};`,
46 );
47 }
packages/react-devtools-inline/src/backend.js
+3
@@ -24,11 +24,13 @@ function startActivation(contentWindow: window) {
24 appendComponentStack,
25 breakOnConsoleErrors,
26 componentFilters,
27 + showInlineWarningsAndErrors,
28 } = data;
29
30 contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
31 contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
32 contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
33 + contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
34
35 // TRICKY
36 // The backend entry point may be required in the context of an iframe or the parent window.
@@ -40,6 +42,7 @@ function startActivation(contentWindow: window) {
42 window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
43 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
44 window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
45 + window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
46 }
47
48 finishActivation(contentWindow);
packages/react-devtools-inline/src/frontend.js
+2
@@ -9,6 +9,7 @@ import {
9 getAppendComponentStack,
10 getBreakOnConsoleErrors,
11 getSavedComponentFilters,
12 + getShowInlineWarningsAndErrors,
13 } from 'react-devtools-shared/src/utils';
14 import {
15 MESSAGE_TYPE_GET_SAVED_PREFERENCES,
@@ -41,6 +42,7 @@ export function initialize(
42 appendComponentStack: getAppendComponentStack(),
43 breakOnConsoleErrors: getBreakOnConsoleErrors(),
44 componentFilters: getSavedComponentFilters(),
45 + showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
46 },
47 '*',
48 );
packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap
+4 -4
@@ -12,19 +12,19 @@ Array [
12 Object {
13 "displayName": "Grandparent",
14 "hocDisplayNames": null,
15 - "id": 7,
15 + "id": 2,
16 "type": 5,
17 },
18 Object {
19 "displayName": "Parent",
20 "hocDisplayNames": null,
21 - "id": 9,
21 + "id": 3,
22 "type": 5,
23 },
24 Object {
25 "displayName": "Child",
26 "hocDisplayNames": null,
27 - "id": 8,
27 + "id": 4,
28 "type": 5,
29 },
30 ]
@@ -115,7 +115,7 @@ Array [
115 Object {
116 "displayName": "Grandparent",
117 "hocDisplayNames": null,
118 - "id": 5,
118 + "id": 2,
119 "type": 5,
120 },
121 ]
packages/react-devtools-shared/src/__tests__/__snapshots__/storeComponentFilters-test.js.snap deleted
-140
@@ -1,140 +0,0 @@
1 -// Jest Snapshot v1, https://goo.gl/fbAQLP
2 -
3 -exports[`Store component filters should filter HOCs: 1: mount 1`] = `
4 -[root]
5 - ▾ <Component> [Bar][Foo]
6 - ▾ <Component> [Foo]
7 - ▾ <Component>
8 - <div>
9 -`;
10 -
11 -exports[`Store component filters should filter HOCs: 2: hide all HOCs 1`] = `
12 -[root]
13 - ▾ <Component>
14 - <div>
15 -`;
16 -
17 -exports[`Store component filters should filter HOCs: 3: disable HOC filter 1`] = `
18 -[root]
19 - ▾ <Component> [Bar][Foo]
20 - ▾ <Component> [Foo]
21 - ▾ <Component>
22 - <div>
23 -`;
24 -
25 -exports[`Store component filters should filter by display name: 1: mount 1`] = `
26 -[root]
27 - ▾ <Foo>
28 - <Text>
29 - ▾ <Bar>
30 - <Text>
31 - ▾ <Baz>
32 - <Text>
33 -`;
34 -
35 -exports[`Store component filters should filter by display name: 2: filter "Foo" 1`] = `
36 -[root]
37 - <Text>
38 - ▾ <Bar>
39 - <Text>
40 - ▾ <Baz>
41 - <Text>
42 -`;
43 -
44 -exports[`Store component filters should filter by display name: 3: filter "Ba" 1`] = `
45 -[root]
46 - ▾ <Foo>
47 - <Text>
48 - <Text>
49 - <Text>
50 -`;
51 -
52 -exports[`Store component filters should filter by display name: 4: filter "B.z" 1`] = `
53 -[root]
54 - ▾ <Foo>
55 - <Text>
56 - ▾ <Bar>
57 - <Text>
58 - <Text>
59 -`;
60 -
61 -exports[`Store component filters should filter by path: 1: mount 1`] = `
62 -[root]
63 - ▾ <Component>
64 - <div>
65 -`;
66 -
67 -exports[`Store component filters should filter by path: 2: hide all components declared within this test filed 1`] = `[root]`;
68 -
69 -exports[`Store component filters should filter by path: 3: hide components in a made up fake path 1`] = `
70 -[root]
71 - ▾ <Component>
72 - <div>
73 -`;
74 -
75 -exports[`Store component filters should ignore invalid ElementTypeRoot filter: 1: mount 1`] = `
76 -[root]
77 - ▾ <Root>
78 - <div>
79 -`;
80 -
81 -exports[`Store component filters should ignore invalid ElementTypeRoot filter: 2: add invalid filter 1`] = `
82 -[root]
83 - ▾ <Root>
84 - <div>
85 -`;
86 -
87 -exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 1: suspended 1`] = `
88 -[root]
89 - ▾ <Wrapper>
90 - ▾ <Loading>
91 - <div>
92 -`;
93 -
94 -exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 2: resolved 1`] = `
95 -[root]
96 - ▾ <Wrapper>
97 - <Component>
98 -`;
99 -
100 -exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 3: suspended 1`] = `
101 -[root]
102 - ▾ <Wrapper>
103 - ▾ <Loading>
104 - <div>
105 -`;
106 -
107 -exports[`Store component filters should support filtering by element type: 1: mount 1`] = `
108 -[root]
109 - ▾ <Root>
110 - ▾ <div>
111 - ▾ <Component>
112 - <div>
113 -`;
114 -
115 -exports[`Store component filters should support filtering by element type: 2: hide host components 1`] = `
116 -[root]
117 - ▾ <Root>
118 - <Component>
119 -`;
120 -
121 -exports[`Store component filters should support filtering by element type: 3: hide class components 1`] = `
122 -[root]
123 - ▾ <div>
124 - ▾ <Component>
125 - <div>
126 -`;
127 -
128 -exports[`Store component filters should support filtering by element type: 4: hide class and function components 1`] = `
129 -[root]
130 - ▾ <div>
131 - <div>
132 -`;
133 -
134 -exports[`Store component filters should support filtering by element type: 5: disable all filters 1`] = `
135 -[root]
136 - ▾ <Root>
137 - ▾ <div>
138 - ▾ <Component>
139 - <div>
140 -`;
packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap deleted
-1293
@@ -1,1293 +0,0 @@
1 -// Jest Snapshot v1, https://goo.gl/fbAQLP
2 -
3 -exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 0: mount 1`] = `
4 -[root]
5 - ▾ <Parent>
6 - ▾ <Suspense>
7 - ▾ <Child>
8 - ▾ <Suspense>
9 - <Grandchild>
10 -`;
11 -
12 -exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 1: initial state 1`] = `
13 -Object {
14 - "inspectedElementID": null,
15 - "numElements": 5,
16 - "ownerFlatTree": null,
17 - "ownerID": null,
18 - "ownerSubtreeLeafElementID": null,
19 - "searchIndex": null,
20 - "searchResults": Array [],
21 - "searchText": "",
22 - "selectedElementID": null,
23 - "selectedElementIndex": null,
24 -}
25 -`;
26 -
27 -exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 2: child owners tree 1`] = `
28 -Object {
29 - "inspectedElementID": 4,
30 - "numElements": 3,
31 - "ownerFlatTree": Array [
32 - Object {
33 - "children": Array [
34 - 5,
35 - ],
36 - "depth": 0,
37 - "displayName": "Child",
38 - "hocDisplayNames": null,
39 - "id": 4,
40 - "isCollapsed": false,
41 - "key": null,
42 - "ownerID": 2,
43 - "parentID": 3,
44 - "type": 5,
45 - "weight": 3,
46 - },
47 - Object {
48 - "children": Array [
49 - 6,
50 - ],
51 - "depth": 1,
52 - "displayName": "Suspense",
53 - "hocDisplayNames": null,
54 - "id": 5,
55 - "isCollapsed": false,
56 - "key": null,
57 - "ownerID": 4,
58 - "parentID": 4,
59 - "type": 12,
60 - "weight": 2,
61 - },
62 - Object {
63 - "children": Array [],
64 - "depth": 2,
65 - "displayName": "Grandchild",
66 - "hocDisplayNames": null,
67 - "id": 6,
68 - "isCollapsed": false,
69 - "key": null,
70 - "ownerID": 4,
71 - "parentID": 5,
72 - "type": 5,
73 - "weight": 1,
74 - },
75 - ],
76 - "ownerID": 4,
77 - "ownerSubtreeLeafElementID": null,
78 - "searchIndex": null,
79 - "searchResults": Array [],
80 - "searchText": "",
81 - "selectedElementID": 4,
82 - "selectedElementIndex": 0,
83 -}
84 -`;
85 -
86 -exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 3: child owners tree 1`] = `
87 -Object {
88 - "inspectedElementID": 5,
89 - "numElements": 3,
90 - "ownerFlatTree": Array [
91 - Object {
92 - "children": Array [
93 - 5,
94 - ],
95 - "depth": 0,
96 - "displayName": "Child",
97 - "hocDisplayNames": null,
98 - "id": 4,
99 - "isCollapsed": false,
100 - "key": null,
101 - "ownerID": 2,
102 - "parentID": 3,
103 - "type": 5,
104 - "weight": 3,
105 - },
106 - Object {
107 - "children": Array [
108 - 6,
109 - ],
110 - "depth": 1,
111 - "displayName": "Suspense",
112 - "hocDisplayNames": null,
113 - "id": 5,
114 - "isCollapsed": false,
115 - "key": null,
116 - "ownerID": 4,
117 - "parentID": 4,
118 - "type": 12,
119 - "weight": 2,
120 - },
121 - Object {
122 - "children": Array [],
123 - "depth": 2,
124 - "displayName": "Grandchild",
125 - "hocDisplayNames": null,
126 - "id": 6,
127 - "isCollapsed": false,
128 - "key": null,
129 - "ownerID": 4,
130 - "parentID": 5,
131 - "type": 5,
132 - "weight": 1,
133 - },
134 - ],
135 - "ownerID": 4,
136 - "ownerSubtreeLeafElementID": null,
137 - "searchIndex": null,
138 - "searchResults": Array [],
139 - "searchText": "",
140 - "selectedElementID": 5,
141 - "selectedElementIndex": 1,
142 -}
143 -`;
144 -
145 -exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 4: main tree 1`] = `
146 -Object {
147 - "inspectedElementID": 5,
148 - "numElements": 5,
149 - "ownerFlatTree": null,
150 - "ownerID": null,
151 - "ownerSubtreeLeafElementID": null,
152 - "searchIndex": null,
153 - "searchResults": Array [],
154 - "searchText": "",
155 - "selectedElementID": 5,
156 - "selectedElementIndex": 1,
157 -}
158 -`;
159 -
160 -exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 0: mount 1`] = `
161 -[root]
162 - ▾ <Parent>
163 - <Child>
164 -`;
165 -
166 -exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 1: initial state 1`] = `
167 -Object {
168 - "inspectedElementID": null,
169 - "numElements": 2,
170 - "ownerFlatTree": null,
171 - "ownerID": null,
172 - "ownerSubtreeLeafElementID": null,
173 - "searchIndex": null,
174 - "searchResults": Array [],
175 - "searchText": "",
176 - "selectedElementID": null,
177 - "selectedElementIndex": null,
178 -}
179 -`;
180 -
181 -exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 2: child owners tree 1`] = `
182 -Object {
183 - "inspectedElementID": 3,
184 - "numElements": 1,
185 - "ownerFlatTree": Array [
186 - Object {
187 - "children": Array [],
188 - "depth": 0,
189 - "displayName": "Child",
190 - "hocDisplayNames": null,
191 - "id": 3,
192 - "isCollapsed": false,
193 - "key": null,
194 - "ownerID": 0,
195 - "parentID": 2,
196 - "type": 5,
197 - "weight": 1,
198 - },
199 - ],
200 - "ownerID": 3,
201 - "ownerSubtreeLeafElementID": null,
202 - "searchIndex": null,
203 - "searchResults": Array [],
204 - "searchText": "",
205 - "selectedElementID": 3,
206 - "selectedElementIndex": 0,
207 -}
208 -`;
209 -
210 -exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 3: remove child 1`] = `
211 -Object {
212 - "inspectedElementID": null,
213 - "numElements": 1,
214 - "ownerFlatTree": null,
215 - "ownerID": null,
216 - "ownerSubtreeLeafElementID": null,
217 - "searchIndex": null,
218 - "searchResults": Array [],
219 - "searchText": "",
220 - "selectedElementID": null,
221 - "selectedElementIndex": 0,
222 -}
223 -`;
224 -
225 -exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 4: parent owners tree 1`] = `
226 -Object {
227 - "inspectedElementID": 2,
228 - "numElements": 1,
229 - "ownerFlatTree": Array [
230 - Object {
231 - "children": Array [],
232 - "depth": 0,
233 - "displayName": "Parent",
234 - "hocDisplayNames": null,
235 - "id": 2,
236 - "isCollapsed": false,
237 - "key": null,
238 - "ownerID": 0,
239 - "parentID": 1,
240 - "type": 5,
241 - "weight": 1,
242 - },
243 - ],
244 - "ownerID": 2,
245 - "ownerSubtreeLeafElementID": null,
246 - "searchIndex": null,
247 - "searchResults": Array [],
248 - "searchText": "",
249 - "selectedElementID": 2,
250 - "selectedElementIndex": 0,
251 -}
252 -`;
253 -
254 -exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 5: unmount root 1`] = `
255 -Object {
256 - "inspectedElementID": null,
257 - "numElements": 0,
258 - "ownerFlatTree": null,
259 - "ownerID": null,
260 - "ownerSubtreeLeafElementID": null,
261 - "searchIndex": null,
262 - "searchResults": Array [],
263 - "searchText": "",
264 - "selectedElementID": null,
265 - "selectedElementIndex": 0,
266 -}
267 -`;
268 -
269 -exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 0: mount 1`] = `
270 -[root]
271 - ▾ <Grandparent>
272 - ▾ <Parent>
273 - <Child key="0">
274 - <Child key="1">
275 -`;
276 -
277 -exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 1: initial state 1`] = `
278 -Object {
279 - "inspectedElementID": null,
280 - "numElements": 4,
281 - "ownerFlatTree": null,
282 - "ownerID": null,
283 - "ownerSubtreeLeafElementID": null,
284 - "searchIndex": null,
285 - "searchResults": Array [],
286 - "searchText": "",
287 - "selectedElementID": null,
288 - "selectedElementIndex": null,
289 -}
290 -`;
291 -
292 -exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 2: parent owners tree 1`] = `
293 -Object {
294 - "inspectedElementID": 3,
295 - "numElements": 3,
296 - "ownerFlatTree": Array [
297 - Object {
298 - "children": Array [
299 - 4,
300 - 5,
301 - ],
302 - "depth": 0,
303 - "displayName": "Parent",
304 - "hocDisplayNames": null,
305 - "id": 3,
306 - "isCollapsed": false,
307 - "key": null,
308 - "ownerID": 2,
309 - "parentID": 2,
310 - "type": 5,
311 - "weight": 3,
312 - },
313 - Object {
314 - "children": Array [],
315 - "depth": 1,
316 - "displayName": "Child",
317 - "hocDisplayNames": null,
318 - "id": 4,
319 - "isCollapsed": false,
320 - "key": "0",
321 - "ownerID": 3,
322 - "parentID": 3,
323 - "type": 5,
324 - "weight": 1,
325 - },
326 - Object {
327 - "children": Array [],
328 - "depth": 1,
329 - "displayName": "Child",
330 - "hocDisplayNames": null,
331 - "id": 5,
332 - "isCollapsed": false,
333 - "key": "1",
334 - "ownerID": 3,
335 - "parentID": 3,
336 - "type": 5,
337 - "weight": 1,
338 - },
339 - ],
340 - "ownerID": 3,
341 - "ownerSubtreeLeafElementID": null,
342 - "searchIndex": null,
343 - "searchResults": Array [],
344 - "searchText": "",
345 - "selectedElementID": 3,
346 - "selectedElementIndex": 0,
347 -}
348 -`;
349 -
350 -exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 3: remove second child 1`] = `
351 -Object {
352 - "inspectedElementID": 3,
353 - "numElements": 2,
354 - "ownerFlatTree": Array [
355 - Object {
356 - "children": Array [
357 - 4,
358 - ],
359 - "depth": 0,
360 - "displayName": "Parent",
361 - "hocDisplayNames": null,
362 - "id": 3,
363 - "isCollapsed": false,
364 - "key": null,
365 - "ownerID": 2,
366 - "parentID": 2,
367 - "type": 5,
368 - "weight": 2,
369 - },
370 - Object {
371 - "children": Array [],
372 - "depth": 1,
373 - "displayName": "Child",
374 - "hocDisplayNames": null,
375 - "id": 4,
376 - "isCollapsed": false,
377 - "key": "0",
378 - "ownerID": 3,
379 - "parentID": 3,
380 - "type": 5,
381 - "weight": 1,
382 - },
383 - ],
384 - "ownerID": 3,
385 - "ownerSubtreeLeafElementID": null,
386 - "searchIndex": null,
387 - "searchResults": Array [],
388 - "searchText": "",
389 - "selectedElementID": 3,
390 - "selectedElementIndex": 0,
391 -}
392 -`;
393 -
394 -exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 4: remove first child 1`] = `
395 -Object {
396 - "inspectedElementID": 3,
397 - "numElements": 1,
398 - "ownerFlatTree": Array [
399 - Object {
400 - "children": Array [],
401 - "depth": 0,
402 - "displayName": "Parent",
403 - "hocDisplayNames": null,
404 - "id": 3,
405 - "isCollapsed": false,
406 - "key": null,
407 - "ownerID": 2,
408 - "parentID": 2,
409 - "type": 5,
410 - "weight": 1,
411 - },
412 - ],
413 - "ownerID": 3,
414 - "ownerSubtreeLeafElementID": null,
415 - "searchIndex": null,
416 - "searchResults": Array [],
417 - "searchText": "",
418 - "selectedElementID": 3,
419 - "selectedElementIndex": 0,
420 -}
421 -`;
422 -
423 -exports[`TreeListContext owners state should support entering and existing the owners tree view: 0: mount 1`] = `
424 -[root]
425 - ▾ <Grandparent>
426 - ▾ <Parent>
427 - <Child>
428 - <Child>
429 -`;
430 -
431 -exports[`TreeListContext owners state should support entering and existing the owners tree view: 1: initial state 1`] = `
432 -Object {
433 - "inspectedElementID": null,
434 - "numElements": 4,
435 - "ownerFlatTree": null,
436 - "ownerID": null,
437 - "ownerSubtreeLeafElementID": null,
438 - "searchIndex": null,
439 - "searchResults": Array [],
440 - "searchText": "",
441 - "selectedElementID": null,
442 - "selectedElementIndex": null,
443 -}
444 -`;
445 -
446 -exports[`TreeListContext owners state should support entering and existing the owners tree view: 2: parent owners tree 1`] = `
447 -Object {
448 - "inspectedElementID": 3,
449 - "numElements": 3,
450 - "ownerFlatTree": Array [
451 - Object {
452 - "children": Array [
453 - 4,
454 - 5,
455 - ],
456 - "depth": 0,
457 - "displayName": "Parent",
458 - "hocDisplayNames": null,
459 - "id": 3,
460 - "isCollapsed": false,
461 - "key": null,
462 - "ownerID": 2,
463 - "parentID": 2,
464 - "type": 5,
465 - "weight": 3,
466 - },
467 - Object {
468 - "children": Array [],
469 - "depth": 1,
470 - "displayName": "Child",
471 - "hocDisplayNames": null,
472 - "id": 4,
473 - "isCollapsed": false,
474 - "key": null,
475 - "ownerID": 3,
476 - "parentID": 3,
477 - "type": 5,
478 - "weight": 1,
479 - },
480 - Object {
481 - "children": Array [],
482 - "depth": 1,
483 - "displayName": "Child",
484 - "hocDisplayNames": null,
485 - "id": 5,
486 - "isCollapsed": false,
487 - "key": null,
488 - "ownerID": 3,
489 - "parentID": 3,
490 - "type": 5,
491 - "weight": 1,
492 - },
493 - ],
494 - "ownerID": 3,
495 - "ownerSubtreeLeafElementID": null,
496 - "searchIndex": null,
497 - "searchResults": Array [],
498 - "searchText": "",
499 - "selectedElementID": 3,
500 - "selectedElementIndex": 0,
501 -}
502 -`;
503 -
504 -exports[`TreeListContext owners state should support entering and existing the owners tree view: 3: final state 1`] = `
505 -Object {
506 - "inspectedElementID": 3,
507 - "numElements": 4,
508 - "ownerFlatTree": null,
509 - "ownerID": null,
510 - "ownerSubtreeLeafElementID": null,
511 - "searchIndex": null,
512 - "searchResults": Array [],
513 - "searchText": "",
514 - "selectedElementID": 3,
515 - "selectedElementIndex": 1,
516 -}
517 -`;
518 -
519 -exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 0: mount 1`] = `
520 -[root]
521 - <Foo>
522 - <Bar>
523 -`;
524 -
525 -exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 1: initial state 1`] = `
526 -Object {
527 - "inspectedElementID": null,
528 - "numElements": 2,
529 - "ownerFlatTree": null,
530 - "ownerID": null,
531 - "ownerSubtreeLeafElementID": null,
532 - "searchIndex": null,
533 - "searchResults": Array [],
534 - "searchText": "",
535 - "selectedElementID": null,
536 - "selectedElementIndex": null,
537 -}
538 -`;
539 -
540 -exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 2: search for "ba" 1`] = `
541 -Object {
542 - "inspectedElementID": 3,
543 - "numElements": 2,
544 - "ownerFlatTree": null,
545 - "ownerID": null,
546 - "ownerSubtreeLeafElementID": null,
547 - "searchIndex": 0,
548 - "searchResults": Array [
549 - 3,
550 - ],
551 - "searchText": "ba",
552 - "selectedElementID": 3,
553 - "selectedElementIndex": 1,
554 -}
555 -`;
556 -
557 -exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 3: mount Baz 1`] = `
558 -Object {
559 - "inspectedElementID": 3,
560 - "numElements": 3,
561 - "ownerFlatTree": null,
562 - "ownerID": null,
563 - "ownerSubtreeLeafElementID": null,
564 - "searchIndex": 0,
565 - "searchResults": Array [
566 - 3,
567 - 4,
568 - ],
569 - "searchText": "ba",
570 - "selectedElementID": 3,
571 - "selectedElementIndex": 1,
572 -}
573 -`;
574 -
575 -exports[`TreeListContext search state should find elements matching search text: 0: mount 1`] = `
576 -[root]
577 - <Foo>
578 - <Bar>
579 - <Baz>
580 - <Qux> [withHOC]
581 -`;
582 -
583 -exports[`TreeListContext search state should find elements matching search text: 1: initial state 1`] = `
584 -Object {
585 - "inspectedElementID": null,
586 - "numElements": 4,
587 - "ownerFlatTree": null,
588 - "ownerID": null,
589 - "ownerSubtreeLeafElementID": null,
590 - "searchIndex": null,
591 - "searchResults": Array [],
592 - "searchText": "",
593 - "selectedElementID": null,
594 - "selectedElementIndex": null,
595 -}
596 -`;
597 -
598 -exports[`TreeListContext search state should find elements matching search text: 2: search for "ba" 1`] = `
599 -Object {
600 - "inspectedElementID": 3,
601 - "numElements": 4,
602 - "ownerFlatTree": null,
603 - "ownerID": null,
604 - "ownerSubtreeLeafElementID": null,
605 - "searchIndex": 0,
606 - "searchResults": Array [
607 - 3,
608 - 4,
609 - ],
610 - "searchText": "ba",
611 - "selectedElementID": 3,
612 - "selectedElementIndex": 1,
613 -}
614 -`;
615 -
616 -exports[`TreeListContext search state should find elements matching search text: 3: search for "f" 1`] = `
617 -Object {
618 - "inspectedElementID": 2,
619 - "numElements": 4,
620 - "ownerFlatTree": null,
621 - "ownerID": null,
622 - "ownerSubtreeLeafElementID": null,
623 - "searchIndex": 0,
624 - "searchResults": Array [
625 - 2,
626 - ],
627 - "searchText": "f",
628 - "selectedElementID": 2,
629 - "selectedElementIndex": 0,
630 -}
631 -`;
632 -
633 -exports[`TreeListContext search state should find elements matching search text: 4: search for "y" 1`] = `
634 -Object {
635 - "inspectedElementID": 2,
636 - "numElements": 4,
637 - "ownerFlatTree": null,
638 - "ownerID": null,
639 - "ownerSubtreeLeafElementID": null,
640 - "searchIndex": null,
641 - "searchResults": Array [],
642 - "searchText": "y",
643 - "selectedElementID": 2,
644 - "selectedElementIndex": 0,
645 -}
646 -`;
647 -
648 -exports[`TreeListContext search state should find elements matching search text: 5: search for "w" 1`] = `
649 -Object {
650 - "inspectedElementID": 5,
651 - "numElements": 4,
652 - "ownerFlatTree": null,
653 - "ownerID": null,
654 - "ownerSubtreeLeafElementID": null,
655 - "searchIndex": 0,
656 - "searchResults": Array [
657 - 5,
658 - ],
659 - "searchText": "w",
660 - "selectedElementID": 5,
661 - "selectedElementIndex": 3,
662 -}
663 -`;
664 -
665 -exports[`TreeListContext search state should remove unmounted elements from the search results set: 0: mount 1`] = `
666 -[root]
667 - <Foo>
668 - <Bar>
669 - <Baz>
670 -`;
671 -
672 -exports[`TreeListContext search state should remove unmounted elements from the search results set: 1: initial state 1`] = `
673 -Object {
674 - "inspectedElementID": null,
675 - "numElements": 3,
676 - "ownerFlatTree": null,
677 - "ownerID": null,
678 - "ownerSubtreeLeafElementID": null,
679 - "searchIndex": null,
680 - "searchResults": Array [],
681 - "searchText": "",
682 - "selectedElementID": null,
683 - "selectedElementIndex": null,
684 -}
685 -`;
686 -
687 -exports[`TreeListContext search state should remove unmounted elements from the search results set: 2: search for "ba" 1`] = `
688 -Object {
689 - "inspectedElementID": 3,
690 - "numElements": 3,
691 - "ownerFlatTree": null,
692 - "ownerID": null,
693 - "ownerSubtreeLeafElementID": null,
694 - "searchIndex": 0,
695 - "searchResults": Array [
696 - 3,
697 - 4,
698 - ],
699 - "searchText": "ba",
700 - "selectedElementID": 3,
701 - "selectedElementIndex": 1,
702 -}
703 -`;
704 -
705 -exports[`TreeListContext search state should remove unmounted elements from the search results set: 3: go to second result 1`] = `
706 -Object {
707 - "inspectedElementID": 4,
708 - "numElements": 3,
709 - "ownerFlatTree": null,
710 - "ownerID": null,
711 - "ownerSubtreeLeafElementID": null,
712 - "searchIndex": 1,
713 - "searchResults": Array [
714 - 3,
715 - 4,
716 - ],
717 - "searchText": "ba",
718 - "selectedElementID": 4,
719 - "selectedElementIndex": 2,
720 -}
721 -`;
722 -
723 -exports[`TreeListContext search state should remove unmounted elements from the search results set: 4: unmount Baz 1`] = `
724 -Object {
725 - "inspectedElementID": null,
726 - "numElements": 2,
727 - "ownerFlatTree": null,
728 - "ownerID": null,
729 - "ownerSubtreeLeafElementID": null,
730 - "searchIndex": 0,
731 - "searchResults": Array [
732 - 3,
733 - ],
734 - "searchText": "ba",
735 - "selectedElementID": null,
736 - "selectedElementIndex": null,
737 -}
738 -`;
739 -
740 -exports[`TreeListContext search state should select the next and previous items within the search results: 0: mount 1`] = `
741 -[root]
742 - <Foo>
743 - <Baz>
744 - <Bar>
745 - <Baz>
746 -`;
747 -
748 -exports[`TreeListContext search state should select the next and previous items within the search results: 1: initial state 1`] = `
749 -Object {
750 - "inspectedElementID": null,
751 - "numElements": 4,
752 - "ownerFlatTree": null,
753 - "ownerID": null,
754 - "ownerSubtreeLeafElementID": null,
755 - "searchIndex": null,
756 - "searchResults": Array [],
757 - "searchText": "",
758 - "selectedElementID": null,
759 - "selectedElementIndex": null,
760 -}
761 -`;
762 -
763 -exports[`TreeListContext search state should select the next and previous items within the search results: 2: search for "ba" 1`] = `
764 -Object {
765 - "inspectedElementID": 3,
766 - "numElements": 4,
767 - "ownerFlatTree": null,
768 - "ownerID": null,
769 - "ownerSubtreeLeafElementID": null,
770 - "searchIndex": 0,
771 - "searchResults": Array [
772 - 3,
773 - 4,
774 - 5,
775 - ],
776 - "searchText": "ba",
777 - "selectedElementID": 3,
778 - "selectedElementIndex": 1,
779 -}
780 -`;
781 -
782 -exports[`TreeListContext search state should select the next and previous items within the search results: 3: go to second result 1`] = `
783 -Object {
784 - "inspectedElementID": 4,
785 - "numElements": 4,
786 - "ownerFlatTree": null,
787 - "ownerID": null,
788 - "ownerSubtreeLeafElementID": null,
789 - "searchIndex": 1,
790 - "searchResults": Array [
791 - 3,
792 - 4,
793 - 5,
794 - ],
795 - "searchText": "ba",
796 - "selectedElementID": 4,
797 - "selectedElementIndex": 2,
798 -}
799 -`;
800 -
801 -exports[`TreeListContext search state should select the next and previous items within the search results: 4: go to third result 1`] = `
802 -Object {
803 - "inspectedElementID": 5,
804 - "numElements": 4,
805 - "ownerFlatTree": null,
806 - "ownerID": null,
807 - "ownerSubtreeLeafElementID": null,
808 - "searchIndex": 2,
809 - "searchResults": Array [
810 - 3,
811 - 4,
812 - 5,
813 - ],
814 - "searchText": "ba",
815 - "selectedElementID": 5,
816 - "selectedElementIndex": 3,
817 -}
818 -`;
819 -
820 -exports[`TreeListContext search state should select the next and previous items within the search results: 5: go to second result 1`] = `
821 -Object {
822 - "inspectedElementID": 4,
823 - "numElements": 4,
824 - "ownerFlatTree": null,
825 - "ownerID": null,
826 - "ownerSubtreeLeafElementID": null,
827 - "searchIndex": 1,
828 - "searchResults": Array [
829 - 3,
830 - 4,
831 - 5,
832 - ],
833 - "searchText": "ba",
834 - "selectedElementID": 4,
835 - "selectedElementIndex": 2,
836 -}
837 -`;
838 -
839 -exports[`TreeListContext search state should select the next and previous items within the search results: 6: go to first result 1`] = `
840 -Object {
841 - "inspectedElementID": 3,
842 - "numElements": 4,
843 - "ownerFlatTree": null,
844 - "ownerID": null,
845 - "ownerSubtreeLeafElementID": null,
846 - "searchIndex": 0,
847 - "searchResults": Array [
848 - 3,
849 - 4,
850 - 5,
851 - ],
852 - "searchText": "ba",
853 - "selectedElementID": 3,
854 - "selectedElementIndex": 1,
855 -}
856 -`;
857 -
858 -exports[`TreeListContext search state should select the next and previous items within the search results: 7: wrap to last result 1`] = `
859 -Object {
860 - "inspectedElementID": 5,
861 - "numElements": 4,
862 - "ownerFlatTree": null,
863 - "ownerID": null,
864 - "ownerSubtreeLeafElementID": null,
865 - "searchIndex": 2,
866 - "searchResults": Array [
867 - 3,
868 - 4,
869 - 5,
870 - ],
871 - "searchText": "ba",
872 - "selectedElementID": 5,
873 - "selectedElementIndex": 3,
874 -}
875 -`;
876 -
877 -exports[`TreeListContext search state should select the next and previous items within the search results: 8: wrap to first result 1`] = `
878 -Object {
879 - "inspectedElementID": 3,
880 - "numElements": 4,
881 - "ownerFlatTree": null,
882 - "ownerID": null,
883 - "ownerSubtreeLeafElementID": null,
884 - "searchIndex": 0,
885 - "searchResults": Array [
886 - 3,
887 - 4,
888 - 5,
889 - ],
890 - "searchText": "ba",
891 - "selectedElementID": 3,
892 - "selectedElementIndex": 1,
893 -}
894 -`;
895 -
896 -exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 0: mount 1`] = `
897 -[root]
898 - ▾ <Grandparent>
899 - ▾ <Parent>
900 - <Child>
901 - <Child>
902 -`;
903 -
904 -exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 1: initial state 1`] = `
905 -Object {
906 - "inspectedElementID": null,
907 - "numElements": 4,
908 - "ownerFlatTree": null,
909 - "ownerID": null,
910 - "ownerSubtreeLeafElementID": null,
911 - "searchIndex": null,
912 - "searchResults": Array [],
913 - "searchText": "",
914 - "selectedElementID": null,
915 - "selectedElementIndex": null,
916 -}
917 -`;
918 -
919 -exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 2: select second child 1`] = `
920 -Object {
921 - "inspectedElementID": 5,
922 - "numElements": 4,
923 - "ownerFlatTree": null,
924 - "ownerID": null,
925 - "ownerSubtreeLeafElementID": null,
926 - "searchIndex": null,
927 - "searchResults": Array [],
928 - "searchText": "",
929 - "selectedElementID": 5,
930 - "selectedElementIndex": 3,
931 -}
932 -`;
933 -
934 -exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 3: remove children (parent should now be selected) 1`] = `
935 -Object {
936 - "inspectedElementID": 3,
937 - "numElements": 2,
938 - "ownerFlatTree": null,
939 - "ownerID": null,
940 - "ownerSubtreeLeafElementID": null,
941 - "searchIndex": null,
942 - "searchResults": Array [],
943 - "searchText": "",
944 - "selectedElementID": 3,
945 - "selectedElementIndex": 1,
946 -}
947 -`;
948 -
949 -exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 4: unmount root (nothing should be selected) 1`] = `
950 -Object {
951 - "inspectedElementID": null,
952 - "numElements": 0,
953 - "ownerFlatTree": null,
954 - "ownerID": null,
955 - "ownerSubtreeLeafElementID": null,
956 - "searchIndex": null,
957 - "searchResults": Array [],
958 - "searchText": "",
959 - "selectedElementID": null,
960 - "selectedElementIndex": null,
961 -}
962 -`;
963 -
964 -exports[`TreeListContext tree state should navigate next/previous sibling and skip over children in between: 0: mount 1`] = `
965 -[root]
966 - ▾ <Grandparent>
967 - ▾ <Parent>
968 - <Child key="0">
969 - ▾ <Parent>
970 - <Child key="0">
971 - <Child key="1">
972 - <Child key="2">
973 - ▾ <Parent>
974 - <Child key="0">
975 - <Child key="1">
976 -`;
977 -
978 -exports[`TreeListContext tree state should navigate the owner hierarchy: 0: mount 1`] = `
979 -[root]
980 - ▾ <Grandparent>
981 - ▾ <Wrapper>
982 - ▾ <Parent>
983 - <Child key="0">
984 - ▾ <Wrapper>
985 - ▾ <Parent>
986 - <Child key="0">
987 - <Child key="1">
988 - <Child key="2">
989 - ▾ <Wrapper>
990 - ▾ <Parent>
991 - <Child key="0">
992 - <Child key="1">
993 -`;
994 -
995 -exports[`TreeListContext tree state should select child elements: 0: mount 1`] = `
996 -[root]
997 - ▾ <Grandparent>
998 - ▾ <Parent>
999 - <Child>
1000 - <Child>
1001 - ▾ <Parent>
1002 - <Child>
1003 - <Child>
1004 -`;
1005 -
1006 -exports[`TreeListContext tree state should select child elements: 1: initial state 1`] = `
1007 -Object {
1008 - "inspectedElementID": null,
1009 - "numElements": 7,
1010 - "ownerFlatTree": null,
1011 - "ownerID": null,
1012 - "ownerSubtreeLeafElementID": null,
1013 - "searchIndex": null,
1014 - "searchResults": Array [],
1015 - "searchText": "",
1016 - "selectedElementID": null,
1017 - "selectedElementIndex": null,
1018 -}
1019 -`;
1020 -
1021 -exports[`TreeListContext tree state should select child elements: 2: select first element 1`] = `
1022 -Object {
1023 - "inspectedElementID": 2,
1024 - "numElements": 7,
1025 - "ownerFlatTree": null,
1026 - "ownerID": null,
1027 - "ownerSubtreeLeafElementID": null,
1028 - "searchIndex": null,
1029 - "searchResults": Array [],
1030 - "searchText": "",
1031 - "selectedElementID": 2,
1032 - "selectedElementIndex": 0,
1033 -}
1034 -`;
1035 -
1036 -exports[`TreeListContext tree state should select child elements: 3: select Parent 1`] = `
1037 -Object {
1038 - "inspectedElementID": 3,
1039 - "numElements": 7,
1040 - "ownerFlatTree": null,
1041 - "ownerID": null,
1042 - "ownerSubtreeLeafElementID": null,
1043 - "searchIndex": null,
1044 - "searchResults": Array [],
1045 - "searchText": "",
1046 - "selectedElementID": 3,
1047 - "selectedElementIndex": 1,
1048 -}
1049 -`;
1050 -
1051 -exports[`TreeListContext tree state should select child elements: 4: select Child 1`] = `
1052 -Object {
1053 - "inspectedElementID": 4,
1054 - "numElements": 7,
1055 - "ownerFlatTree": null,
1056 - "ownerID": null,
1057 - "ownerSubtreeLeafElementID": null,
1058 - "searchIndex": null,
1059 - "searchResults": Array [],
1060 - "searchText": "",
1061 - "selectedElementID": 4,
1062 - "selectedElementIndex": 2,
1063 -}
1064 -`;
1065 -
1066 -exports[`TreeListContext tree state should select parent elements and then collapse: 0: mount 1`] = `
1067 -[root]
1068 - ▾ <Grandparent>
1069 - ▾ <Parent>
1070 - <Child>
1071 - <Child>
1072 - ▾ <Parent>
1073 - <Child>
1074 - <Child>
1075 -`;
1076 -
1077 -exports[`TreeListContext tree state should select parent elements and then collapse: 1: initial state 1`] = `
1078 -Object {
1079 - "inspectedElementID": null,
1080 - "numElements": 7,
1081 - "ownerFlatTree": null,
1082 - "ownerID": null,
1083 - "ownerSubtreeLeafElementID": null,
1084 - "searchIndex": null,
1085 - "searchResults": Array [],
1086 - "searchText": "",
1087 - "selectedElementID": null,
1088 - "selectedElementIndex": null,
1089 -}
1090 -`;
1091 -
1092 -exports[`TreeListContext tree state should select parent elements and then collapse: 2: select last child 1`] = `
1093 -Object {
1094 - "inspectedElementID": 8,
1095 - "numElements": 7,
1096 - "ownerFlatTree": null,
1097 - "ownerID": null,
1098 - "ownerSubtreeLeafElementID": null,
1099 - "searchIndex": null,
1100 - "searchResults": Array [],
1101 - "searchText": "",
1102 - "selectedElementID": 8,
1103 - "selectedElementIndex": 6,
1104 -}
1105 -`;
1106 -
1107 -exports[`TreeListContext tree state should select parent elements and then collapse: 3: select Parent 1`] = `
1108 -Object {
1109 - "inspectedElementID": 6,
1110 - "numElements": 7,
1111 - "ownerFlatTree": null,
1112 - "ownerID": null,
1113 - "ownerSubtreeLeafElementID": null,
1114 - "searchIndex": null,
1115 - "searchResults": Array [],
1116 - "searchText": "",
1117 - "selectedElementID": 6,
1118 - "selectedElementIndex": 4,
1119 -}
1120 -`;
1121 -
1122 -exports[`TreeListContext tree state should select parent elements and then collapse: 4: select Grandparent 1`] = `
1123 -Object {
1124 - "inspectedElementID": 2,
1125 - "numElements": 7,
1126 - "ownerFlatTree": null,
1127 - "ownerID": null,
1128 - "ownerSubtreeLeafElementID": null,
1129 - "searchIndex": null,
1130 - "searchResults": Array [],
1131 - "searchText": "",
1132 - "selectedElementID": 2,
1133 - "selectedElementIndex": 0,
1134 -}
1135 -`;
1136 -
1137 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 0: mount 1`] = `
1138 -[root]
1139 - ▾ <Grandparent>
1140 - ▾ <Parent>
1141 - <Child>
1142 - <Child>
1143 -`;
1144 -
1145 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 1: initial state 1`] = `
1146 -Object {
1147 - "inspectedElementID": null,
1148 - "numElements": 4,
1149 - "ownerFlatTree": null,
1150 - "ownerID": null,
1151 - "ownerSubtreeLeafElementID": null,
1152 - "searchIndex": null,
1153 - "searchResults": Array [],
1154 - "searchText": "",
1155 - "selectedElementID": null,
1156 - "selectedElementIndex": null,
1157 -}
1158 -`;
1159 -
1160 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 2: select first element 1`] = `
1161 -Object {
1162 - "inspectedElementID": 2,
1163 - "numElements": 4,
1164 - "ownerFlatTree": null,
1165 - "ownerID": null,
1166 - "ownerSubtreeLeafElementID": null,
1167 - "searchIndex": null,
1168 - "searchResults": Array [],
1169 - "searchText": "",
1170 - "selectedElementID": 2,
1171 - "selectedElementIndex": 0,
1172 -}
1173 -`;
1174 -
1175 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 3: select element after (0) 1`] = `
1176 -Object {
1177 - "inspectedElementID": 3,
1178 - "numElements": 4,
1179 - "ownerFlatTree": null,
1180 - "ownerID": null,
1181 - "ownerSubtreeLeafElementID": null,
1182 - "searchIndex": null,
1183 - "searchResults": Array [],
1184 - "searchText": "",
1185 - "selectedElementID": 3,
1186 - "selectedElementIndex": 1,
1187 -}
1188 -`;
1189 -
1190 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 3: select element after (1) 1`] = `
1191 -Object {
1192 - "inspectedElementID": 4,
1193 - "numElements": 4,
1194 - "ownerFlatTree": null,
1195 - "ownerID": null,
1196 - "ownerSubtreeLeafElementID": null,
1197 - "searchIndex": null,
1198 - "searchResults": Array [],
1199 - "searchText": "",
1200 - "selectedElementID": 4,
1201 - "selectedElementIndex": 2,
1202 -}
1203 -`;
1204 -
1205 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 3: select element after (2) 1`] = `
1206 -Object {
1207 - "inspectedElementID": 5,
1208 - "numElements": 4,
1209 - "ownerFlatTree": null,
1210 - "ownerID": null,
1211 - "ownerSubtreeLeafElementID": null,
1212 - "searchIndex": null,
1213 - "searchResults": Array [],
1214 - "searchText": "",
1215 - "selectedElementID": 5,
1216 - "selectedElementIndex": 3,
1217 -}
1218 -`;
1219 -
1220 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 4: select element before (1) 1`] = `
1221 -Object {
1222 - "inspectedElementID": 2,
1223 - "numElements": 4,
1224 - "ownerFlatTree": null,
1225 - "ownerID": null,
1226 - "ownerSubtreeLeafElementID": null,
1227 - "searchIndex": null,
1228 - "searchResults": Array [],
1229 - "searchText": "",
1230 - "selectedElementID": 2,
1231 - "selectedElementIndex": 0,
1232 -}
1233 -`;
1234 -
1235 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 4: select element before (2) 1`] = `
1236 -Object {
1237 - "inspectedElementID": 3,
1238 - "numElements": 4,
1239 - "ownerFlatTree": null,
1240 - "ownerID": null,
1241 - "ownerSubtreeLeafElementID": null,
1242 - "searchIndex": null,
1243 - "searchResults": Array [],
1244 - "searchText": "",
1245 - "selectedElementID": 3,
1246 - "selectedElementIndex": 1,
1247 -}
1248 -`;
1249 -
1250 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 4: select element before (3) 1`] = `
1251 -Object {
1252 - "inspectedElementID": 4,
1253 - "numElements": 4,
1254 - "ownerFlatTree": null,
1255 - "ownerID": null,
1256 - "ownerSubtreeLeafElementID": null,
1257 - "searchIndex": null,
1258 - "searchResults": Array [],
1259 - "searchText": "",
1260 - "selectedElementID": 4,
1261 - "selectedElementIndex": 2,
1262 -}
1263 -`;
1264 -
1265 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 5: select previous wraps around to last 1`] = `
1266 -Object {
1267 - "inspectedElementID": 5,
1268 - "numElements": 4,
1269 - "ownerFlatTree": null,
1270 - "ownerID": null,
1271 - "ownerSubtreeLeafElementID": null,
1272 - "searchIndex": null,
1273 - "searchResults": Array [],
1274 - "searchText": "",
1275 - "selectedElementID": 5,
1276 - "selectedElementIndex": 3,
1277 -}
1278 -`;
1279 -
1280 -exports[`TreeListContext tree state should select the next and previous elements in the tree: 6: select next wraps around to first 1`] = `
1281 -Object {
1282 - "inspectedElementID": 2,
1283 - "numElements": 4,
1284 - "ownerFlatTree": null,
1285 - "ownerID": null,
1286 - "ownerSubtreeLeafElementID": null,
1287 - "searchIndex": null,
1288 - "searchResults": Array [],
1289 - "searchText": "",
1290 - "selectedElementID": 2,
1291 - "selectedElementIndex": 0,
1292 -}
1293 -`;
packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+442 -21
@@ -15,6 +15,7 @@ import type {
15 } from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
16 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
17 import type Store from 'react-devtools-shared/src/devtools/store';
18 +import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';
19
20 describe('InspectedElementContext', () => {
21 let React;
@@ -66,6 +67,10 @@ describe('InspectedElementContext', () => {
67 .TreeContextController;
68 });
69
70 + afterEach(() => {
71 + jest.restoreAllMocks();
72 + });
73 +
74 const Contexts = ({
75 children,
76 defaultSelectedElementID = null,
@@ -386,10 +391,10 @@ describe('InspectedElementContext', () => {
391 it('should temporarily disable console logging when re-running a component to inspect its hooks', async done => {
392 let targetRenderCount = 0;
393
389 - const errorSpy = ((console: any).error = jest.fn());
390 - const infoSpy = ((console: any).info = jest.fn());
391 - const logSpy = ((console: any).log = jest.fn());
392 - const warnSpy = ((console: any).warn = jest.fn());
394 + jest.spyOn(console, 'error').mockImplementation(() => {});
395 + jest.spyOn(console, 'info').mockImplementation(() => {});
396 + jest.spyOn(console, 'log').mockImplementation(() => {});
397 + jest.spyOn(console, 'warn').mockImplementation(() => {});
398
399 const Target = React.memo(props => {
400 targetRenderCount++;
@@ -407,14 +412,14 @@ describe('InspectedElementContext', () => {
412 );
413
414 expect(targetRenderCount).toBe(1);
410 - expect(errorSpy).toHaveBeenCalledTimes(1);
411 - expect(errorSpy).toHaveBeenCalledWith('error');
412 - expect(infoSpy).toHaveBeenCalledTimes(1);
413 - expect(infoSpy).toHaveBeenCalledWith('info');
414 - expect(logSpy).toHaveBeenCalledTimes(1);
415 - expect(logSpy).toHaveBeenCalledWith('log');
416 - expect(warnSpy).toHaveBeenCalledTimes(1);
417 - expect(warnSpy).toHaveBeenCalledWith('warn');
415 + expect(console.error).toHaveBeenCalledTimes(1);
416 + expect(console.error).toHaveBeenCalledWith('error');
417 + expect(console.info).toHaveBeenCalledTimes(1);
418 + expect(console.info).toHaveBeenCalledWith('info');
419 + expect(console.log).toHaveBeenCalledTimes(1);
420 + expect(console.log).toHaveBeenCalledWith('log');
421 + expect(console.warn).toHaveBeenCalledTimes(1);
422 + expect(console.warn).toHaveBeenCalledWith('warn');
423
424 const id = ((store.getElementIDAtIndex(0): any): number);
425
@@ -442,10 +447,10 @@ describe('InspectedElementContext', () => {
447
448 expect(inspectedElement).not.toBe(null);
449 expect(targetRenderCount).toBe(2);
445 - expect(errorSpy).toHaveBeenCalledTimes(1);
446 - expect(infoSpy).toHaveBeenCalledTimes(1);
447 - expect(logSpy).toHaveBeenCalledTimes(1);
448 - expect(warnSpy).toHaveBeenCalledTimes(1);
450 + expect(console.error).toHaveBeenCalledTimes(1);
451 + expect(console.info).toHaveBeenCalledTimes(1);
452 + expect(console.log).toHaveBeenCalledTimes(1);
453 + expect(console.warn).toHaveBeenCalledTimes(1);
454
455 done();
456 });
@@ -1569,21 +1574,20 @@ describe('InspectedElementContext', () => {
1574 );
1575 expect(storeAsGlobal).not.toBeNull();
1576
1572 - const logSpy = jest.fn();
1573 - spyOn(console, 'log').and.callFake(logSpy);
1577 + jest.spyOn(console, 'log').mockImplementation(() => {});
1578
1579 // Should store the whole value (not just the hydrated parts)
1580 storeAsGlobal(id, ['props', 'nestedObject']);
1581 jest.runOnlyPendingTimers();
1578 - expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
1582 + expect(console.log).toHaveBeenCalledWith('$reactTemp1');
1583 expect(global.$reactTemp1).toBe(nestedObject);
1584
1581 - logSpy.mockReset();
1585 + console.log.mockReset();
1586
1587 // Should store the nested property specified (not just the outer value)
1588 storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
1589 jest.runOnlyPendingTimers();
1586 - expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
1590 + expect(console.log).toHaveBeenCalledWith('$reactTemp2');
1591 expect(global.$reactTemp2).toBe(nestedObject.a.b);
1592
1593 done();
@@ -1805,4 +1809,421 @@ describe('InspectedElementContext', () => {
1809
1810 done();
1811 });
1812 +
1813 + describe('inline errors and warnings', () => {
1814 + // Some actions require the Fiber id.
1815 + // In those instances you might want to make assertions based on the ID instead of the index.
1816 + function getErrorsAndWarningsForElement(id: number) {
1817 + const index = ((store.getIndexOfElementID(id): any): number);
1818 + return getErrorsAndWarningsForElementAtIndex(index);
1819 + }
1820 +
1821 + async function getErrorsAndWarningsForElementAtIndex(index) {
1822 + const id = ((store.getElementIDAtIndex(index): any): number);
1823 +
1824 + let errors = null;
1825 + let warnings = null;
1826 +
1827 + function Suspender({target}) {
1828 + const {getInspectedElement} = React.useContext(InspectedElementContext);
1829 + const inspectedElement = getInspectedElement(id);
1830 + errors = inspectedElement.errors;
1831 + warnings = inspectedElement.warnings;
1832 + return null;
1833 + }
1834 +
1835 + let root;
1836 + await utils.actAsync(() => {
1837 + root = TestRenderer.create(
1838 + <Contexts
1839 + defaultSelectedElementID={id}
1840 + defaultSelectedElementIndex={index}>
1841 + <React.Suspense fallback={null}>
1842 + <Suspender target={id} />
1843 + </React.Suspense>
1844 + </Contexts>,
1845 + );
1846 + }, false);
1847 + await utils.actAsync(() => {
1848 + root.unmount();
1849 + }, false);
1850 +
1851 + return {errors, warnings};
1852 + }
1853 +
1854 + it('during render get recorded', async () => {
1855 + const Example = () => {
1856 + console.error('test-only: render error');
1857 + console.warn('test-only: render warning');
1858 + return null;
1859 + };
1860 +
1861 + const container = document.createElement('div');
1862 +
1863 + await withErrorsOrWarningsIgnored(['test-only: '], async () => {
1864 + await utils.actAsync(() =>
1865 + ReactDOM.render(<Example repeatWarningCount={1} />, container),
1866 + );
1867 + });
1868 +
1869 + const data = await getErrorsAndWarningsForElementAtIndex(0);
1870 + expect(data).toMatchInlineSnapshot(`
1871 + Object {
1872 + "errors": Array [
1873 + Array [
1874 + "test-only: render error",
1875 + 1,
1876 + ],
1877 + ],
1878 + "warnings": Array [
1879 + Array [
1880 + "test-only: render warning",
1881 + 1,
1882 + ],
1883 + ],
1884 + }
1885 + `);
1886 + });
1887 +
1888 + it('during render get deduped', async () => {
1889 + const Example = () => {
1890 + console.error('test-only: render error');
1891 + console.error('test-only: render error');
1892 + console.warn('test-only: render warning');
1893 + console.warn('test-only: render warning');
1894 + console.warn('test-only: render warning');
1895 + return null;
1896 + };
1897 +
1898 + const container = document.createElement('div');
1899 + await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
1900 + await utils.actAsync(() =>
1901 + ReactDOM.render(<Example repeatWarningCount={1} />, container),
1902 + );
1903 + });
1904 + const data = await getErrorsAndWarningsForElementAtIndex(0);
1905 + expect(data).toMatchInlineSnapshot(`
1906 + Object {
1907 + "errors": Array [
1908 + Array [
1909 + "test-only: render error",
1910 + 2,
1911 + ],
1912 + ],
1913 + "warnings": Array [
1914 + Array [
1915 + "test-only: render warning",
1916 + 3,
1917 + ],
1918 + ],
1919 + }
1920 + `);
1921 + });
1922 +
1923 + it('during layout (mount) get recorded', async () => {
1924 + const Example = () => {
1925 + // Note we only test mount because once the component unmounts,
1926 + // it is no longer in the store and warnings are ignored.
1927 + React.useLayoutEffect(() => {
1928 + console.error('test-only: useLayoutEffect error');
1929 + console.warn('test-only: useLayoutEffect warning');
1930 + }, []);
1931 + return null;
1932 + };
1933 +
1934 + const container = document.createElement('div');
1935 + await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
1936 + await utils.actAsync(() =>
1937 + ReactDOM.render(<Example repeatWarningCount={1} />, container),
1938 + );
1939 + });
1940 +
1941 + const data = await getErrorsAndWarningsForElementAtIndex(0);
1942 + expect(data).toMatchInlineSnapshot(`
1943 + Object {
1944 + "errors": Array [
1945 + Array [
1946 + "test-only: useLayoutEffect error",
1947 + 1,
1948 + ],
1949 + ],
1950 + "warnings": Array [
1951 + Array [
1952 + "test-only: useLayoutEffect warning",
1953 + 1,
1954 + ],
1955 + ],
1956 + }
1957 + `);
1958 + });
1959 +
1960 + it('during passive (mount) get recorded', async () => {
1961 + const Example = () => {
1962 + // Note we only test mount because once the component unmounts,
1963 + // it is no longer in the store and warnings are ignored.
1964 + React.useEffect(() => {
1965 + console.error('test-only: useEffect error');
1966 + console.warn('test-only: useEffect warning');
1967 + }, []);
1968 + return null;
1969 + };
1970 +
1971 + const container = document.createElement('div');
1972 + await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
1973 + await utils.actAsync(() =>
1974 + ReactDOM.render(<Example repeatWarningCount={1} />, container),
1975 + );
1976 + });
1977 +
1978 + const data = await getErrorsAndWarningsForElementAtIndex(0);
1979 + expect(data).toMatchInlineSnapshot(`
1980 + Object {
1981 + "errors": Array [
1982 + Array [
1983 + "test-only: useEffect error",
1984 + 1,
1985 + ],
1986 + ],
1987 + "warnings": Array [
1988 + Array [
1989 + "test-only: useEffect warning",
1990 + 1,
1991 + ],
1992 + ],
1993 + }
1994 + `);
1995 + });
1996 +
1997 + it('from react get recorded without a component stack', async () => {
1998 + const Example = () => {
1999 + return [<div />];
2000 + };
2001 +
2002 + const container = document.createElement('div');
2003 + await utils.withErrorsOrWarningsIgnored(
2004 + ['Warning: Each child in a list should have a unique "key" prop.'],
2005 + async () => {
2006 + await utils.actAsync(() =>
2007 + ReactDOM.render(<Example repeatWarningCount={1} />, container),
2008 + );
2009 + },
2010 + );
2011 +
2012 + const data = await getErrorsAndWarningsForElementAtIndex(0);
2013 + expect(data).toMatchInlineSnapshot(`
2014 + Object {
2015 + "errors": Array [
2016 + Array [
2017 + "Warning: Each child in a list should have a unique \\"key\\" prop. See https://reactjs.org/link/warning-keys for more information.",
2018 + 1,
2019 + ],
2020 + ],
2021 + "warnings": Array [],
2022 + }
2023 + `);
2024 + });
2025 +
2026 + it('can be cleared for the whole app', async () => {
2027 + const Example = () => {
2028 + console.error('test-only: render error');
2029 + console.warn('test-only: render warning');
2030 + return null;
2031 + };
2032 +
2033 + const container = document.createElement('div');
2034 + await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
2035 + await utils.actAsync(() =>
2036 + ReactDOM.render(<Example repeatWarningCount={1} />, container),
2037 + );
2038 + });
2039 +
2040 + store.clearErrorsAndWarnings();
2041 + // Flush events to the renderer.
2042 + jest.runOnlyPendingTimers();
2043 +
2044 + const data = await getErrorsAndWarningsForElementAtIndex(0);
2045 + expect(data).toMatchInlineSnapshot(`
2046 + Object {
2047 + "errors": Array [],
2048 + "warnings": Array [],
2049 + }
2050 + `);
2051 + });
2052 +
2053 + it('can be cleared for a particular Fiber (only errors)', async () => {
2054 + const Example = ({id}) => {
2055 + console.error(`test-only: render error #${id}`);
2056 + console.warn(`test-only: render warning #${id}`);
2057 + return null;
2058 + };
2059 +
2060 + const container = document.createElement('div');
2061 + await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
2062 + await utils.actAsync(() =>
2063 + ReactDOM.render(
2064 + <React.Fragment>
2065 + <Example id={1} />
2066 + <Example id={2} />
2067 + </React.Fragment>,
2068 + container,
2069 + ),
2070 + );
2071 + });
2072 +
2073 + store.clearWarningsForElement(2);
2074 + // Flush events to the renderer.
2075 + jest.runOnlyPendingTimers();
2076 +
2077 + let data = [
2078 + await getErrorsAndWarningsForElement(1),
2079 + await getErrorsAndWarningsForElement(2),
2080 + ];
2081 + expect(data).toMatchInlineSnapshot(`
2082 + Array [
2083 + Object {
2084 + "errors": Array [
2085 + Array [
2086 + "test-only: render error #1",
2087 + 1,
2088 + ],
2089 + ],
2090 + "warnings": Array [
2091 + Array [
2092 + "test-only: render warning #1",
2093 + 1,
2094 + ],
2095 + ],
2096 + },
2097 + Object {
2098 + "errors": Array [
2099 + Array [
2100 + "test-only: render error #2",
2101 + 1,
2102 + ],
2103 + ],
2104 + "warnings": Array [],
2105 + },
2106 + ]
2107 + `);
2108 +
2109 + store.clearWarningsForElement(1);
2110 + // Flush events to the renderer.
2111 + jest.runOnlyPendingTimers();
2112 +
2113 + data = [
2114 + await getErrorsAndWarningsForElement(1),
2115 + await getErrorsAndWarningsForElement(2),
2116 + ];
2117 + expect(data).toMatchInlineSnapshot(`
2118 + Array [
2119 + Object {
2120 + "errors": Array [
2121 + Array [
2122 + "test-only: render error #1",
2123 + 1,
2124 + ],
2125 + ],
2126 + "warnings": Array [],
2127 + },
2128 + Object {
2129 + "errors": Array [
2130 + Array [
2131 + "test-only: render error #2",
2132 + 1,
2133 + ],
2134 + ],
2135 + "warnings": Array [],
2136 + },
2137 + ]
2138 + `);
2139 + });
2140 +
2141 + it('can be cleared for a particular Fiber (only warnings)', async () => {
2142 + const Example = ({id}) => {
2143 + console.error(`test-only: render error #${id}`);
2144 + console.warn(`test-only: render warning #${id}`);
2145 + return null;
2146 + };
2147 +
2148 + const container = document.createElement('div');
2149 + await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
2150 + await utils.actAsync(() =>
2151 + ReactDOM.render(
2152 + <React.Fragment>
2153 + <Example id={1} />
2154 + <Example id={2} />
2155 + </React.Fragment>,
2156 + container,
2157 + ),
2158 + );
2159 + });
2160 +
2161 + store.clearErrorsForElement(2);
2162 + // Flush events to the renderer.
2163 + jest.runOnlyPendingTimers();
2164 +
2165 + let data = [
2166 + await getErrorsAndWarningsForElement(1),
2167 + await getErrorsAndWarningsForElement(2),
2168 + ];
2169 + expect(data).toMatchInlineSnapshot(`
2170 + Array [
2171 + Object {
2172 + "errors": Array [
2173 + Array [
2174 + "test-only: render error #1",
2175 + 1,
2176 + ],
2177 + ],
2178 + "warnings": Array [
2179 + Array [
2180 + "test-only: render warning #1",
2181 + 1,
2182 + ],
2183 + ],
2184 + },
2185 + Object {
2186 + "errors": Array [],
2187 + "warnings": Array [
2188 + Array [
2189 + "test-only: render warning #2",
2190 + 1,
2191 + ],
2192 + ],
2193 + },
2194 + ]
2195 + `);
2196 +
2197 + store.clearErrorsForElement(1);
2198 + // Flush events to the renderer.
2199 + jest.runOnlyPendingTimers();
2200 +
2201 + data = [
2202 + await getErrorsAndWarningsForElement(1),
2203 + await getErrorsAndWarningsForElement(2),
2204 + ];
2205 + expect(data).toMatchInlineSnapshot(`
2206 + Array [
2207 + Object {
2208 + "errors": Array [],
2209 + "warnings": Array [
2210 + Array [
2211 + "test-only: render warning #1",
2212 + 1,
2213 + ],
2214 + ],
2215 + },
2216 + Object {
2217 + "errors": Array [],
2218 + "warnings": Array [
2219 + Array [
2220 + "test-only: render warning #2",
2221 + 1,
2222 + ],
2223 + ],
2224 + },
2225 + ]
2226 + `);
2227 + });
2228 + });
2229 });
packages/react-devtools-shared/src/__tests__/profilerStore-test.js
+15
@@ -120,4 +120,19 @@ describe('ProfilerStore', () => {
120 expect(data.commitData).toHaveLength(1);
121 expect(data.operations).toHaveLength(1);
122 });
123 +
124 + it('should throw if component filters are modified while profiling', () => {
125 + utils.act(() => store.profilerStore.startProfiling());
126 +
127 + expect(() => {
128 + utils.act(() => {
129 + const {
130 + ElementTypeHostComponent,
131 + } = require('react-devtools-shared/src/types');
132 + store.componentFilters = [
133 + utils.createElementTypeFilter(ElementTypeHostComponent),
134 + ];
135 + });
136 + }).toThrow('Cannot modify filter preferences while profiling');
137 + });
138 });
packages/react-devtools-shared/src/__tests__/setupTests.js
+31
@@ -33,11 +33,24 @@ env.beforeEach(() => {
33 const {
34 getDefaultComponentFilters,
35 saveComponentFilters,
36 + setShowInlineWarningsAndErrors,
37 } = require('react-devtools-shared/src/utils');
38
39 // Fake timers let us flush Bridge operations between setup and assertions.
40 jest.useFakeTimers();
41
42 + // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
43 + global._ignoredErrorOrWarningMessages = [];
44 + function shouldIgnoreConsoleErrorOrWarn(args) {
45 + const firstArg = args[0];
46 + if (typeof firstArg !== 'string') {
47 + return false;
48 + }
49 + return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
50 + return firstArg.indexOf(errorOrWarningMessage) !== -1;
51 + });
52 + }
53 +
54 const originalConsoleError = console.error;
55 // $FlowFixMe
56 console.error = (...args) => {
@@ -54,14 +67,32 @@ env.beforeEach(() => {
67 // DevTools intentionally wraps updates with acts from both DOM and test-renderer,
68 // since test updates are expected to impact both renderers.
69 return;
70 + } else if (shouldIgnoreConsoleErrorOrWarn(args)) {
71 + // Allows testing how DevTools behaves when it encounters console.error without cluttering the test output.
72 + // Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
73 + return;
74 }
75 originalConsoleError.apply(console, args);
76 };
77 + const originalConsoleWarn = console.warn;
78 + // $FlowFixMe
79 + console.warn = (...args) => {
80 + if (shouldIgnoreConsoleErrorOrWarn(args)) {
81 + // Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
82 + // Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
83 + return;
84 + }
85 + originalConsoleWarn.apply(console, args);
86 + };
87
88 // Initialize filters to a known good state.
89 saveComponentFilters(getDefaultComponentFilters());
90 global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters();
91
92 + // Also initialize inline warnings so that we can test them.
93 + setShowInlineWarningsAndErrors(true);
94 + global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
95 +
96 installHook(global);
97
98 const bridgeListeners = [];
packages/react-devtools-shared/src/__tests__/store-test.js
+314
@@ -14,6 +14,7 @@ describe('Store', () => {
14 let act;
15 let getRendererID;
16 let store;
17 + let withErrorsOrWarningsIgnored;
18
19 beforeEach(() => {
20 agent = global.agent;
@@ -25,6 +26,7 @@ describe('Store', () => {
26 const utils = require('./utils');
27 act = utils.act;
28 getRendererID = utils.getRendererID;
29 + withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
30 });
31
32 it('should not allow a root node to be collapsed', () => {
@@ -1008,4 +1010,316 @@ describe('Store', () => {
1010 done();
1011 });
1012 });
1013 +
1014 + describe('inline errors and warnings', () => {
1015 + it('during render are counted', () => {
1016 + function Example() {
1017 + console.error('test-only: render error');
1018 + console.warn('test-only: render warning');
1019 + return null;
1020 + }
1021 + const container = document.createElement('div');
1022 +
1023 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1024 + act(() => ReactDOM.render(<Example />, container));
1025 + });
1026 +
1027 + expect(store).toMatchInlineSnapshot(`
1028 + ✕ 1, ⚠ 1
1029 + [root]
1030 + <Example> ✕⚠
1031 + `);
1032 +
1033 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1034 + act(() => ReactDOM.render(<Example rerender={1} />, container));
1035 + });
1036 +
1037 + expect(store).toMatchInlineSnapshot(`
1038 + ✕ 2, ⚠ 2
1039 + [root]
1040 + <Example> ✕⚠
1041 + `);
1042 + });
1043 +
1044 + it('during layout get counted', () => {
1045 + function Example() {
1046 + React.useLayoutEffect(() => {
1047 + console.error('test-only: layout error');
1048 + console.warn('test-only: layout warning');
1049 + });
1050 + return null;
1051 + }
1052 + const container = document.createElement('div');
1053 +
1054 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1055 + act(() => ReactDOM.render(<Example />, container));
1056 + });
1057 +
1058 + expect(store).toMatchInlineSnapshot(`
1059 + ✕ 1, ⚠ 1
1060 + [root]
1061 + <Example> ✕⚠
1062 + `);
1063 +
1064 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1065 + act(() => ReactDOM.render(<Example rerender={1} />, container));
1066 + });
1067 +
1068 + expect(store).toMatchInlineSnapshot(`
1069 + ✕ 2, ⚠ 2
1070 + [root]
1071 + <Example> ✕⚠
1072 + `);
1073 + });
1074 +
1075 + // This is not great, but it seems safer than potentially flushing between commits.
1076 + // Our logic for determining how to handle e.g. suspended trees or error boundaries
1077 + // is built on the assumption that we're evaluating the results of a commit, not an in-progress render.
1078 + it('during passive get counted (but not until the next commit)', () => {
1079 + function Example() {
1080 + React.useEffect(() => {
1081 + console.error('test-only: passive error');
1082 + console.warn('test-only: passive warning');
1083 + });
1084 + return null;
1085 + }
1086 + const container = document.createElement('div');
1087 +
1088 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1089 + act(() => ReactDOM.render(<Example />, container));
1090 + });
1091 +
1092 + expect(store).toMatchInlineSnapshot(`
1093 + [root]
1094 + <Example>
1095 + `);
1096 +
1097 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1098 + act(() => ReactDOM.render(<Example rerender={1} />, container));
1099 + });
1100 +
1101 + expect(store).toMatchInlineSnapshot(`
1102 + ✕ 1, ⚠ 1
1103 + [root]
1104 + <Example> ✕⚠
1105 + `);
1106 +
1107 + act(() => ReactDOM.unmountComponentAtNode(container));
1108 + expect(store).toMatchInlineSnapshot(``);
1109 + });
1110 +
1111 + it('from react get counted', () => {
1112 + const container = document.createElement('div');
1113 + function Example() {
1114 + return [<Child />];
1115 + }
1116 + function Child() {
1117 + return null;
1118 + }
1119 +
1120 + withErrorsOrWarningsIgnored(
1121 + ['Warning: Each child in a list should have a unique "key" prop'],
1122 + () => {
1123 + act(() => ReactDOM.render(<Example />, container));
1124 + },
1125 + );
1126 +
1127 + expect(store).toMatchInlineSnapshot(`
1128 + ✕ 1, ⚠ 0
1129 + [root]
1130 + ▾ <Example> ✕
1131 + <Child>
1132 + `);
1133 + });
1134 +
1135 + it('can be cleared for the whole app', () => {
1136 + function Example() {
1137 + console.error('test-only: render error');
1138 + console.warn('test-only: render warning');
1139 + return null;
1140 + }
1141 + const container = document.createElement('div');
1142 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1143 + act(() =>
1144 + ReactDOM.render(
1145 + <React.Fragment>
1146 + <Example />
1147 + <Example />
1148 + </React.Fragment>,
1149 + container,
1150 + ),
1151 + );
1152 + });
1153 +
1154 + expect(store).toMatchInlineSnapshot(`
1155 + ✕ 2, ⚠ 2
1156 + [root]
1157 + <Example> ✕⚠
1158 + <Example> ✕⚠
1159 + `);
1160 +
1161 + store.clearErrorsAndWarnings();
1162 + // flush events to the renderer
1163 + jest.runAllTimers();
1164 +
1165 + expect(store).toMatchInlineSnapshot(`
1166 + [root]
1167 + <Example>
1168 + <Example>
1169 + `);
1170 + });
1171 +
1172 + it('can be cleared for particular Fiber (only warnings)', () => {
1173 + function Example() {
1174 + console.error('test-only: render error');
1175 + console.warn('test-only: render warning');
1176 + return null;
1177 + }
1178 + const container = document.createElement('div');
1179 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1180 + act(() =>
1181 + ReactDOM.render(
1182 + <React.Fragment>
1183 + <Example />
1184 + <Example />
1185 + </React.Fragment>,
1186 + container,
1187 + ),
1188 + );
1189 + });
1190 +
1191 + expect(store).toMatchInlineSnapshot(`
1192 + ✕ 2, ⚠ 2
1193 + [root]
1194 + <Example> ✕⚠
1195 + <Example> ✕⚠
1196 + `);
1197 +
1198 + store.clearWarningsForElement(2);
1199 + // Flush events to the renderer.
1200 + jest.runAllTimers();
1201 +
1202 + expect(store).toMatchInlineSnapshot(`
1203 + ✕ 2, ⚠ 1
1204 + [root]
1205 + <Example> ✕⚠
1206 + <Example> ✕
1207 + `);
1208 + });
1209 +
1210 + it('can be cleared for a particular Fiber (only errors)', () => {
1211 + function Example() {
1212 + console.error('test-only: render error');
1213 + console.warn('test-only: render warning');
1214 + return null;
1215 + }
1216 + const container = document.createElement('div');
1217 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1218 + act(() =>
1219 + ReactDOM.render(
1220 + <React.Fragment>
1221 + <Example />
1222 + <Example />
1223 + </React.Fragment>,
1224 + container,
1225 + ),
1226 + );
1227 + });
1228 +
1229 + expect(store).toMatchInlineSnapshot(`
1230 + ✕ 2, ⚠ 2
1231 + [root]
1232 + <Example> ✕⚠
1233 + <Example> ✕⚠
1234 + `);
1235 +
1236 + store.clearErrorsForElement(2);
1237 + // Flush events to the renderer.
1238 + jest.runAllTimers();
1239 +
1240 + expect(store).toMatchInlineSnapshot(`
1241 + ✕ 1, ⚠ 2
1242 + [root]
1243 + <Example> ✕⚠
1244 + <Example> ⚠
1245 + `);
1246 + });
1247 +
1248 + it('are updated when fibers are removed from the tree', () => {
1249 + function ComponentWithWarning() {
1250 + console.warn('test-only: render warning');
1251 + return null;
1252 + }
1253 + function ComponentWithError() {
1254 + console.error('test-only: render error');
1255 + return null;
1256 + }
1257 + function ComponentWithWarningAndError() {
1258 + console.error('test-only: render error');
1259 + console.warn('test-only: render warning');
1260 + return null;
1261 + }
1262 + const container = document.createElement('div');
1263 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1264 + act(() =>
1265 + ReactDOM.render(
1266 + <React.Fragment>
1267 + <ComponentWithError />
1268 + <ComponentWithWarning />
1269 + <ComponentWithWarningAndError />
1270 + </React.Fragment>,
1271 + container,
1272 + ),
1273 + );
1274 + });
1275 + expect(store).toMatchInlineSnapshot(`
1276 + ✕ 2, ⚠ 2
1277 + [root]
1278 + <ComponentWithError> ✕
1279 + <ComponentWithWarning> ⚠
1280 + <ComponentWithWarningAndError> ✕⚠
1281 + `);
1282 +
1283 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1284 + act(() =>
1285 + ReactDOM.render(
1286 + <React.Fragment>
1287 + <ComponentWithWarning />
1288 + <ComponentWithWarningAndError />
1289 + </React.Fragment>,
1290 + container,
1291 + ),
1292 + );
1293 + });
1294 + expect(store).toMatchInlineSnapshot(`
1295 + ✕ 1, ⚠ 2
1296 + [root]
1297 + <ComponentWithWarning> ⚠
1298 + <ComponentWithWarningAndError> ✕⚠
1299 + `);
1300 +
1301 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1302 + act(() =>
1303 + ReactDOM.render(
1304 + <React.Fragment>
1305 + <ComponentWithWarning />
1306 + </React.Fragment>,
1307 + container,
1308 + ),
1309 + );
1310 + });
1311 + expect(store).toMatchInlineSnapshot(`
1312 + ✕ 0, ⚠ 2
1313 + [root]
1314 + <ComponentWithWarning> ⚠
1315 + `);
1316 +
1317 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1318 + act(() => ReactDOM.render(<React.Fragment />, container));
1319 + });
1320 + expect(store).toMatchInlineSnapshot(`[root]`);
1321 + expect(store.errorCount).toBe(0);
1322 + expect(store.warningCount).toBe(0);
1323 + });
1324 + });
1325 });
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+226 -35
@@ -48,22 +48,28 @@ describe('Store component filters', () => {
48 });
49
50 it('should support filtering by element type', () => {
51 - class Root extends React.Component<{|children: React$Node|}> {
51 + class ClassComponent extends React.Component<{|children: React$Node|}> {
52 render() {
53 return <div>{this.props.children}</div>;
54 }
55 }
56 - const Component = () => <div>Hi</div>;
56 + const FunctionComponent = () => <div>Hi</div>;
57
58 act(() =>
59 ReactDOM.render(
60 - <Root>
61 - <Component />
62 - </Root>,
60 + <ClassComponent>
61 + <FunctionComponent />
62 + </ClassComponent>,
63 document.createElement('div'),
64 ),
65 );
66 - expect(store).toMatchSnapshot('1: mount');
66 + expect(store).toMatchInlineSnapshot(`
67 + [root]
68 + ▾ <ClassComponent>
69 + ▾ <div>
70 + ▾ <FunctionComponent>
71 + <div>
72 + `);
73
74 act(
75 () =>
@@ -71,8 +77,11 @@ describe('Store component filters', () => {
77 utils.createElementTypeFilter(Types.ElementTypeHostComponent),
78 ]),
79 );
74 -
75 - expect(store).toMatchSnapshot('2: hide host components');
80 + expect(store).toMatchInlineSnapshot(`
81 + [root]
82 + ▾ <ClassComponent>
83 + <FunctionComponent>
84 + `);
85
86 act(
87 () =>
@@ -80,8 +89,12 @@ describe('Store component filters', () => {
89 utils.createElementTypeFilter(Types.ElementTypeClass),
90 ]),
91 );
83 -
84 - expect(store).toMatchSnapshot('3: hide class components');
92 + expect(store).toMatchInlineSnapshot(`
93 + [root]
94 + ▾ <div>
95 + ▾ <FunctionComponent>
96 + <div>
97 + `);
98
99 act(
100 () =>
@@ -90,8 +103,11 @@ describe('Store component filters', () => {
103 utils.createElementTypeFilter(Types.ElementTypeFunction),
104 ]),
105 );
93 -
94 - expect(store).toMatchSnapshot('4: hide class and function components');
106 + expect(store).toMatchInlineSnapshot(`
107 + [root]
108 + ▾ <div>
109 + <div>
110 + `);
111
112 act(
113 () =>
@@ -100,15 +116,33 @@ describe('Store component filters', () => {
116 utils.createElementTypeFilter(Types.ElementTypeFunction, false),
117 ]),
118 );
103 -
104 - expect(store).toMatchSnapshot('5: disable all filters');
119 + expect(store).toMatchInlineSnapshot(`
120 + [root]
121 + ▾ <ClassComponent>
122 + ▾ <div>
123 + ▾ <FunctionComponent>
124 + <div>
125 + `);
126 +
127 + act(() => (store.componentFilters = []));
128 + expect(store).toMatchInlineSnapshot(`
129 + [root]
130 + ▾ <ClassComponent>
131 + ▾ <div>
132 + ▾ <FunctionComponent>
133 + <div>
134 + `);
135 });
136
137 it('should ignore invalid ElementTypeRoot filter', () => {
108 - const Root = () => <div>Hi</div>;
138 + const Component = () => <div>Hi</div>;
139
110 - act(() => ReactDOM.render(<Root />, document.createElement('div')));
111 - expect(store).toMatchSnapshot('1: mount');
140 + act(() => ReactDOM.render(<Component />, document.createElement('div')));
141 + expect(store).toMatchInlineSnapshot(`
142 + [root]
143 + ▾ <Component>
144 + <div>
145 + `);
146
147 act(
148 () =>
@@ -117,7 +151,11 @@ describe('Store component filters', () => {
151 ]),
152 );
153
120 - expect(store).toMatchSnapshot('2: add invalid filter');
154 + expect(store).toMatchInlineSnapshot(`
155 + [root]
156 + ▾ <Component>
157 + <div>
158 + `);
159 });
160
161 it('should filter by display name', () => {
@@ -136,27 +174,59 @@ describe('Store component filters', () => {
174 document.createElement('div'),
175 ),
176 );
139 - expect(store).toMatchSnapshot('1: mount');
177 + expect(store).toMatchInlineSnapshot(`
178 + [root]
179 + ▾ <Foo>
180 + <Text>
181 + ▾ <Bar>
182 + <Text>
183 + ▾ <Baz>
184 + <Text>
185 + `);
186
187 act(
188 () => (store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
189 );
144 - expect(store).toMatchSnapshot('2: filter "Foo"');
190 + expect(store).toMatchInlineSnapshot(`
191 + [root]
192 + <Text>
193 + ▾ <Bar>
194 + <Text>
195 + ▾ <Baz>
196 + <Text>
197 + `);
198
199 act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
147 - expect(store).toMatchSnapshot('3: filter "Ba"');
200 + expect(store).toMatchInlineSnapshot(`
201 + [root]
202 + ▾ <Foo>
203 + <Text>
204 + <Text>
205 + <Text>
206 + `);
207
208 act(
209 () => (store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
210 );
152 - expect(store).toMatchSnapshot('4: filter "B.z"');
211 + expect(store).toMatchInlineSnapshot(`
212 + [root]
213 + ▾ <Foo>
214 + <Text>
215 + ▾ <Bar>
216 + <Text>
217 + <Text>
218 + `);
219 });
220
221 it('should filter by path', () => {
222 const Component = () => <div>Hi</div>;
223
224 act(() => ReactDOM.render(<Component />, document.createElement('div')));
159 - expect(store).toMatchSnapshot('1: mount');
225 + expect(store).toMatchInlineSnapshot(`
226 + [root]
227 + ▾ <Component>
228 + <div>
229 + `);
230
231 act(
232 () =>
@@ -165,9 +235,7 @@ describe('Store component filters', () => {
235 ]),
236 );
237
168 - expect(store).toMatchSnapshot(
169 - '2: hide all components declared within this test filed',
170 - );
238 + expect(store).toMatchInlineSnapshot(`[root]`);
239
240 act(
241 () =>
@@ -176,7 +244,11 @@ describe('Store component filters', () => {
244 ]),
245 );
246
179 - expect(store).toMatchSnapshot('3: hide components in a made up fake path');
247 + expect(store).toMatchInlineSnapshot(`
248 + [root]
249 + ▾ <Component>
250 + <div>
251 + `);
252 });
253
254 it('should filter HOCs', () => {
@@ -187,15 +259,29 @@ describe('Store component filters', () => {
259 Bar.displayName = 'Bar(Foo(Component))';
260
261 act(() => ReactDOM.render(<Bar />, document.createElement('div')));
190 - expect(store).toMatchSnapshot('1: mount');
262 + expect(store).toMatchInlineSnapshot(`
263 + [root]
264 + ▾ <Component> [Bar][Foo]
265 + ▾ <Component> [Foo]
266 + ▾ <Component>
267 + <div>
268 + `);
269
270 act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
193 -
194 - expect(store).toMatchSnapshot('2: hide all HOCs');
271 + expect(store).toMatchInlineSnapshot(`
272 + [root]
273 + ▾ <Component>
274 + <div>
275 + `);
276
277 act(() => (store.componentFilters = [utils.createHOCFilter(false)]));
197 -
198 - expect(store).toMatchSnapshot('3: disable HOC filter');
278 + expect(store).toMatchInlineSnapshot(`
279 + [root]
280 + ▾ <Component> [Bar][Foo]
281 + ▾ <Component> [Foo]
282 + ▾ <Component>
283 + <div>
284 + `);
285 });
286
287 it('should not send a bridge update if the set of enabled filters has not changed', () => {
@@ -252,12 +338,117 @@ describe('Store component filters', () => {
338
339 const container = document.createElement('div');
340 act(() => ReactDOM.render(<Wrapper shouldSuspend={true} />, container));
255 - expect(store).toMatchSnapshot('1: suspended');
341 + expect(store).toMatchInlineSnapshot(`
342 + [root]
343 + ▾ <Wrapper>
344 + ▾ <Loading>
345 + <div>
346 + `);
347
348 act(() => ReactDOM.render(<Wrapper shouldSuspend={false} />, container));
258 - expect(store).toMatchSnapshot('2: resolved');
349 + expect(store).toMatchInlineSnapshot(`
350 + [root]
351 + ▾ <Wrapper>
352 + <Component>
353 + `);
354
355 act(() => ReactDOM.render(<Wrapper shouldSuspend={true} />, container));
261 - expect(store).toMatchSnapshot('3: suspended');
356 + expect(store).toMatchInlineSnapshot(`
357 + [root]
358 + ▾ <Wrapper>
359 + ▾ <Loading>
360 + <div>
361 + `);
362 + });
363 +
364 + describe('inline errors and warnings', () => {
365 + it('only counts for unfiltered components', () => {
366 + function ComponentWithWarning() {
367 + console.warn('test-only: render warning');
368 + return null;
369 + }
370 + function ComponentWithError() {
371 + console.error('test-only: render error');
372 + return null;
373 + }
374 + function ComponentWithWarningAndError() {
375 + console.error('test-only: render error');
376 + console.warn('test-only: render warning');
377 + return null;
378 + }
379 + const container = document.createElement('div');
380 + utils.withErrorsOrWarningsIgnored(['test-only:'], () => {
381 + act(
382 + () =>
383 + (store.componentFilters = [
384 + utils.createDisplayNameFilter('Warning'),
385 + utils.createDisplayNameFilter('Error'),
386 + ]),
387 + );
388 + act(() =>
389 + ReactDOM.render(
390 + <React.Fragment>
391 + <ComponentWithError />
392 + <ComponentWithWarning />
393 + <ComponentWithWarningAndError />
394 + </React.Fragment>,
395 + container,
396 + ),
397 + );
398 + });
399 +
400 + expect(store).toMatchInlineSnapshot(`[root]`);
401 + expect(store.errorCount).toBe(0);
402 + expect(store.warningCount).toBe(0);
403 +
404 + act(() => (store.componentFilters = []));
405 + expect(store).toMatchInlineSnapshot(`
406 + ✕ 2, ⚠ 2
407 + [root]
408 + <ComponentWithError> ✕
409 + <ComponentWithWarning> ⚠
410 + <ComponentWithWarningAndError> ✕⚠
411 + `);
412 +
413 + act(
414 + () =>
415 + (store.componentFilters = [utils.createDisplayNameFilter('Warning')]),
416 + );
417 + expect(store).toMatchInlineSnapshot(`
418 + ✕ 1, ⚠ 0
419 + [root]
420 + <ComponentWithError> ✕
421 + `);
422 +
423 + act(
424 + () =>
425 + (store.componentFilters = [utils.createDisplayNameFilter('Error')]),
426 + );
427 + expect(store).toMatchInlineSnapshot(`
428 + ✕ 0, ⚠ 1
429 + [root]
430 + <ComponentWithWarning> ⚠
431 + `);
432 +
433 + act(
434 + () =>
435 + (store.componentFilters = [
436 + utils.createDisplayNameFilter('Warning'),
437 + utils.createDisplayNameFilter('Error'),
438 + ]),
439 + );
440 + expect(store).toMatchInlineSnapshot(`[root]`);
441 + expect(store.errorCount).toBe(0);
442 + expect(store.warningCount).toBe(0);
443 +
444 + act(() => (store.componentFilters = []));
445 + expect(store).toMatchInlineSnapshot(`
446 + ✕ 2, ⚠ 2
447 + [root]
448 + <ComponentWithError> ✕
449 + <ComponentWithWarning> ⚠
450 + <ComponentWithWarningAndError> ✕⚠
451 + `);
452 + });
453 });
454 });
packages/react-devtools-shared/src/__tests__/treeContext-test.js
+1990 -189
@@ -22,6 +22,7 @@ describe('TreeListContext', () => {
22 let bridge: FrontendBridge;
23 let store: Store;
24 let utils;
25 + let withErrorsOrWarningsIgnored;
26
27 let BridgeContext;
28 let StoreContext;
@@ -34,6 +35,8 @@ describe('TreeListContext', () => {
35 utils = require('./utils');
36 utils.beforeEachProfiling();
37
38 + withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
39 +
40 bridge = global.bridge;
41 store = global.store;
42 store.collapseNodesByDefault = false;
@@ -88,43 +91,111 @@ describe('TreeListContext', () => {
91 ReactDOM.render(<Grandparent />, document.createElement('div')),
92 );
93
91 - expect(store).toMatchSnapshot('0: mount');
92 -
94 let renderer;
95 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
95 - expect(state).toMatchSnapshot('1: initial state');
96 + expect(state).toMatchInlineSnapshot(`
97 + [root]
98 + ▾ <Grandparent>
99 + ▾ <Parent>
100 + <Child>
101 + <Child>
102 + `);
103 +
104 + // Test stepping through to the end
105
106 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
107 utils.act(() => renderer.update(<Contexts />));
99 - expect(state).toMatchSnapshot('2: select first element');
108 + expect(state).toMatchInlineSnapshot(`
109 + [root]
110 + → ▾ <Grandparent>
111 + ▾ <Parent>
112 + <Child>
113 + <Child>
114 + `);
115
101 - while (
102 - state.selectedElementIndex !== null &&
103 - state.selectedElementIndex < store.numElements - 1
104 - ) {
105 - const index = ((state.selectedElementIndex: any): number);
106 - utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
107 - utils.act(() => renderer.update(<Contexts />));
108 - expect(state).toMatchSnapshot(`3: select element after (${index})`);
109 - }
116 + utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
117 + utils.act(() => renderer.update(<Contexts />));
118 + expect(state).toMatchInlineSnapshot(`
119 + [root]
120 + ▾ <Grandparent>
121 + → ▾ <Parent>
122 + <Child>
123 + <Child>
124 + `);
125
111 - while (
112 - state.selectedElementIndex !== null &&
113 - state.selectedElementIndex > 0
114 - ) {
115 - const index = ((state.selectedElementIndex: any): number);
116 - utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
117 - utils.act(() => renderer.update(<Contexts />));
118 - expect(state).toMatchSnapshot(`4: select element before (${index})`);
119 - }
126 + utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
127 + utils.act(() => renderer.update(<Contexts />));
128 + expect(state).toMatchInlineSnapshot(`
129 + [root]
130 + ▾ <Grandparent>
131 + ▾ <Parent>
132 + → <Child>
133 + <Child>
134 + `);
135 +
136 + utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
137 + utils.act(() => renderer.update(<Contexts />));
138 + expect(state).toMatchInlineSnapshot(`
139 + [root]
140 + ▾ <Grandparent>
141 + ▾ <Parent>
142 + <Child>
143 + → <Child>
144 + `);
145 +
146 + // Test stepping back to the beginning
147 +
148 + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
149 + utils.act(() => renderer.update(<Contexts />));
150 + expect(state).toMatchInlineSnapshot(`
151 + [root]
152 + ▾ <Grandparent>
153 + ▾ <Parent>
154 + → <Child>
155 + <Child>
156 + `);
157 +
158 + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
159 + utils.act(() => renderer.update(<Contexts />));
160 + expect(state).toMatchInlineSnapshot(`
161 + [root]
162 + ▾ <Grandparent>
163 + → ▾ <Parent>
164 + <Child>
165 + <Child>
166 + `);
167 +
168 + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
169 + utils.act(() => renderer.update(<Contexts />));
170 + expect(state).toMatchInlineSnapshot(`
171 + [root]
172 + → ▾ <Grandparent>
173 + ▾ <Parent>
174 + <Child>
175 + <Child>
176 + `);
177 +
178 + // Test wrap around behavior
179
180 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
181 utils.act(() => renderer.update(<Contexts />));
123 - expect(state).toMatchSnapshot('5: select previous wraps around to last');
182 + expect(state).toMatchInlineSnapshot(`
183 + [root]
184 + ▾ <Grandparent>
185 + ▾ <Parent>
186 + <Child>
187 + → <Child>
188 + `);
189
190 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
191 utils.act(() => renderer.update(<Contexts />));
127 - expect(state).toMatchSnapshot('6: select next wraps around to first');
192 + expect(state).toMatchInlineSnapshot(`
193 + [root]
194 + → ▾ <Grandparent>
195 + ▾ <Parent>
196 + <Child>
197 + <Child>
198 + `);
199 });
200
201 it('should select child elements', () => {
@@ -146,30 +217,71 @@ describe('TreeListContext', () => {
217 ReactDOM.render(<Grandparent />, document.createElement('div')),
218 );
219
149 - expect(store).toMatchSnapshot('0: mount');
150 -
220 let renderer;
221 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
153 - expect(state).toMatchSnapshot('1: initial state');
222 + expect(state).toMatchInlineSnapshot(`
223 + [root]
224 + ▾ <Grandparent>
225 + ▾ <Parent>
226 + <Child>
227 + <Child>
228 + ▾ <Parent>
229 + <Child>
230 + <Child>
231 + `);
232
233 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
234 utils.act(() => renderer.update(<Contexts />));
157 - expect(state).toMatchSnapshot('2: select first element');
235 + expect(state).toMatchInlineSnapshot(`
236 + [root]
237 + → ▾ <Grandparent>
238 + ▾ <Parent>
239 + <Child>
240 + <Child>
241 + ▾ <Parent>
242 + <Child>
243 + <Child>
244 + `);
245
246 utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
247 utils.act(() => renderer.update(<Contexts />));
161 - expect(state).toMatchSnapshot('3: select Parent');
248 + expect(state).toMatchInlineSnapshot(`
249 + [root]
250 + ▾ <Grandparent>
251 + → ▾ <Parent>
252 + <Child>
253 + <Child>
254 + ▾ <Parent>
255 + <Child>
256 + <Child>
257 + `);
258
259 utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
260 utils.act(() => renderer.update(<Contexts />));
165 - expect(state).toMatchSnapshot('4: select Child');
166 -
167 - const previousState = state;
261 + expect(state).toMatchInlineSnapshot(`
262 + [root]
263 + ▾ <Grandparent>
264 + ▾ <Parent>
265 + → <Child>
266 + <Child>
267 + ▾ <Parent>
268 + <Child>
269 + <Child>
270 + `);
271
272 // There are no more children to select, so this should be a no-op
273 utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
274 utils.act(() => renderer.update(<Contexts />));
172 - expect(state).toEqual(previousState);
275 + expect(state).toMatchInlineSnapshot(`
276 + [root]
277 + ▾ <Grandparent>
278 + ▾ <Parent>
279 + → <Child>
280 + <Child>
281 + ▾ <Parent>
282 + <Child>
283 + <Child>
284 + `);
285 });
286
287 it('should select parent elements and then collapse', () => {
@@ -191,27 +303,78 @@ describe('TreeListContext', () => {
303 ReactDOM.render(<Grandparent />, document.createElement('div')),
304 );
305
194 - expect(store).toMatchSnapshot('0: mount');
195 -
306 let renderer;
307 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
198 - expect(state).toMatchSnapshot('1: initial state');
308 + expect(state).toMatchInlineSnapshot(`
309 + [root]
310 + ▾ <Grandparent>
311 + ▾ <Parent>
312 + <Child>
313 + <Child>
314 + ▾ <Parent>
315 + <Child>
316 + <Child>
317 + `);
318
319 const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
320
321 + // Select the last child
322 utils.act(() =>
323 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID}),
324 );
325 utils.act(() => renderer.update(<Contexts />));
206 - expect(state).toMatchSnapshot('2: select last child');
207 -
326 + expect(state).toMatchInlineSnapshot(`
327 + [root]
328 + ▾ <Grandparent>
329 + ▾ <Parent>
330 + <Child>
331 + <Child>
332 + ▾ <Parent>
333 + <Child>
334 + → <Child>
335 + `);
336 +
337 + // Select its parent
338 utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
339 utils.act(() => renderer.update(<Contexts />));
210 - expect(state).toMatchSnapshot('3: select Parent');
211 -
340 + expect(state).toMatchInlineSnapshot(`
341 + [root]
342 + ▾ <Grandparent>
343 + ▾ <Parent>
344 + <Child>
345 + <Child>
346 + → ▾ <Parent>
347 + <Child>
348 + <Child>
349 + `);
350 +
351 + // Select grandparent
352 utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
353 utils.act(() => renderer.update(<Contexts />));
214 - expect(state).toMatchSnapshot('4: select Grandparent');
354 + expect(state).toMatchInlineSnapshot(`
355 + [root]
356 + → ▾ <Grandparent>
357 + ▾ <Parent>
358 + <Child>
359 + <Child>
360 + ▾ <Parent>
361 + <Child>
362 + <Child>
363 + `);
364 +
365 + // No-op
366 + utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
367 + utils.act(() => renderer.update(<Contexts />));
368 + expect(state).toMatchInlineSnapshot(`
369 + [root]
370 + → ▾ <Grandparent>
371 + ▾ <Parent>
372 + <Child>
373 + <Child>
374 + ▾ <Parent>
375 + <Child>
376 + <Child>
377 + `);
378
379 const previousState = state;
380
@@ -221,7 +384,7 @@ describe('TreeListContext', () => {
384 expect(state).toEqual(previousState);
385 });
386
224 - it('should clear selection if the selected element is unmounted', async done => {
387 + it('should clear selection if the selected element is unmounted', async () => {
388 const Grandparent = props => props.children || null;
389 const Parent = props => props.children || null;
390 const Child = () => null;
@@ -239,16 +402,28 @@ describe('TreeListContext', () => {
402 ),
403 );
404
242 - expect(store).toMatchSnapshot('0: mount');
243 -
405 let renderer;
406 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
246 - expect(state).toMatchSnapshot('1: initial state');
247 -
407 + expect(state).toMatchInlineSnapshot(`
408 + [root]
409 + ▾ <Grandparent>
410 + ▾ <Parent>
411 + <Child>
412 + <Child>
413 + `);
414 +
415 + // Select the second child
416 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 3}));
417 utils.act(() => renderer.update(<Contexts />));
250 - expect(state).toMatchSnapshot('2: select second child');
251 -
418 + expect(state).toMatchInlineSnapshot(`
419 + [root]
420 + ▾ <Grandparent>
421 + ▾ <Parent>
422 + <Child>
423 + → <Child>
424 + `);
425 +
426 + // Remove the child (which should auto-select the parent)
427 await utils.actAsync(() =>
428 ReactDOM.render(
429 <Grandparent>
@@ -257,16 +432,15 @@ describe('TreeListContext', () => {
432 container,
433 ),
434 );
260 - expect(state).toMatchSnapshot(
261 - '3: remove children (parent should now be selected)',
262 - );
435 + expect(state).toMatchInlineSnapshot(`
436 + [root]
437 + ▾ <Grandparent>
438 + → <Parent>
439 + `);
440
441 + // Unmount the root (so that nothing is selected)
442 await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
265 - expect(state).toMatchSnapshot(
266 - '4: unmount root (nothing should be selected)',
267 - );
268 -
269 - done();
443 + expect(state).toMatchInlineSnapshot(``);
444 });
445
446 it('should navigate next/previous sibling and skip over children in between', () => {
@@ -287,21 +461,6 @@ describe('TreeListContext', () => {
461 ReactDOM.render(<Grandparent />, document.createElement('div')),
462 );
463
290 - /*
291 - * 0 ▾ <Grandparent>
292 - * 1 ▾ <Parent>
293 - * 2 <Child key="0">
294 - * 3 ▾ <Parent>
295 - * 4 <Child key="0">
296 - * 5 <Child key="1">
297 - * 6 <Child key="2">
298 - * 7 ▾ <Parent>
299 - * 8 <Child key="0">
300 - * 9 <Child key="1">
301 - */
302 -
303 - expect(store).toMatchSnapshot('0: mount');
304 -
464 let renderer;
465 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
466
@@ -311,31 +470,131 @@ describe('TreeListContext', () => {
470 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}),
471 );
472 utils.act(() => renderer.update(<Contexts />));
314 - expect(state.selectedElementIndex).toBe(1);
473 + expect(state).toMatchInlineSnapshot(`
474 + [root]
475 + ▾ <Grandparent>
476 + → ▾ <Parent>
477 + <Child key="0">
478 + ▾ <Parent>
479 + <Child key="0">
480 + <Child key="1">
481 + <Child key="2">
482 + ▾ <Parent>
483 + <Child key="0">
484 + <Child key="1">
485 + `);
486
487 utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
488 utils.act(() => renderer.update(<Contexts />));
318 - expect(state.selectedElementIndex).toBe(3);
489 + expect(state).toMatchInlineSnapshot(`
490 + [root]
491 + ▾ <Grandparent>
492 + ▾ <Parent>
493 + <Child key="0">
494 + → ▾ <Parent>
495 + <Child key="0">
496 + <Child key="1">
497 + <Child key="2">
498 + ▾ <Parent>
499 + <Child key="0">
500 + <Child key="1">
501 + `);
502
503 utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
504 utils.act(() => renderer.update(<Contexts />));
322 - expect(state.selectedElementIndex).toBe(7);
505 + expect(state).toMatchInlineSnapshot(`
506 + [root]
507 + ▾ <Grandparent>
508 + ▾ <Parent>
509 + <Child key="0">
510 + ▾ <Parent>
511 + <Child key="0">
512 + <Child key="1">
513 + <Child key="2">
514 + → ▾ <Parent>
515 + <Child key="0">
516 + <Child key="1">
517 + `);
518
519 utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
520 utils.act(() => renderer.update(<Contexts />));
326 - expect(state.selectedElementIndex).toBe(1);
521 + expect(state).toMatchInlineSnapshot(`
522 + [root]
523 + ▾ <Grandparent>
524 + → ▾ <Parent>
525 + <Child key="0">
526 + ▾ <Parent>
527 + <Child key="0">
528 + <Child key="1">
529 + <Child key="2">
530 + ▾ <Parent>
531 + <Child key="0">
532 + <Child key="1">
533 + `);
534 +
535 + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
536 + utils.act(() => renderer.update(<Contexts />));
537 + expect(state).toMatchInlineSnapshot(`
538 + [root]
539 + ▾ <Grandparent>
540 + ▾ <Parent>
541 + <Child key="0">
542 + ▾ <Parent>
543 + <Child key="0">
544 + <Child key="1">
545 + <Child key="2">
546 + → ▾ <Parent>
547 + <Child key="0">
548 + <Child key="1">
549 + `);
550
551 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
552 utils.act(() => renderer.update(<Contexts />));
330 - expect(state.selectedElementIndex).toBe(7);
553 + expect(state).toMatchInlineSnapshot(`
554 + [root]
555 + ▾ <Grandparent>
556 + ▾ <Parent>
557 + <Child key="0">
558 + → ▾ <Parent>
559 + <Child key="0">
560 + <Child key="1">
561 + <Child key="2">
562 + ▾ <Parent>
563 + <Child key="0">
564 + <Child key="1">
565 + `);
566
567 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
568 utils.act(() => renderer.update(<Contexts />));
334 - expect(state.selectedElementIndex).toBe(3);
569 + expect(state).toMatchInlineSnapshot(`
570 + [root]
571 + ▾ <Grandparent>
572 + → ▾ <Parent>
573 + <Child key="0">
574 + ▾ <Parent>
575 + <Child key="0">
576 + <Child key="1">
577 + <Child key="2">
578 + ▾ <Parent>
579 + <Child key="0">
580 + <Child key="1">
581 + `);
582
583 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
584 utils.act(() => renderer.update(<Contexts />));
338 - expect(state.selectedElementIndex).toBe(1);
585 + expect(state).toMatchInlineSnapshot(`
586 + [root]
587 + ▾ <Grandparent>
588 + ▾ <Parent>
589 + <Child key="0">
590 + ▾ <Parent>
591 + <Child key="0">
592 + <Child key="1">
593 + <Child key="2">
594 + → ▾ <Parent>
595 + <Child key="0">
596 + <Child key="1">
597 + `);
598 });
599
600 it('should navigate the owner hierarchy', () => {
@@ -363,24 +622,6 @@ describe('TreeListContext', () => {
622 ReactDOM.render(<Grandparent />, document.createElement('div')),
623 );
624
366 - /*
367 - * 0 ▾ <Grandparent>
368 - * 1 ▾ <Wrapper>
369 - * 2 ▾ <Parent>
370 - * 3 <Child key="0">
371 - * 4 ▾ <Wrapper>
372 - * 5 ▾ <Parent>
373 - * 6 <Child key="0">
374 - * 7 <Child key="1">
375 - * 8 <Child key="2">
376 - * 9 ▾ <Wrapper>
377 - * 10 ▾ <Parent>
378 - * 11 <Child key="0">
379 - * 12 <Child key="1">
380 - */
381 -
382 - expect(store).toMatchSnapshot('0: mount');
383 -
625 let renderer;
626 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
627
@@ -389,86 +630,281 @@ describe('TreeListContext', () => {
630 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}),
631 );
632 utils.act(() => renderer.update(<Contexts />));
392 - expect(state.ownerSubtreeLeafElementID).toBeNull();
393 - expect(state.selectedElementIndex).toBe(7);
633 + expect(state).toMatchInlineSnapshot(`
634 + [root]
635 + ▾ <Grandparent>
636 + ▾ <Wrapper>
637 + ▾ <Parent>
638 + <Child key="0">
639 + ▾ <Wrapper>
640 + ▾ <Parent>
641 + <Child key="0">
642 + → <Child key="1">
643 + <Child key="2">
644 + ▾ <Wrapper>
645 + ▾ <Parent>
646 + <Child key="0">
647 + <Child key="1">
648 + `);
649
650 // Basic navigation test
651 utils.act(() =>
652 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
653 );
654 utils.act(() => renderer.update(<Contexts />));
400 - expect(state.ownerSubtreeLeafElementID).toBe(childID);
401 - expect(state.selectedElementIndex).toBe(5);
655 + expect(state).toMatchInlineSnapshot(`
656 + [root]
657 + ▾ <Grandparent>
658 + ▾ <Wrapper>
659 + ▾ <Parent>
660 + <Child key="0">
661 + ▾ <Wrapper>
662 + → ▾ <Parent>
663 + <Child key="0">
664 + <Child key="1">
665 + <Child key="2">
666 + ▾ <Wrapper>
667 + ▾ <Parent>
668 + <Child key="0">
669 + <Child key="1">
670 + `);
671
672 utils.act(() =>
673 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
674 );
675 utils.act(() => renderer.update(<Contexts />));
407 - expect(state.selectedElementIndex).toBe(0);
408 -
676 + expect(state).toMatchInlineSnapshot(`
677 + [root]
678 + → ▾ <Grandparent>
679 + ▾ <Wrapper>
680 + ▾ <Parent>
681 + <Child key="0">
682 + ▾ <Wrapper>
683 + ▾ <Parent>
684 + <Child key="0">
685 + <Child key="1">
686 + <Child key="2">
687 + ▾ <Wrapper>
688 + ▾ <Parent>
689 + <Child key="0">
690 + <Child key="1">
691 + `);
692 +
693 + // Noop (since we're at the root already)
694 utils.act(() =>
695 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
696 );
697 utils.act(() => renderer.update(<Contexts />));
413 - expect(state.selectedElementIndex).toBe(0); // noop since we're at the top
698 + expect(state).toMatchInlineSnapshot(`
699 + [root]
700 + → ▾ <Grandparent>
701 + ▾ <Wrapper>
702 + ▾ <Parent>
703 + <Child key="0">
704 + ▾ <Wrapper>
705 + ▾ <Parent>
706 + <Child key="0">
707 + <Child key="1">
708 + <Child key="2">
709 + ▾ <Wrapper>
710 + ▾ <Parent>
711 + <Child key="0">
712 + <Child key="1">
713 + `);
714
715 utils.act(() =>
716 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
717 );
718 utils.act(() => renderer.update(<Contexts />));
419 - expect(state.selectedElementIndex).toBe(5);
719 + expect(state).toMatchInlineSnapshot(`
720 + [root]
721 + ▾ <Grandparent>
722 + ▾ <Wrapper>
723 + ▾ <Parent>
724 + <Child key="0">
725 + ▾ <Wrapper>
726 + → ▾ <Parent>
727 + <Child key="0">
728 + <Child key="1">
729 + <Child key="2">
730 + ▾ <Wrapper>
731 + ▾ <Parent>
732 + <Child key="0">
733 + <Child key="1">
734 + `);
735
736 utils.act(() =>
737 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
738 );
739 utils.act(() => renderer.update(<Contexts />));
425 - expect(state.selectedElementIndex).toBe(7);
426 -
740 + expect(state).toMatchInlineSnapshot(`
741 + [root]
742 + ▾ <Grandparent>
743 + ▾ <Wrapper>
744 + ▾ <Parent>
745 + <Child key="0">
746 + ▾ <Wrapper>
747 + ▾ <Parent>
748 + <Child key="0">
749 + → <Child key="1">
750 + <Child key="2">
751 + ▾ <Wrapper>
752 + ▾ <Parent>
753 + <Child key="0">
754 + <Child key="1">
755 + `);
756 +
757 + // Noop (since we're at the leaf node)
758 utils.act(() =>
759 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
760 );
761 utils.act(() => renderer.update(<Contexts />));
431 - expect(state.selectedElementIndex).toBe(7); // noop since we're at the leaf node
762 + expect(state).toMatchInlineSnapshot(`
763 + [root]
764 + ▾ <Grandparent>
765 + ▾ <Wrapper>
766 + ▾ <Parent>
767 + <Child key="0">
768 + ▾ <Wrapper>
769 + ▾ <Parent>
770 + <Child key="0">
771 + → <Child key="1">
772 + <Child key="2">
773 + ▾ <Wrapper>
774 + ▾ <Parent>
775 + <Child key="0">
776 + <Child key="1">
777 + `);
778
779 // Other navigational actions should clear out the temporary owner chain.
780 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
781 utils.act(() => renderer.update(<Contexts />));
436 - expect(state.selectedElementIndex).toBe(6);
437 - expect(state.ownerSubtreeLeafElementID).toBeNull();
438 -
782 + expect(state).toMatchInlineSnapshot(`
783 + [root]
784 + ▾ <Grandparent>
785 + ▾ <Wrapper>
786 + ▾ <Parent>
787 + <Child key="0">
788 + ▾ <Wrapper>
789 + ▾ <Parent>
790 + → <Child key="0">
791 + <Child key="1">
792 + <Child key="2">
793 + ▾ <Wrapper>
794 + ▾ <Parent>
795 + <Child key="0">
796 + <Child key="1">
797 + `);
798 +
799 + // Start a new tree on parent
800 const parentID = ((store.getElementIDAtIndex(5): any): number);
801 utils.act(() =>
802 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: parentID}),
803 );
804 utils.act(() => renderer.update(<Contexts />));
444 - expect(state.ownerSubtreeLeafElementID).toBeNull();
445 - expect(state.selectedElementIndex).toBe(5);
805 + expect(state).toMatchInlineSnapshot(`
806 + [root]
807 + ▾ <Grandparent>
808 + ▾ <Wrapper>
809 + ▾ <Parent>
810 + <Child key="0">
811 + ▾ <Wrapper>
812 + → ▾ <Parent>
813 + <Child key="0">
814 + <Child key="1">
815 + <Child key="2">
816 + ▾ <Wrapper>
817 + ▾ <Parent>
818 + <Child key="0">
819 + <Child key="1">
820 + `);
821
447 - // It should not be possible to navigate beyond the owner chain leaf.
822 utils.act(() =>
823 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
824 );
825 utils.act(() => renderer.update(<Contexts />));
452 - expect(state.ownerSubtreeLeafElementID).toBe(parentID);
453 - expect(state.selectedElementIndex).toBe(0);
454 -
826 + expect(state).toMatchInlineSnapshot(`
827 + [root]
828 + → ▾ <Grandparent>
829 + ▾ <Wrapper>
830 + ▾ <Parent>
831 + <Child key="0">
832 + ▾ <Wrapper>
833 + ▾ <Parent>
834 + <Child key="0">
835 + <Child key="1">
836 + <Child key="2">
837 + ▾ <Wrapper>
838 + ▾ <Parent>
839 + <Child key="0">
840 + <Child key="1">
841 + `);
842 +
843 + // Noop (since we're at the top)
844 utils.act(() =>
845 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
846 );
847 utils.act(() => renderer.update(<Contexts />));
459 - expect(state.selectedElementIndex).toBe(0); // noop since we're at the top
848 + expect(state).toMatchInlineSnapshot(`
849 + [root]
850 + → ▾ <Grandparent>
851 + ▾ <Wrapper>
852 + ▾ <Parent>
853 + <Child key="0">
854 + ▾ <Wrapper>
855 + ▾ <Parent>
856 + <Child key="0">
857 + <Child key="1">
858 + <Child key="2">
859 + ▾ <Wrapper>
860 + ▾ <Parent>
861 + <Child key="0">
862 + <Child key="1">
863 + `);
864
865 utils.act(() =>
866 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
867 );
868 utils.act(() => renderer.update(<Contexts />));
465 - expect(state.selectedElementIndex).toBe(5);
466 -
869 + expect(state).toMatchInlineSnapshot(`
870 + [root]
871 + ▾ <Grandparent>
872 + ▾ <Wrapper>
873 + ▾ <Parent>
874 + <Child key="0">
875 + ▾ <Wrapper>
876 + → ▾ <Parent>
877 + <Child key="0">
878 + <Child key="1">
879 + <Child key="2">
880 + ▾ <Wrapper>
881 + ▾ <Parent>
882 + <Child key="0">
883 + <Child key="1">
884 + `);
885 +
886 + // Noop (since we're at the leaf of this owner tree)
887 + // It should not be possible to navigate beyond the owner chain leaf.
888 utils.act(() =>
889 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
890 );
891 utils.act(() => renderer.update(<Contexts />));
471 - expect(state.selectedElementIndex).toBe(5); // noop since we're at the leaf node
892 + expect(state).toMatchInlineSnapshot(`
893 + [root]
894 + ▾ <Grandparent>
895 + ▾ <Wrapper>
896 + ▾ <Parent>
897 + <Child key="0">
898 + ▾ <Wrapper>
899 + → ▾ <Parent>
900 + <Child key="0">
901 + <Child key="1">
902 + <Child key="2">
903 + ▾ <Wrapper>
904 + ▾ <Parent>
905 + <Child key="0">
906 + <Child key="1">
907 + `);
908 });
909 });
910
@@ -493,31 +929,59 @@ describe('TreeListContext', () => {
929 ),
930 );
931
496 - expect(store).toMatchSnapshot('0: mount');
497 -
932 let renderer;
933 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
500 - expect(state).toMatchSnapshot('1: initial state');
934 + expect(state).toMatchInlineSnapshot(`
935 + [root]
936 + <Foo>
937 + <Bar>
938 + <Baz>
939 + <Qux> [withHOC]
940 + `);
941
942 // NOTE: multi-match
943 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
944 utils.act(() => renderer.update(<Contexts />));
505 - expect(state).toMatchSnapshot('2: search for "ba"');
945 + expect(state).toMatchInlineSnapshot(`
946 + [root]
947 + <Foo>
948 + → <Bar>
949 + <Baz>
950 + <Qux> [withHOC]
951 + `);
952
953 // NOTE: single match
954 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'f'}));
955 utils.act(() => renderer.update(<Contexts />));
510 - expect(state).toMatchSnapshot('3: search for "f"');
956 + expect(state).toMatchInlineSnapshot(`
957 + [root]
958 + → <Foo>
959 + <Bar>
960 + <Baz>
961 + <Qux> [withHOC]
962 + `);
963
964 // NOTE: no match
965 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'y'}));
966 utils.act(() => renderer.update(<Contexts />));
515 - expect(state).toMatchSnapshot('4: search for "y"');
967 + expect(state).toMatchInlineSnapshot(`
968 + [root]
969 + → <Foo>
970 + <Bar>
971 + <Baz>
972 + <Qux> [withHOC]
973 + `);
974
975 // NOTE: HOC match
976 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'w'}));
977 utils.act(() => renderer.update(<Contexts />));
520 - expect(state).toMatchSnapshot('5: search for "w"');
978 + expect(state).toMatchInlineSnapshot(`
979 + [root]
980 + <Foo>
981 + <Bar>
982 + <Baz>
983 + → <Qux> [withHOC]
984 + `);
985 });
986
987 it('should select the next and previous items within the search results', () => {
@@ -537,42 +1001,95 @@ describe('TreeListContext', () => {
1001 ),
1002 );
1003
540 - expect(store).toMatchSnapshot('0: mount');
541 -
1004 let renderer;
1005 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
544 - expect(state).toMatchSnapshot('1: initial state');
545 -
1006 + expect(state).toMatchInlineSnapshot(`
1007 + [root]
1008 + <Foo>
1009 + <Baz>
1010 + <Bar>
1011 + <Baz>
1012 + `);
1013 +
1014 + // search for "ba"
1015 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1016 utils.act(() => renderer.update(<Contexts />));
548 - expect(state).toMatchSnapshot('2: search for "ba"');
549 -
1017 + expect(state).toMatchInlineSnapshot(`
1018 + [root]
1019 + <Foo>
1020 + → <Baz>
1021 + <Bar>
1022 + <Baz>
1023 + `);
1024 +
1025 + // go to second result
1026 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1027 utils.act(() => renderer.update(<Contexts />));
552 - expect(state).toMatchSnapshot('3: go to second result');
553 -
1028 + expect(state).toMatchInlineSnapshot(`
1029 + [root]
1030 + <Foo>
1031 + <Baz>
1032 + → <Bar>
1033 + <Baz>
1034 + `);
1035 +
1036 + // go to third result
1037 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1038 utils.act(() => renderer.update(<Contexts />));
556 - expect(state).toMatchSnapshot('4: go to third result');
557 -
1039 + expect(state).toMatchInlineSnapshot(`
1040 + [root]
1041 + <Foo>
1042 + <Baz>
1043 + <Bar>
1044 + → <Baz>
1045 + `);
1046 +
1047 + // go to second result
1048 utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
1049 utils.act(() => renderer.update(<Contexts />));
560 - expect(state).toMatchSnapshot('5: go to second result');
561 -
1050 + expect(state).toMatchInlineSnapshot(`
1051 + [root]
1052 + <Foo>
1053 + <Baz>
1054 + → <Bar>
1055 + <Baz>
1056 + `);
1057 +
1058 + // go to first result
1059 utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
1060 utils.act(() => renderer.update(<Contexts />));
564 - expect(state).toMatchSnapshot('6: go to first result');
565 -
1061 + expect(state).toMatchInlineSnapshot(`
1062 + [root]
1063 + <Foo>
1064 + → <Baz>
1065 + <Bar>
1066 + <Baz>
1067 + `);
1068 +
1069 + // wrap to last result
1070 utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
1071 utils.act(() => renderer.update(<Contexts />));
568 - expect(state).toMatchSnapshot('7: wrap to last result');
569 -
1072 + expect(state).toMatchInlineSnapshot(`
1073 + [root]
1074 + <Foo>
1075 + <Baz>
1076 + <Bar>
1077 + → <Baz>
1078 + `);
1079 +
1080 + // wrap to first result
1081 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1082 utils.act(() => renderer.update(<Contexts />));
572 - expect(state).toMatchSnapshot('8: wrap to first result');
1083 + expect(state).toMatchInlineSnapshot(`
1084 + [root]
1085 + <Foo>
1086 + → <Baz>
1087 + <Bar>
1088 + <Baz>
1089 + `);
1090 });
1091
575 - it('should add newly mounted elements to the search results set if they match the current text', async done => {
1092 + it('should add newly mounted elements to the search results set if they match the current text', async () => {
1093 const Foo = () => null;
1094 const Bar = () => null;
1095 const Baz = () => null;
@@ -589,15 +1106,21 @@ describe('TreeListContext', () => {
1106 ),
1107 );
1108
592 - expect(store).toMatchSnapshot('0: mount');
593 -
1109 let renderer;
1110 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
596 - expect(state).toMatchSnapshot('1: initial state');
1111 + expect(state).toMatchInlineSnapshot(`
1112 + [root]
1113 + <Foo>
1114 + <Bar>
1115 + `);
1116
1117 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1118 utils.act(() => renderer.update(<Contexts />));
600 - expect(state).toMatchSnapshot('2: search for "ba"');
1119 + expect(state).toMatchInlineSnapshot(`
1120 + [root]
1121 + <Foo>
1122 + → <Bar>
1123 + `);
1124
1125 await utils.actAsync(() =>
1126 ReactDOM.render(
@@ -610,12 +1133,24 @@ describe('TreeListContext', () => {
1133 ),
1134 );
1135 utils.act(() => renderer.update(<Contexts />));
613 - expect(state).toMatchSnapshot('3: mount Baz');
1136 + expect(state).toMatchInlineSnapshot(`
1137 + [root]
1138 + <Foo>
1139 + → <Bar>
1140 + <Baz>
1141 + `);
1142
615 - done();
1143 + utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1144 + utils.act(() => renderer.update(<Contexts />));
1145 + expect(state).toMatchInlineSnapshot(`
1146 + [root]
1147 + <Foo>
1148 + <Bar>
1149 + → <Baz>
1150 + `);
1151 });
1152
618 - it('should remove unmounted elements from the search results set', async done => {
1153 + it('should remove unmounted elements from the search results set', async () => {
1154 const Foo = () => null;
1155 const Bar = () => null;
1156 const Baz = () => null;
@@ -633,19 +1168,32 @@ describe('TreeListContext', () => {
1168 ),
1169 );
1170
636 - expect(store).toMatchSnapshot('0: mount');
637 -
1171 let renderer;
1172 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
640 - expect(state).toMatchSnapshot('1: initial state');
1173 + expect(state).toMatchInlineSnapshot(`
1174 + [root]
1175 + <Foo>
1176 + <Bar>
1177 + <Baz>
1178 + `);
1179
1180 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1181 utils.act(() => renderer.update(<Contexts />));
644 - expect(state).toMatchSnapshot('2: search for "ba"');
1182 + expect(state).toMatchInlineSnapshot(`
1183 + [root]
1184 + <Foo>
1185 + → <Bar>
1186 + <Baz>
1187 + `);
1188
1189 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1190 utils.act(() => renderer.update(<Contexts />));
648 - expect(state).toMatchSnapshot('3: go to second result');
1191 + expect(state).toMatchInlineSnapshot(`
1192 + [root]
1193 + <Foo>
1194 + <Bar>
1195 + → <Baz>
1196 + `);
1197
1198 await utils.actAsync(() =>
1199 ReactDOM.render(
@@ -657,9 +1205,28 @@ describe('TreeListContext', () => {
1205 ),
1206 );
1207 utils.act(() => renderer.update(<Contexts />));
660 - expect(state).toMatchSnapshot('4: unmount Baz');
1208 + expect(state).toMatchInlineSnapshot(`
1209 + [root]
1210 + <Foo>
1211 + <Bar>
1212 + `);
1213 +
1214 + utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1215 + utils.act(() => renderer.update(<Contexts />));
1216 + expect(state).toMatchInlineSnapshot(`
1217 + [root]
1218 + <Foo>
1219 + → <Bar>
1220 + `);
1221
662 - done();
1222 + // Noop since the list is now one item long
1223 + utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1224 + utils.act(() => renderer.update(<Contexts />));
1225 + expect(state).toMatchInlineSnapshot(`
1226 + [root]
1227 + <Foo>
1228 + → <Bar>
1229 + `);
1230 });
1231 });
1232
@@ -678,23 +1245,38 @@ describe('TreeListContext', () => {
1245 ReactDOM.render(<Grandparent />, document.createElement('div')),
1246 );
1247
681 - expect(store).toMatchSnapshot('0: mount');
682 -
1248 let renderer;
1249 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
685 - expect(state).toMatchSnapshot('1: initial state');
1250 + expect(state).toMatchInlineSnapshot(`
1251 + [root]
1252 + ▾ <Grandparent>
1253 + ▾ <Parent>
1254 + <Child>
1255 + <Child>
1256 + `);
1257
1258 const parentID = ((store.getElementIDAtIndex(1): any): number);
1259 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
1260 utils.act(() => renderer.update(<Contexts />));
690 - expect(state).toMatchSnapshot('2: parent owners tree');
1261 + expect(state).toMatchInlineSnapshot(`
1262 + [owners]
1263 + → ▾ <Parent>
1264 + <Child>
1265 + <Child>
1266 + `);
1267
1268 utils.act(() => dispatch({type: 'RESET_OWNER_STACK'}));
1269 utils.act(() => renderer.update(<Contexts />));
694 - expect(state).toMatchSnapshot('3: final state');
1270 + expect(state).toMatchInlineSnapshot(`
1271 + [root]
1272 + ▾ <Grandparent>
1273 + → ▾ <Parent>
1274 + <Child>
1275 + <Child>
1276 + `);
1277 });
1278
697 - it('should remove an element from the owners list if it is unmounted', async done => {
1279 + it('should remove an element from the owners list if it is unmounted', async () => {
1280 const Grandparent = ({count}) => <Parent count={count} />;
1281 const Parent = ({count}) =>
1282 new Array(count).fill(true).map((_, index) => <Child key={index} />);
@@ -703,31 +1285,45 @@ describe('TreeListContext', () => {
1285 const container = document.createElement('div');
1286 utils.act(() => ReactDOM.render(<Grandparent count={2} />, container));
1287
706 - expect(store).toMatchSnapshot('0: mount');
707 -
1288 let renderer;
1289 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
710 - expect(state).toMatchSnapshot('1: initial state');
1290 + expect(state).toMatchInlineSnapshot(`
1291 + [root]
1292 + ▾ <Grandparent>
1293 + ▾ <Parent>
1294 + <Child key="0">
1295 + <Child key="1">
1296 + `);
1297
1298 const parentID = ((store.getElementIDAtIndex(1): any): number);
1299 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
1300 utils.act(() => renderer.update(<Contexts />));
715 - expect(state).toMatchSnapshot('2: parent owners tree');
1301 + expect(state).toMatchInlineSnapshot(`
1302 + [owners]
1303 + → ▾ <Parent>
1304 + <Child key="0">
1305 + <Child key="1">
1306 + `);
1307
1308 await utils.actAsync(() =>
1309 ReactDOM.render(<Grandparent count={1} />, container),
1310 );
720 - expect(state).toMatchSnapshot('3: remove second child');
1311 + expect(state).toMatchInlineSnapshot(`
1312 + [owners]
1313 + → ▾ <Parent>
1314 + <Child key="0">
1315 + `);
1316
1317 await utils.actAsync(() =>
1318 ReactDOM.render(<Grandparent count={0} />, container),
1319 );
725 - expect(state).toMatchSnapshot('4: remove first child');
726 -
727 - done();
1320 + expect(state).toMatchInlineSnapshot(`
1321 + [owners]
1322 + → <Parent>
1323 + `);
1324 });
1325
730 - it('should exit the owners list if the current owner is unmounted', async done => {
1326 + it('should exit the owners list if the current owner is unmounted', async () => {
1327 const Parent = props => props.children || null;
1328 const Child = () => null;
1329
@@ -741,29 +1337,38 @@ describe('TreeListContext', () => {
1337 ),
1338 );
1339
744 - expect(store).toMatchSnapshot('0: mount');
745 -
1340 let renderer;
1341 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
748 - expect(state).toMatchSnapshot('1: initial state');
1342 + expect(state).toMatchInlineSnapshot(`
1343 + [root]
1344 + ▾ <Parent>
1345 + <Child>
1346 + `);
1347
1348 const childID = ((store.getElementIDAtIndex(1): any): number);
1349 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
1350 utils.act(() => renderer.update(<Contexts />));
753 - expect(state).toMatchSnapshot('2: child owners tree');
1351 + expect(state).toMatchInlineSnapshot(`
1352 + [owners]
1353 + → <Child>
1354 + `);
1355
1356 await utils.actAsync(() => ReactDOM.render(<Parent />, container));
756 - expect(state).toMatchSnapshot('3: remove child');
1357 + expect(state).toMatchInlineSnapshot(`
1358 + [root]
1359 + → <Parent>
1360 + `);
1361
1362 const parentID = ((store.getElementIDAtIndex(0): any): number);
1363 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
1364 utils.act(() => renderer.update(<Contexts />));
761 - expect(state).toMatchSnapshot('4: parent owners tree');
1365 + expect(state).toMatchInlineSnapshot(`
1366 + [owners]
1367 + → <Parent>
1368 + `);
1369
1370 await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
764 - expect(state).toMatchSnapshot('5: unmount root');
765 -
766 - done();
1371 + expect(state).toMatchInlineSnapshot(``);
1372 });
1373
1374 // This tests ensures support for toggling Suspense boundaries outside of the active owners list.
@@ -783,11 +1388,16 @@ describe('TreeListContext', () => {
1388 const container = document.createElement('div');
1389 utils.act(() => ReactDOM.render(<Parent />, container));
1390
786 - expect(store).toMatchSnapshot('0: mount');
787 -
1391 let renderer;
1392 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
790 - expect(state).toMatchSnapshot('1: initial state');
1393 + expect(state).toMatchInlineSnapshot(`
1394 + [root]
1395 + ▾ <Parent>
1396 + ▾ <Suspense>
1397 + ▾ <Child>
1398 + ▾ <Suspense>
1399 + <Grandchild>
1400 + `);
1401
1402 const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
1403 const childID = ((store.getElementIDAtIndex(2): any): number);
@@ -795,21 +1405,1212 @@ describe('TreeListContext', () => {
1405
1406 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
1407 utils.act(() => renderer.update(<Contexts />));
798 - expect(state).toMatchSnapshot('2: child owners tree');
1408 + expect(state).toMatchInlineSnapshot(`
1409 + [owners]
1410 + → ▾ <Child>
1411 + ▾ <Suspense>
1412 + <Grandchild>
1413 + `);
1414
1415 // Toggling a Suspense boundary inside of the flat list should update selected index
1416 utils.act(() =>
1417 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: innerSuspenseID}),
1418 );
1419 utils.act(() => renderer.update(<Contexts />));
805 - expect(state).toMatchSnapshot('3: child owners tree');
1420 + expect(state).toMatchInlineSnapshot(`
1421 + [owners]
1422 + ▾ <Child>
1423 + → ▾ <Suspense>
1424 + <Grandchild>
1425 + `);
1426
1427 // Toggling a Suspense boundary outside of the flat list should exit owners list and update index
1428 utils.act(() =>
1429 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: outerSuspenseID}),
1430 );
1431 utils.act(() => renderer.update(<Contexts />));
812 - expect(state).toMatchSnapshot('4: main tree');
1432 + expect(state).toMatchInlineSnapshot(`
1433 + [root]
1434 + ▾ <Parent>
1435 + → ▾ <Suspense>
1436 + ▾ <Child>
1437 + ▾ <Suspense>
1438 + <Grandchild>
1439 + `);
1440 + });
1441 + });
1442 +
1443 + describe('inline errors/warnings state', () => {
1444 + function clearAllErrors() {
1445 + utils.act(() => store.clearErrorsAndWarnings());
1446 + // flush events to the renderer
1447 + jest.runAllTimers();
1448 + }
1449 +
1450 + function clearErrorsForElement(id) {
1451 + utils.act(() => store.clearErrorsForElement(id));
1452 + // flush events to the renderer
1453 + jest.runAllTimers();
1454 + }
1455 +
1456 + function clearWarningsForElement(id) {
1457 + utils.act(() => store.clearWarningsForElement(id));
1458 + // flush events to the renderer
1459 + jest.runAllTimers();
1460 + }
1461 +
1462 + function selectNextErrorOrWarning() {
1463 + utils.act(() =>
1464 + dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'}),
1465 + );
1466 + }
1467 +
1468 + function selectPreviousErrorOrWarning() {
1469 + utils.act(() =>
1470 + dispatch({
1471 + type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
1472 + }),
1473 + );
1474 + }
1475 +
1476 + function Child({logError = false, logWarning = false}) {
1477 + if (logError === true) {
1478 + console.error('test-only: error');
1479 + }
1480 + if (logWarning === true) {
1481 + console.warn('test-only: warning');
1482 + }
1483 + return null;
1484 + }
1485 +
1486 + it('should handle when there are no errors/warnings', () => {
1487 + utils.act(() =>
1488 + ReactDOM.render(
1489 + <React.Fragment>
1490 + <Child />
1491 + <Child />
1492 + <Child />
1493 + </React.Fragment>,
1494 + document.createElement('div'),
1495 + ),
1496 + );
1497 +
1498 + utils.act(() => TestRenderer.create(<Contexts />));
1499 +
1500 + expect(state).toMatchInlineSnapshot(`
1501 + [root]
1502 + <Child>
1503 + <Child>
1504 + <Child>
1505 + `);
1506 +
1507 + // Next/previous errors should be a no-op
1508 + selectPreviousErrorOrWarning();
1509 + expect(state).toMatchInlineSnapshot(`
1510 + [root]
1511 + <Child>
1512 + <Child>
1513 + <Child>
1514 + `);
1515 + selectNextErrorOrWarning();
1516 + expect(state).toMatchInlineSnapshot(`
1517 + [root]
1518 + <Child>
1519 + <Child>
1520 + <Child>
1521 + `);
1522 +
1523 + utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
1524 + expect(state).toMatchInlineSnapshot(`
1525 + [root]
1526 + → <Child>
1527 + <Child>
1528 + <Child>
1529 + `);
1530 +
1531 + // Next/previous errors should still be a no-op
1532 + selectPreviousErrorOrWarning();
1533 + expect(state).toMatchInlineSnapshot(`
1534 + [root]
1535 + → <Child>
1536 + <Child>
1537 + <Child>
1538 + `);
1539 + selectNextErrorOrWarning();
1540 + expect(state).toMatchInlineSnapshot(`
1541 + [root]
1542 + → <Child>
1543 + <Child>
1544 + <Child>
1545 + `);
1546 + });
1547 +
1548 + it('should cycle through the next errors/warnings and wrap around', () => {
1549 + withErrorsOrWarningsIgnored(['test-only:'], () =>
1550 + utils.act(() =>
1551 + ReactDOM.render(
1552 + <React.Fragment>
1553 + <Child />
1554 + <Child logWarning={true} />
1555 + <Child />
1556 + <Child logError={true} />
1557 + <Child />
1558 + </React.Fragment>,
1559 + document.createElement('div'),
1560 + ),
1561 + ),
1562 + );
1563 +
1564 + utils.act(() => TestRenderer.create(<Contexts />));
1565 + expect(state).toMatchInlineSnapshot(`
1566 + ✕ 1, ⚠ 1
1567 + [root]
1568 + <Child>
1569 + <Child> ⚠
1570 + <Child>
1571 + <Child> ✕
1572 + <Child>
1573 + `);
1574 +
1575 + selectNextErrorOrWarning();
1576 + expect(state).toMatchInlineSnapshot(`
1577 + ✕ 1, ⚠ 1
1578 + [root]
1579 + <Child>
1580 + → <Child> ⚠
1581 + <Child>
1582 + <Child> ✕
1583 + <Child>
1584 + `);
1585 +
1586 + selectNextErrorOrWarning();
1587 + expect(state).toMatchInlineSnapshot(`
1588 + ✕ 1, ⚠ 1
1589 + [root]
1590 + <Child>
1591 + <Child> ⚠
1592 + <Child>
1593 + → <Child> ✕
1594 + <Child>
1595 + `);
1596 +
1597 + selectNextErrorOrWarning();
1598 + expect(state).toMatchInlineSnapshot(`
1599 + ✕ 1, ⚠ 1
1600 + [root]
1601 + <Child>
1602 + → <Child> ⚠
1603 + <Child>
1604 + <Child> ✕
1605 + <Child>
1606 + `);
1607 + });
1608 +
1609 + it('should cycle through the previous errors/warnings and wrap around', () => {
1610 + withErrorsOrWarningsIgnored(['test-only:'], () =>
1611 + utils.act(() =>
1612 + ReactDOM.render(
1613 + <React.Fragment>
1614 + <Child />
1615 + <Child logWarning={true} />
1616 + <Child />
1617 + <Child logError={true} />
1618 + <Child />
1619 + </React.Fragment>,
1620 + document.createElement('div'),
1621 + ),
1622 + ),
1623 + );
1624 +
1625 + utils.act(() => TestRenderer.create(<Contexts />));
1626 + expect(state).toMatchInlineSnapshot(`
1627 + ✕ 1, ⚠ 1
1628 + [root]
1629 + <Child>
1630 + <Child> ⚠
1631 + <Child>
1632 + <Child> ✕
1633 + <Child>
1634 + `);
1635 +
1636 + selectPreviousErrorOrWarning();
1637 + expect(state).toMatchInlineSnapshot(`
1638 + ✕ 1, ⚠ 1
1639 + [root]
1640 + <Child>
1641 + <Child> ⚠
1642 + <Child>
1643 + → <Child> ✕
1644 + <Child>
1645 + `);
1646 +
1647 + selectPreviousErrorOrWarning();
1648 + expect(state).toMatchInlineSnapshot(`
1649 + ✕ 1, ⚠ 1
1650 + [root]
1651 + <Child>
1652 + → <Child> ⚠
1653 + <Child>
1654 + <Child> ✕
1655 + <Child>
1656 + `);
1657 +
1658 + selectPreviousErrorOrWarning();
1659 + expect(state).toMatchInlineSnapshot(`
1660 + ✕ 1, ⚠ 1
1661 + [root]
1662 + <Child>
1663 + <Child> ⚠
1664 + <Child>
1665 + → <Child> ✕
1666 + <Child>
1667 + `);
1668 + });
1669 +
1670 + it('should cycle through the next errors/warnings and wrap around with multiple roots', () => {
1671 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1672 + utils.act(() => {
1673 + ReactDOM.render(
1674 + <React.Fragment>
1675 + <Child />
1676 + <Child logWarning={true} />,
1677 + </React.Fragment>,
1678 + document.createElement('div'),
1679 + );
1680 + ReactDOM.render(
1681 + <React.Fragment>
1682 + <Child />
1683 + <Child logError={true} />
1684 + <Child />
1685 + </React.Fragment>,
1686 + document.createElement('div'),
1687 + );
1688 + });
1689 + });
1690 +
1691 + utils.act(() => TestRenderer.create(<Contexts />));
1692 + expect(state).toMatchInlineSnapshot(`
1693 + ✕ 1, ⚠ 1
1694 + [root]
1695 + <Child>
1696 + <Child> ⚠
1697 + [root]
1698 + <Child>
1699 + <Child> ✕
1700 + <Child>
1701 + `);
1702 +
1703 + selectNextErrorOrWarning();
1704 + expect(state).toMatchInlineSnapshot(`
1705 + ✕ 1, ⚠ 1
1706 + [root]
1707 + <Child>
1708 + → <Child> ⚠
1709 + [root]
1710 + <Child>
1711 + <Child> ✕
1712 + <Child>
1713 + `);
1714 +
1715 + selectNextErrorOrWarning();
1716 + expect(state).toMatchInlineSnapshot(`
1717 + ✕ 1, ⚠ 1
1718 + [root]
1719 + <Child>
1720 + <Child> ⚠
1721 + [root]
1722 + <Child>
1723 + → <Child> ✕
1724 + <Child>
1725 + `);
1726 +
1727 + selectNextErrorOrWarning();
1728 + expect(state).toMatchInlineSnapshot(`
1729 + ✕ 1, ⚠ 1
1730 + [root]
1731 + <Child>
1732 + → <Child> ⚠
1733 + [root]
1734 + <Child>
1735 + <Child> ✕
1736 + <Child>
1737 + `);
1738 + });
1739 +
1740 + it('should cycle through the previous errors/warnings and wrap around with multiple roots', () => {
1741 + withErrorsOrWarningsIgnored(['test-only:'], () => {
1742 + utils.act(() => {
1743 + ReactDOM.render(
1744 + <React.Fragment>
1745 + <Child />
1746 + <Child logWarning={true} />,
1747 + </React.Fragment>,
1748 + document.createElement('div'),
1749 + );
1750 + ReactDOM.render(
1751 + <React.Fragment>
1752 + <Child />
1753 + <Child logError={true} />
1754 + <Child />
1755 + </React.Fragment>,
1756 + document.createElement('div'),
1757 + );
1758 + });
1759 + });
1760 +
1761 + utils.act(() => TestRenderer.create(<Contexts />));
1762 + expect(state).toMatchInlineSnapshot(`
1763 + ✕ 1, ⚠ 1
1764 + [root]
1765 + <Child>
1766 + <Child> ⚠
1767 + [root]
1768 + <Child>
1769 + <Child> ✕
1770 + <Child>
1771 + `);
1772 +
1773 + selectPreviousErrorOrWarning();
1774 + expect(state).toMatchInlineSnapshot(`
1775 + ✕ 1, ⚠ 1
1776 + [root]
1777 + <Child>
1778 + <Child> ⚠
1779 + [root]
1780 + <Child>
1781 + → <Child> ✕
1782 + <Child>
1783 + `);
1784 +
1785 + selectPreviousErrorOrWarning();
1786 + expect(state).toMatchInlineSnapshot(`
1787 + ✕ 1, ⚠ 1
1788 + [root]
1789 + <Child>
1790 + → <Child> ⚠
1791 + [root]
1792 + <Child>
1793 + <Child> ✕
1794 + <Child>
1795 + `);
1796 +
1797 + selectPreviousErrorOrWarning();
1798 + expect(state).toMatchInlineSnapshot(`
1799 + ✕ 1, ⚠ 1
1800 + [root]
1801 + <Child>
1802 + <Child> ⚠
1803 + [root]
1804 + <Child>
1805 + → <Child> ✕
1806 + <Child>
1807 + `);
1808 + });
1809 +
1810 + it('should select the next or previous element relative to the current selection', () => {
1811 + withErrorsOrWarningsIgnored(['test-only:'], () =>
1812 + utils.act(() =>
1813 + ReactDOM.render(
1814 + <React.Fragment>
1815 + <Child />
1816 + <Child logWarning={true} />
1817 + <Child />
1818 + <Child logError={true} />
1819 + <Child />
1820 + </React.Fragment>,
1821 + document.createElement('div'),
1822 + ),
1823 + ),
1824 + );
1825 +
1826 + utils.act(() => TestRenderer.create(<Contexts />));
1827 + utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
1828 + expect(state).toMatchInlineSnapshot(`
1829 + ✕ 1, ⚠ 1
1830 + [root]
1831 + <Child>
1832 + <Child> ⚠
1833 + → <Child>
1834 + <Child> ✕
1835 + <Child>
1836 + `);
1837 +
1838 + selectNextErrorOrWarning();
1839 + expect(state).toMatchInlineSnapshot(`
1840 + ✕ 1, ⚠ 1
1841 + [root]
1842 + <Child>
1843 + <Child> ⚠
1844 + <Child>
1845 + → <Child> ✕
1846 + <Child>
1847 + `);
1848 +
1849 + utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
1850 + expect(state).toMatchInlineSnapshot(`
1851 + ✕ 1, ⚠ 1
1852 + [root]
1853 + <Child>
1854 + <Child> ⚠
1855 + → <Child>
1856 + <Child> ✕
1857 + <Child>
1858 + `);
1859 +
1860 + selectPreviousErrorOrWarning();
1861 + expect(state).toMatchInlineSnapshot(`
1862 + ✕ 1, ⚠ 1
1863 + [root]
1864 + <Child>
1865 + → <Child> ⚠
1866 + <Child>
1867 + <Child> ✕
1868 + <Child>
1869 + `);
1870 + });
1871 +
1872 + it('should update correctly when errors/warnings are cleared for a fiber in the list', () => {
1873 + withErrorsOrWarningsIgnored(['test-only:'], () =>
1874 + utils.act(() =>
1875 + ReactDOM.render(
1876 + <React.Fragment>
1877 + <Child logWarning={true} />
1878 + <Child logError={true} />
1879 + <Child logError={true} />
1880 + <Child logWarning={true} />
1881 + </React.Fragment>,
1882 + document.createElement('div'),
1883 + ),
1884 + ),
1885 + );
1886 +
1887 + utils.act(() => TestRenderer.create(<Contexts />));
1888 + expect(state).toMatchInlineSnapshot(`
1889 + ✕ 2, ⚠ 2
1890 + [root]
1891 + <Child> ⚠
1892 + <Child> ✕
1893 + <Child> ✕
1894 + <Child> ⚠
1895 + `);
1896 +
1897 + // Select the first item in the list
1898 + selectNextErrorOrWarning();
1899 + expect(state).toMatchInlineSnapshot(`
1900 + ✕ 2, ⚠ 2
1901 + [root]
1902 + → <Child> ⚠
1903 + <Child> ✕
1904 + <Child> ✕
1905 + <Child> ⚠
1906 + `);
1907 +
1908 + // Clear warnings (but the next Fiber has only errors)
1909 + clearWarningsForElement(store.getElementIDAtIndex(1));
1910 + selectNextErrorOrWarning();
1911 + expect(state).toMatchInlineSnapshot(`
1912 + ✕ 2, ⚠ 2
1913 + [root]
1914 + <Child> ⚠
1915 + → <Child> ✕
1916 + <Child> ✕
1917 + <Child> ⚠
1918 + `);
1919 +
1920 + clearErrorsForElement(store.getElementIDAtIndex(2));
1921 +
1922 + // Should step to the (now) next one in the list.
1923 + selectNextErrorOrWarning();
1924 + expect(state).toMatchInlineSnapshot(`
1925 + ✕ 1, ⚠ 2
1926 + [root]
1927 + <Child> ⚠
1928 + <Child> ✕
1929 + <Child>
1930 + → <Child> ⚠
1931 + `);
1932 +
1933 + // Should skip over the (now) cleared Fiber
1934 + selectPreviousErrorOrWarning();
1935 + expect(state).toMatchInlineSnapshot(`
1936 + ✕ 1, ⚠ 2
1937 + [root]
1938 + <Child> ⚠
1939 + → <Child> ✕
1940 + <Child>
1941 + <Child> ⚠
1942 + `);
1943 + });
1944 +
1945 + it('should update correctly when errors/warnings are cleared for the currently selected fiber', () => {
1946 + withErrorsOrWarningsIgnored(['test-only:'], () =>
1947 + utils.act(() =>
1948 + ReactDOM.render(
1949 + <React.Fragment>
1950 + <Child logWarning={true} />
1951 + <Child logError={true} />
1952 + </React.Fragment>,
1953 + document.createElement('div'),
1954 + ),
1955 + ),
1956 + );
1957 +
1958 + utils.act(() => TestRenderer.create(<Contexts />));
1959 + expect(state).toMatchInlineSnapshot(`
1960 + ✕ 1, ⚠ 1
1961 + [root]
1962 + <Child> ⚠
1963 + <Child> ✕
1964 + `);
1965 +
1966 + selectNextErrorOrWarning();
1967 + expect(state).toMatchInlineSnapshot(`
1968 + ✕ 1, ⚠ 1
1969 + [root]
1970 + → <Child> ⚠
1971 + <Child> ✕
1972 + `);
1973 +
1974 + clearWarningsForElement(store.getElementIDAtIndex(0));
1975 + selectNextErrorOrWarning();
1976 + expect(state).toMatchInlineSnapshot(`
1977 + ✕ 1, ⚠ 0
1978 + [root]
1979 + <Child>
1980 + → <Child> ✕
1981 + `);
1982 + });
1983 +
1984 + it('should update correctly when new errors/warnings are added', () => {
1985 + const container = document.createElement('div');
1986 +
1987 + withErrorsOrWarningsIgnored(['test-only:'], () =>
1988 + utils.act(() =>
1989 + ReactDOM.render(
1990 + <React.Fragment>
1991 + <Child logWarning={true} />
1992 + <Child />
1993 + <Child />
1994 + <Child logError={true} />
1995 + </React.Fragment>,
1996 + container,
1997 + ),
1998 + ),
1999 + );
2000 +
2001 + utils.act(() => TestRenderer.create(<Contexts />));
2002 + expect(state).toMatchInlineSnapshot(`
2003 + ✕ 1, ⚠ 1
2004 + [root]
2005 + <Child> ⚠
2006 + <Child>
2007 + <Child>
2008 + <Child> ✕
2009 + `);
2010 +
2011 + selectNextErrorOrWarning();
2012 + expect(state).toMatchInlineSnapshot(`
2013 + ✕ 1, ⚠ 1
2014 + [root]
2015 + → <Child> ⚠
2016 + <Child>
2017 + <Child>
2018 + <Child> ✕
2019 + `);
2020 +
2021 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2022 + utils.act(() =>
2023 + ReactDOM.render(
2024 + <React.Fragment>
2025 + <Child />
2026 + <Child logWarning={true} />
2027 + <Child />
2028 + <Child />
2029 + </React.Fragment>,
2030 + container,
2031 + ),
2032 + ),
2033 + );
2034 +
2035 + selectNextErrorOrWarning();
2036 + expect(state).toMatchInlineSnapshot(`
2037 + ✕ 1, ⚠ 2
2038 + [root]
2039 + <Child> ⚠
2040 + → <Child> ⚠
2041 + <Child>
2042 + <Child> ✕
2043 + `);
2044 +
2045 + selectNextErrorOrWarning();
2046 + expect(state).toMatchInlineSnapshot(`
2047 + ✕ 1, ⚠ 2
2048 + [root]
2049 + <Child> ⚠
2050 + <Child> ⚠
2051 + <Child>
2052 + → <Child> ✕
2053 + `);
2054 +
2055 + selectNextErrorOrWarning();
2056 + expect(state).toMatchInlineSnapshot(`
2057 + ✕ 1, ⚠ 2
2058 + [root]
2059 + → <Child> ⚠
2060 + <Child> ⚠
2061 + <Child>
2062 + <Child> ✕
2063 + `);
2064 + });
2065 +
2066 + it('should update correctly when all errors/warnings are cleared', () => {
2067 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2068 + utils.act(() =>
2069 + ReactDOM.render(
2070 + <React.Fragment>
2071 + <Child logWarning={true} />
2072 + <Child logError={true} />
2073 + </React.Fragment>,
2074 + document.createElement('div'),
2075 + ),
2076 + ),
2077 + );
2078 +
2079 + utils.act(() => TestRenderer.create(<Contexts />));
2080 + expect(state).toMatchInlineSnapshot(`
2081 + ✕ 1, ⚠ 1
2082 + [root]
2083 + <Child> ⚠
2084 + <Child> ✕
2085 + `);
2086 +
2087 + selectNextErrorOrWarning();
2088 + expect(state).toMatchInlineSnapshot(`
2089 + ✕ 1, ⚠ 1
2090 + [root]
2091 + → <Child> ⚠
2092 + <Child> ✕
2093 + `);
2094 +
2095 + clearAllErrors();
2096 +
2097 + selectNextErrorOrWarning();
2098 + expect(state).toMatchInlineSnapshot(`
2099 + [root]
2100 + → <Child>
2101 + <Child>
2102 + `);
2103 +
2104 + selectPreviousErrorOrWarning();
2105 + expect(state).toMatchInlineSnapshot(`
2106 + [root]
2107 + → <Child>
2108 + <Child>
2109 + `);
2110 + });
2111 +
2112 + it('should update select and auto-expand parts components within hidden parts of the tree', () => {
2113 + const Wrapper = ({children}) => children;
2114 +
2115 + store.collapseNodesByDefault = true;
2116 +
2117 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2118 + utils.act(() =>
2119 + ReactDOM.render(
2120 + <React.Fragment>
2121 + <Wrapper>
2122 + <Child logWarning={true} />
2123 + </Wrapper>
2124 + <Wrapper>
2125 + <Wrapper>
2126 + <Child logWarning={true} />
2127 + </Wrapper>
2128 + </Wrapper>
2129 + </React.Fragment>,
2130 + document.createElement('div'),
2131 + ),
2132 + ),
2133 + );
2134 +
2135 + utils.act(() => TestRenderer.create(<Contexts />));
2136 + expect(state).toMatchInlineSnapshot(`
2137 + ✕ 0, ⚠ 2
2138 + [root]
2139 + ▸ <Wrapper>
2140 + ▸ <Wrapper>
2141 + `);
2142 +
2143 + selectNextErrorOrWarning();
2144 + expect(state).toMatchInlineSnapshot(`
2145 + ✕ 0, ⚠ 2
2146 + [root]
2147 + ▾ <Wrapper>
2148 + → <Child> ⚠
2149 + ▸ <Wrapper>
2150 + `);
2151 +
2152 + selectNextErrorOrWarning();
2153 + expect(state).toMatchInlineSnapshot(`
2154 + ✕ 0, ⚠ 2
2155 + [root]
2156 + ▾ <Wrapper>
2157 + <Child> ⚠
2158 + ▾ <Wrapper>
2159 + ▾ <Wrapper>
2160 + → <Child> ⚠
2161 + `);
2162 + });
2163 +
2164 + it('should properly handle when components filters are updated', () => {
2165 + const Wrapper = ({children}) => children;
2166 +
2167 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2168 + utils.act(() =>
2169 + ReactDOM.render(
2170 + <React.Fragment>
2171 + <Wrapper>
2172 + <Child logWarning={true} />
2173 + </Wrapper>
2174 + <Wrapper>
2175 + <Wrapper>
2176 + <Child logWarning={true} />
2177 + </Wrapper>
2178 + </Wrapper>
2179 + </React.Fragment>,
2180 + document.createElement('div'),
2181 + ),
2182 + ),
2183 + );
2184 +
2185 + utils.act(() => TestRenderer.create(<Contexts />));
2186 + expect(state).toMatchInlineSnapshot(`
2187 + ✕ 0, ⚠ 2
2188 + [root]
2189 + ▾ <Wrapper>
2190 + <Child> ⚠
2191 + ▾ <Wrapper>
2192 + ▾ <Wrapper>
2193 + <Child> ⚠
2194 + `);
2195 +
2196 + selectNextErrorOrWarning();
2197 + expect(state).toMatchInlineSnapshot(`
2198 + ✕ 0, ⚠ 2
2199 + [root]
2200 + ▾ <Wrapper>
2201 + → <Child> ⚠
2202 + ▾ <Wrapper>
2203 + ▾ <Wrapper>
2204 + <Child> ⚠
2205 + `);
2206 +
2207 + utils.act(() => {
2208 + store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
2209 + });
2210 + expect(state).toMatchInlineSnapshot(`
2211 + ✕ 0, ⚠ 2
2212 + [root]
2213 + → <Child> ⚠
2214 + <Child> ⚠
2215 + `);
2216 +
2217 + selectNextErrorOrWarning();
2218 + expect(state).toMatchInlineSnapshot(`
2219 + ✕ 0, ⚠ 2
2220 + [root]
2221 + <Child> ⚠
2222 + → <Child> ⚠
2223 + `);
2224 +
2225 + utils.act(() => {
2226 + store.componentFilters = [];
2227 + });
2228 + expect(state).toMatchInlineSnapshot(`
2229 + ✕ 0, ⚠ 2
2230 + [root]
2231 + ▾ <Wrapper>
2232 + <Child> ⚠
2233 + ▾ <Wrapper>
2234 + ▾ <Wrapper>
2235 + → <Child> ⚠
2236 + `);
2237 +
2238 + selectPreviousErrorOrWarning();
2239 + expect(state).toMatchInlineSnapshot(`
2240 + ✕ 0, ⚠ 2
2241 + [root]
2242 + ▾ <Wrapper>
2243 + → <Child> ⚠
2244 + ▾ <Wrapper>
2245 + ▾ <Wrapper>
2246 + <Child> ⚠
2247 + `);
2248 + });
2249 +
2250 + it('should preserve errors for fibers even if they are filtered out of the tree initially', () => {
2251 + const Wrapper = ({children}) => children;
2252 +
2253 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2254 + utils.act(() =>
2255 + ReactDOM.render(
2256 + <React.Fragment>
2257 + <Wrapper>
2258 + <Child logWarning={true} />
2259 + </Wrapper>
2260 + <Wrapper>
2261 + <Wrapper>
2262 + <Child logWarning={true} />
2263 + </Wrapper>
2264 + </Wrapper>
2265 + </React.Fragment>,
2266 + document.createElement('div'),
2267 + ),
2268 + ),
2269 + );
2270 +
2271 + store.componentFilters = [utils.createDisplayNameFilter('Child')];
2272 +
2273 + utils.act(() => TestRenderer.create(<Contexts />));
2274 + expect(state).toMatchInlineSnapshot(`
2275 + [root]
2276 + <Wrapper>
2277 + ▾ <Wrapper>
2278 + <Wrapper>
2279 + `);
2280 +
2281 + utils.act(() => {
2282 + store.componentFilters = [];
2283 + });
2284 + expect(state).toMatchInlineSnapshot(`
2285 + ✕ 0, ⚠ 2
2286 + [root]
2287 + ▾ <Wrapper>
2288 + <Child> ⚠
2289 + ▾ <Wrapper>
2290 + ▾ <Wrapper>
2291 + <Child> ⚠
2292 + `);
2293 +
2294 + selectNextErrorOrWarning();
2295 + expect(state).toMatchInlineSnapshot(`
2296 + ✕ 0, ⚠ 2
2297 + [root]
2298 + ▾ <Wrapper>
2299 + → <Child> ⚠
2300 + ▾ <Wrapper>
2301 + ▾ <Wrapper>
2302 + <Child> ⚠
2303 + `);
2304 + });
2305 +
2306 + describe('suspense', () => {
2307 + // This verifies that we don't flush before the tree has been committed.
2308 + it('should properly handle errors/warnings from components inside of delayed Suspense', async () => {
2309 + const NeverResolves = React.lazy(() => new Promise(() => {}));
2310 +
2311 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2312 + utils.act(() =>
2313 + ReactDOM.render(
2314 + <React.Suspense fallback={null}>
2315 + <Child logWarning={true} />
2316 + <NeverResolves />
2317 + </React.Suspense>,
2318 + document.createElement('div'),
2319 + ),
2320 + ),
2321 + );
2322 + utils.act(() => TestRenderer.create(<Contexts />));
2323 +
2324 + jest.runAllTimers();
2325 +
2326 + expect(state).toMatchInlineSnapshot(`
2327 + [root]
2328 + <Suspense>
2329 + `);
2330 +
2331 + selectNextErrorOrWarning();
2332 +
2333 + expect(state).toMatchInlineSnapshot(`
2334 + [root]
2335 + <Suspense>
2336 + `);
2337 + });
2338 +
2339 + it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
2340 + async function fakeImport(result) {
2341 + return {default: result};
2342 + }
2343 + const LazyComponent = React.lazy(() => fakeImport(Child));
2344 +
2345 + const container = document.createElement('div');
2346 +
2347 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2348 + utils.act(() =>
2349 + ReactDOM.render(
2350 + <React.Suspense fallback={null}>
2351 + <Child logWarning={true} />
2352 + <LazyComponent />
2353 + </React.Suspense>,
2354 + container,
2355 + ),
2356 + ),
2357 + );
2358 + utils.act(() => TestRenderer.create(<Contexts />));
2359 +
2360 + expect(state).toMatchInlineSnapshot(`
2361 + [root]
2362 + <Suspense>
2363 + `);
2364 +
2365 + await Promise.resolve();
2366 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2367 + utils.act(() =>
2368 + ReactDOM.render(
2369 + <React.Suspense fallback={null}>
2370 + <Child logWarning={true} />
2371 + <LazyComponent />
2372 + </React.Suspense>,
2373 + container,
2374 + ),
2375 + ),
2376 + );
2377 +
2378 + expect(state).toMatchInlineSnapshot(`
2379 + ✕ 0, ⚠ 1
2380 + [root]
2381 + ▾ <Suspense>
2382 + <Child> ⚠
2383 + <Child>
2384 + `);
2385 + });
2386 +
2387 + it('should properly show errors/warnings from components in the Suspense fallback tree', async () => {
2388 + async function fakeImport(result) {
2389 + return {default: result};
2390 + }
2391 + const LazyComponent = React.lazy(() => fakeImport(Child));
2392 +
2393 + const Fallback = () => <Child logError={true} />;
2394 +
2395 + const container = document.createElement('div');
2396 +
2397 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2398 + utils.act(() =>
2399 + ReactDOM.render(
2400 + <React.Suspense fallback={<Fallback />}>
2401 + <LazyComponent />
2402 + </React.Suspense>,
2403 + container,
2404 + ),
2405 + ),
2406 + );
2407 + utils.act(() => TestRenderer.create(<Contexts />));
2408 +
2409 + expect(state).toMatchInlineSnapshot(`
2410 + ✕ 1, ⚠ 0
2411 + [root]
2412 + ▾ <Suspense>
2413 + ▾ <Fallback>
2414 + <Child> ✕
2415 + `);
2416 +
2417 + await Promise.resolve();
2418 + withErrorsOrWarningsIgnored(['test-only:'], () =>
2419 + utils.act(() =>
2420 + ReactDOM.render(
2421 + <React.Suspense fallback={<Fallback />}>
2422 + <LazyComponent />
2423 + </React.Suspense>,
2424 + container,
2425 + ),
2426 + ),
2427 + );
2428 +
2429 + expect(state).toMatchInlineSnapshot(`
2430 + [root]
2431 + ▾ <Suspense>
2432 + <Child>
2433 + `);
2434 + });
2435 + });
2436 +
2437 + describe('error boundaries', () => {
2438 + it('should properly handle errors/warnings from components that dont mount because of an error', () => {
2439 + class ErrorBoundary extends React.Component {
2440 + state = {error: null};
2441 + static getDerivedStateFromError(error) {
2442 + return {error};
2443 + }
2444 + render() {
2445 + if (this.state.error) {
2446 + return null;
2447 + }
2448 + return this.props.children;
2449 + }
2450 + }
2451 +
2452 + class BadRender extends React.Component {
2453 + render() {
2454 + console.error('test-only: I am about to throw!');
2455 + throw new Error('test-only: Oops!');
2456 + }
2457 + }
2458 +
2459 + const container = document.createElement('div');
2460 + withErrorsOrWarningsIgnored(
2461 + ['test-only:', 'React will try to recreate this component tree'],
2462 + () => {
2463 + utils.act(() =>
2464 + ReactDOM.render(
2465 + <ErrorBoundary>
2466 + <BadRender />
2467 + </ErrorBoundary>,
2468 + container,
2469 + ),
2470 + );
2471 + },
2472 + );
2473 +
2474 + utils.act(() => TestRenderer.create(<Contexts />));
2475 +
2476 + expect(store).toMatchInlineSnapshot(`
2477 + ✕ 1, ⚠ 0
2478 + [root]
2479 + <ErrorBoundary> ✕
2480 + `);
2481 +
2482 + selectNextErrorOrWarning();
2483 + expect(state).toMatchInlineSnapshot(`
2484 + ✕ 1, ⚠ 0
2485 + [root]
2486 + → <ErrorBoundary> ✕
2487 + `);
2488 +
2489 + utils.act(() => ReactDOM.unmountComponentAtNode(container));
2490 + expect(state).toMatchInlineSnapshot(``);
2491 +
2492 + // Should be a noop
2493 + selectNextErrorOrWarning();
2494 + expect(state).toMatchInlineSnapshot(``);
2495 + });
2496 +
2497 + it('should properly handle errors/warnings from components that dont mount because of an error', () => {
2498 + class ErrorBoundary extends React.Component {
2499 + state = {error: null};
2500 + static getDerivedStateFromError(error) {
2501 + return {error};
2502 + }
2503 + render() {
2504 + if (this.state.error) {
2505 + return null;
2506 + }
2507 + return this.props.children;
2508 + }
2509 + }
2510 +
2511 + class LogsWarning extends React.Component {
2512 + render() {
2513 + console.warn('test-only: I am about to throw!');
2514 + return <ThrowsError />;
2515 + }
2516 + }
2517 + class ThrowsError extends React.Component {
2518 + render() {
2519 + throw new Error('test-only: Oops!');
2520 + }
2521 + }
2522 +
2523 + const container = document.createElement('div');
2524 + withErrorsOrWarningsIgnored(
2525 + ['test-only:', 'React will try to recreate this component tree'],
2526 + () => {
2527 + utils.act(() =>
2528 + ReactDOM.render(
2529 + <ErrorBoundary>
2530 + <LogsWarning />
2531 + </ErrorBoundary>,
2532 + container,
2533 + ),
2534 + );
2535 + },
2536 + );
2537 +
2538 + utils.act(() => TestRenderer.create(<Contexts />));
2539 +
2540 + expect(store).toMatchInlineSnapshot(`
2541 + ✕ 1, ⚠ 0
2542 + [root]
2543 + <ErrorBoundary> ✕
2544 + `);
2545 +
2546 + selectNextErrorOrWarning();
2547 + expect(state).toMatchInlineSnapshot(`
2548 + ✕ 1, ⚠ 0
2549 + [root]
2550 + → <ErrorBoundary> ✕
2551 + `);
2552 +
2553 + utils.act(() => ReactDOM.unmountComponentAtNode(container));
2554 + expect(state).toMatchInlineSnapshot(``);
2555 +
2556 + // Should be a noop
2557 + selectNextErrorOrWarning();
2558 + expect(state).toMatchInlineSnapshot(``);
2559 + });
2560 +
2561 + it('should properly handle errors/warnings from inside of an error boundary', () => {
2562 + class ErrorBoundary extends React.Component {
2563 + state = {error: null};
2564 + static getDerivedStateFromError(error) {
2565 + return {error};
2566 + }
2567 + render() {
2568 + if (this.state.error) {
2569 + return <Child logError={true} />;
2570 + }
2571 + return this.props.children;
2572 + }
2573 + }
2574 +
2575 + class BadRender extends React.Component {
2576 + render() {
2577 + console.error('test-only: I am about to throw!');
2578 + throw new Error('test-only: Oops!');
2579 + }
2580 + }
2581 +
2582 + const container = document.createElement('div');
2583 + withErrorsOrWarningsIgnored(
2584 + ['test-only:', 'React will try to recreate this component tree'],
2585 + () => {
2586 + utils.act(() =>
2587 + ReactDOM.render(
2588 + <ErrorBoundary>
2589 + <BadRender />
2590 + </ErrorBoundary>,
2591 + container,
2592 + ),
2593 + );
2594 + },
2595 + );
2596 +
2597 + utils.act(() => TestRenderer.create(<Contexts />));
2598 +
2599 + expect(store).toMatchInlineSnapshot(`
2600 + ✕ 2, ⚠ 0
2601 + [root]
2602 + ▾ <ErrorBoundary> ✕
2603 + <Child> ✕
2604 + `);
2605 +
2606 + selectNextErrorOrWarning();
2607 + expect(state).toMatchInlineSnapshot(`
2608 + ✕ 2, ⚠ 0
2609 + [root]
2610 + → ▾ <ErrorBoundary> ✕
2611 + <Child> ✕
2612 + `);
2613 + });
2614 });
2615 });
2616 });
packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js new
+24
@@ -0,0 +1,24 @@
1 +import {printStore} from 'react-devtools-shared/src/devtools/utils';
2 +
3 +// test() is part of Jest's serializer API
4 +export function test(maybeState) {
5 + if (maybeState === null || typeof maybeState !== 'object') {
6 + return false;
7 + }
8 +
9 + // Duck typing at its finest.
10 + return (
11 + maybeState.hasOwnProperty('inspectedElementID') &&
12 + maybeState.hasOwnProperty('ownerFlatTree') &&
13 + maybeState.hasOwnProperty('ownerSubtreeLeafElementID')
14 + );
15 +}
16 +
17 +// print() is part of Jest's serializer API
18 +export function print(state, serialize, indent) {
19 + // This is a big of a hack but it works around having to pass in a meta object e.g. {store, state}.
20 + // DevTools tests depend on a global Store object anyway (initialized via setupTest).
21 + const store = global.store;
22 +
23 + return printStore(store, false, state);
24 +}
packages/react-devtools-shared/src/__tests__/utils.js
+34
@@ -213,3 +213,37 @@ export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
213 profilerStore.profilingData = profilingDataFrontend;
214 });
215 }
216 +
217 +/**
218 + * Runs `fn` while preventing console error and warnings that partially match any given `errorOrWarningMessages` from appearing in the console.
219 + * @param errorOrWarningMessages Messages are matched partially (i.e. indexOf), pre-formatting.
220 + * @param fn
221 + */
222 +export function withErrorsOrWarningsIgnored<T: void | Promise<void>>(
223 + errorOrWarningMessages: string[],
224 + fn: () => T,
225 +): T {
226 + let resetIgnoredErrorOrWarningMessages = true;
227 + try {
228 + global._ignoredErrorOrWarningMessages = errorOrWarningMessages;
229 + const maybeThenable = fn();
230 + if (
231 + maybeThenable !== undefined &&
232 + typeof maybeThenable.then === 'function'
233 + ) {
234 + resetIgnoredErrorOrWarningMessages = false;
235 + return maybeThenable.then(
236 + () => {
237 + global._ignoredErrorOrWarningMessages = [];
238 + },
239 + () => {
240 + global._ignoredErrorOrWarningMessages = [];
241 + },
242 + );
243 + }
244 + } finally {
245 + if (resetIgnoredErrorOrWarningMessages) {
246 + global._ignoredErrorOrWarningMessages = [];
247 + }
248 + }
249 +}
packages/react-devtools-shared/src/backend/agent.js
+37 -1
@@ -169,6 +169,9 @@ export default class Agent extends EventEmitter<{|
169
170 this._bridge = bridge;
171
172 + bridge.addListener('clearErrorsAndWarnings', this.clearErrorsAndWarnings);
173 + bridge.addListener('clearErrorsForFiberID', this.clearErrorsForFiberID);
174 + bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
175 bridge.addListener('copyElementPath', this.copyElementPath);
176 bridge.addListener('deletePath', this.deletePath);
177 bridge.addListener('getProfilingData', this.getProfilingData);
@@ -226,6 +229,33 @@ export default class Agent extends EventEmitter<{|
229 return this._rendererInterfaces;
230 }
231
232 + clearErrorsAndWarnings = ({rendererID}: {|rendererID: RendererID|}) => {
233 + const renderer = this._rendererInterfaces[rendererID];
234 + if (renderer == null) {
235 + console.warn(`Invalid renderer id "${rendererID}"`);
236 + } else {
237 + renderer.clearErrorsAndWarnings();
238 + }
239 + };
240 +
241 + clearErrorsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
242 + const renderer = this._rendererInterfaces[rendererID];
243 + if (renderer == null) {
244 + console.warn(`Invalid renderer id "${rendererID}"`);
245 + } else {
246 + renderer.clearErrorsForFiberID(id);
247 + }
248 + };
249 +
250 + clearWarningsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
251 + const renderer = this._rendererInterfaces[rendererID];
252 + if (renderer == null) {
253 + console.warn(`Invalid renderer id "${rendererID}"`);
254 + } else {
255 + renderer.clearWarningsForFiberID(id);
256 + }
257 + };
258 +
259 copyElementPath = ({id, path, rendererID}: CopyElementParams) => {
260 const renderer = this._rendererInterfaces[rendererID];
261 if (renderer == null) {
@@ -571,16 +601,22 @@ export default class Agent extends EventEmitter<{|
601 updateConsolePatchSettings = ({
602 appendComponentStack,
603 breakOnConsoleErrors,
604 + showInlineWarningsAndErrors,
605 }: {|
606 appendComponentStack: boolean,
607 breakOnConsoleErrors: boolean,
608 + showInlineWarningsAndErrors: boolean,
609 |}) => {
610 // If the frontend preference has change,
611 // or in the case of React Native- if the backend is just finding out the preference-
612 // then install or uninstall the console overrides.
613 // It's safe to call these methods multiple times, so we don't need to worry about that.
614 if (appendComponentStack || breakOnConsoleErrors) {
583 - patchConsole({appendComponentStack, breakOnConsoleErrors});
615 + patchConsole({
616 + appendComponentStack,
617 + breakOnConsoleErrors,
618 + showInlineWarningsAndErrors,
619 + });
620 } else {
621 unpatchConsole();
622 }
packages/react-devtools-shared/src/backend/console.js
+71 -31
@@ -22,11 +22,22 @@ const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
22 // but we can fallback to looking for location info (e.g. "foo.js:12:345")
23 const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
24
25 +export function isStringComponentStack(text: string): boolean {
26 + return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
27 +}
28 +
29 +type OnErrorOrWarning = (
30 + fiber: Fiber,
31 + type: 'error' | 'warn',
32 + args: Array<any>,
33 +) => void;
34 +
35 const injectedRenderers: Map<
36 ReactRenderer,
37 {|
38 currentDispatcherRef: CurrentDispatcherRef,
39 getCurrentFiber: () => Fiber | null,
40 + onErrorOrWarning: ?OnErrorOrWarning,
41 workTagMap: WorkTagMap,
42 |},
43 > = new Map();
@@ -54,7 +65,10 @@ export function dangerous_setTargetConsoleForTesting(
65 // v16 renderers should use this method to inject internals necessary to generate a component stack.
66 // These internals will be used if the console is patched.
67 // Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
57 -export function registerRenderer(renderer: ReactRenderer): void {
68 +export function registerRenderer(
69 + renderer: ReactRenderer,
70 + onErrorOrWarning?: OnErrorOrWarning,
71 +): void {
72 const {
73 currentDispatcherRef,
74 getCurrentFiber,
@@ -76,6 +90,7 @@ export function registerRenderer(renderer: ReactRenderer): void {
90 currentDispatcherRef,
91 getCurrentFiber,
92 workTagMap: ReactTypeOfWork,
93 + onErrorOrWarning,
94 });
95 }
96 }
@@ -83,6 +98,7 @@ export function registerRenderer(renderer: ReactRenderer): void {
98 const consoleSettingsRef = {
99 appendComponentStack: false,
100 breakOnConsoleErrors: false,
101 + showInlineWarningsAndErrors: false,
102 };
103
104 // Patches console methods to append component stack for the current fiber.
@@ -90,14 +106,17 @@ const consoleSettingsRef = {
106 export function patch({
107 appendComponentStack,
108 breakOnConsoleErrors,
109 + showInlineWarningsAndErrors,
110 }: {
111 appendComponentStack: boolean,
112 breakOnConsoleErrors: boolean,
113 + showInlineWarningsAndErrors: boolean,
114 }): void {
115 // Settings may change after we've patched the console.
116 // Using a shared ref allows the patch function to read the latest values.
117 consoleSettingsRef.appendComponentStack = appendComponentStack;
118 consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
119 + consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
120
121 if (unpatchFn !== null) {
122 // Don't patch twice.
@@ -121,32 +140,51 @@ export function patch({
140 targetConsole[method]);
141
142 const overrideMethod = (...args) => {
124 - const latestAppendComponentStack =
125 - consoleSettingsRef.appendComponentStack;
126 - const latestBreakOnConsoleErrors =
127 - consoleSettingsRef.breakOnConsoleErrors;
128 -
129 - if (latestAppendComponentStack) {
130 - try {
131 - // If we are ever called with a string that already has a component stack, e.g. a React error/warning,
132 - // don't append a second stack.
133 - const lastArg = args.length > 0 ? args[args.length - 1] : null;
134 - const alreadyHasComponentStack =
135 - lastArg !== null &&
136 - (PREFIX_REGEX.test(lastArg) ||
137 - ROW_COLUMN_NUMBER_REGEX.test(lastArg));
138 -
139 - if (!alreadyHasComponentStack) {
140 - // If there's a component stack for at least one of the injected renderers, append it.
141 - // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
142 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
143 - for (const {
144 - currentDispatcherRef,
145 - getCurrentFiber,
146 - workTagMap,
147 - } of injectedRenderers.values()) {
148 - const current: ?Fiber = getCurrentFiber();
149 - if (current != null) {
143 + const lastArg = args.length > 0 ? args[args.length - 1] : null;
144 + const alreadyHasComponentStack =
145 + lastArg !== null && isStringComponentStack(lastArg);
146 +
147 + let shouldAppendWarningStack = false;
148 + if (consoleSettingsRef.appendComponentStack) {
149 + // If we are ever called with a string that already has a component stack,
150 + // e.g. a React error/warning, don't append a second stack.
151 + shouldAppendWarningStack = !alreadyHasComponentStack;
152 + }
153 +
154 + const shouldShowInlineWarningsAndErrors =
155 + consoleSettingsRef.showInlineWarningsAndErrors &&
156 + (method === 'error' || method === 'warn');
157 +
158 + if (shouldAppendWarningStack || shouldShowInlineWarningsAndErrors) {
159 + // Search for the first renderer that has a current Fiber.
160 + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
161 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
162 + for (const {
163 + currentDispatcherRef,
164 + getCurrentFiber,
165 + onErrorOrWarning,
166 + workTagMap,
167 + } of injectedRenderers.values()) {
168 + const current: ?Fiber = getCurrentFiber();
169 + if (current != null) {
170 + try {
171 + if (shouldShowInlineWarningsAndErrors) {
172 + // patch() is called by two places: (1) the hook and (2) the renderer backend.
173 + // The backend is what impliments a message queue, so it's the only one that injects onErrorOrWarning.
174 + if (typeof onErrorOrWarning === 'function') {
175 + onErrorOrWarning(
176 + current,
177 + ((method: any): 'error' | 'warn'),
178 + // Copy args before we mutate them (e.g. adding the component stack)
179 + alreadyHasComponentStack
180 + ? // Replace component stack with an empty string in case there's a string placeholder for it.
181 + [...args.slice(0, -1), '']
182 + : args.slice(),
183 + );
184 + }
185 + }
186 +
187 + if (shouldAppendWarningStack) {
188 const componentStack = getStackByFiberInDevAndProd(
189 workTagMap,
190 current,
@@ -155,16 +193,17 @@ export function patch({
193 if (componentStack !== '') {
194 args.push(componentStack);
195 }
158 - break;
196 }
197 + } catch (error) {
198 + // Don't let a DevTools or React internal error interfere with logging.
199 + } finally {
200 + break;
201 }
202 }
162 - } catch (error) {
163 - // Don't let a DevTools or React internal error interfere with logging.
203 }
204 }
205
167 - if (latestBreakOnConsoleErrors) {
206 + if (consoleSettingsRef.breakOnConsoleErrors) {
207 // --- Welcome to debugging with React DevTools ---
208 // This debugger statement means that you've enabled the "break on warnings" feature.
209 // Use the browser's Call Stack panel to step out of this override function-
@@ -177,6 +216,7 @@ export function patch({
216 };
217
218 overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
219 + originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
220
221 // $FlowFixMe property error|warn is not writable.
222 targetConsole[method] = overrideMethod;
packages/react-devtools-shared/src/backend/legacy/renderer.js
+21
@@ -779,6 +779,10 @@ export function attach(
779 state = publicInstance.state || null;
780 }
781
782 + // Not implemented
783 + const errors = [];
784 + const warnings = [];
785 +
786 return {
787 id,
788
@@ -812,6 +816,8 @@ export function attach(
816 hooks: null,
817 props,
818 state,
819 + errors,
820 + warnings,
821
822 // List of owners
823 owners,
@@ -1040,7 +1046,22 @@ export function attach(
1046 return null;
1047 }
1048
1049 + function clearErrorsAndWarnings() {
1050 + // Not implemented
1051 + }
1052 +
1053 + function clearErrorsForFiberID(id: number) {
1054 + // Not implemented
1055 + }
1056 +
1057 + function clearWarningsForFiberID(id: number) {
1058 + // Not implemented
1059 + }
1060 +
1061 return {
1062 + clearErrorsAndWarnings,
1063 + clearErrorsForFiberID,
1064 + clearWarningsForFiberID,
1065 cleanup,
1066 copyElementPath,
1067 deletePath,
packages/react-devtools-shared/src/backend/renderer.js
+200 -4
@@ -49,7 +49,9 @@ import {
49 SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
50 TREE_OPERATION_ADD,
51 TREE_OPERATION_REMOVE,
52 + TREE_OPERATION_REMOVE_ROOT,
53 TREE_OPERATION_REORDER_CHILDREN,
54 + TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
55 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
56 } from '../constants';
57 import {inspectHooksOfFiber} from 'react-debug-tools';
@@ -76,6 +78,7 @@ import {
78 MEMO_NUMBER,
79 MEMO_SYMBOL_STRING,
80 } from './ReactSymbols';
81 +import {format} from './utils';
82
83 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
84 import type {
@@ -117,6 +120,7 @@ type ReactTypeOfSideEffectType = {|
120 NoFlags: number,
121 PerformedWork: number,
122 Placement: number,
123 + Incomplete: number,
124 |};
125
126 function getFiberFlags(fiber: Fiber): number {
@@ -143,6 +147,7 @@ export function getInternalReactConstants(
147 NoFlags: 0b00,
148 PerformedWork: 0b01,
149 Placement: 0b10,
150 + Incomplete: 0b10000000000000,
151 };
152
153 // **********************************************************
@@ -479,7 +484,7 @@ export function attach(
484 ReactTypeOfWork,
485 ReactTypeOfSideEffect,
486 } = getInternalReactConstants(renderer.version);
482 - const {NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
487 + const {Incomplete, NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
488 const {
489 FunctionComponent,
490 ClassComponent,
@@ -522,13 +527,104 @@ export function attach(
527 typeof setSuspenseHandler === 'function' &&
528 typeof scheduleUpdate === 'function';
529
530 + // Set of Fibers (IDs) with recently changed number of error/warning messages.
531 + const fibersWithChangedErrorOrWarningCounts: Set<number> = new Set();
532 +
533 + // Mapping of fiber IDs to error/warning messages and counts.
534 + const fiberToErrorsMap: Map<number, Map<string, number>> = new Map();
535 + const fiberToWarningsMap: Map<number, Map<string, number>> = new Map();
536 +
537 + function clearErrorsAndWarnings() {
538 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
539 + for (const id of fiberToErrorsMap.keys()) {
540 + fibersWithChangedErrorOrWarningCounts.add(id);
541 + updateMostRecentlyInspectedElementIfNecessary(id);
542 + }
543 +
544 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
545 + for (const id of fiberToWarningsMap.keys()) {
546 + fibersWithChangedErrorOrWarningCounts.add(id);
547 + updateMostRecentlyInspectedElementIfNecessary(id);
548 + }
549 +
550 + fiberToErrorsMap.clear();
551 + fiberToWarningsMap.clear();
552 +
553 + flushPendingEvents();
554 + }
555 +
556 + function clearErrorsForFiberID(id: number) {
557 + if (fiberToErrorsMap.has(id)) {
558 + fiberToErrorsMap.delete(id);
559 + fibersWithChangedErrorOrWarningCounts.add(id);
560 + flushPendingEvents();
561 + }
562 +
563 + updateMostRecentlyInspectedElementIfNecessary(id);
564 + }
565 +
566 + function clearWarningsForFiberID(id: number) {
567 + if (fiberToWarningsMap.has(id)) {
568 + fiberToWarningsMap.delete(id);
569 + fibersWithChangedErrorOrWarningCounts.add(id);
570 + flushPendingEvents();
571 + }
572 +
573 + updateMostRecentlyInspectedElementIfNecessary(id);
574 + }
575 +
576 + function updateMostRecentlyInspectedElementIfNecessary(
577 + fiberID: number,
578 + ): void {
579 + if (
580 + mostRecentlyInspectedElement !== null &&
581 + mostRecentlyInspectedElement.id === fiberID
582 + ) {
583 + hasElementUpdatedSinceLastInspected = true;
584 + }
585 + }
586 +
587 + // Called when an error or warning is logged during render, commit, or passive (including unmount functions).
588 + function onErrorOrWarning(
589 + fiber: Fiber,
590 + type: 'error' | 'warn',
591 + args: $ReadOnlyArray<any>,
592 + ): void {
593 + const message = format(...args);
594 +
595 + // Note that by calling these functions we may be creating the ID for the first time.
596 + // If the Fiber is then never mounted, we are responsible for cleaning up after ourselves.
597 + // This is important because getPrimaryFiber() stores a Fiber in the primaryFibers Set.
598 + // If a Fiber never mounts, and we don't clean up after this code, we could leak.
599 + // Fortunately we would only leak Fibers that have errors/warnings associated with them,
600 + // which is hopefully only a small set and only in DEV mode– but this is still not great.
601 + // We should clean up Fibers like this when flushing; see recordPendingErrorsAndWarnings().
602 + const fiberID = getFiberID(getPrimaryFiber(fiber));
603 +
604 + // Mark this Fiber as needed its warning/error count updated during the next flush.
605 + fibersWithChangedErrorOrWarningCounts.add(fiberID);
606 +
607 + // Update the error/warning messages and counts for the Fiber.
608 + const fiberMap = type === 'error' ? fiberToErrorsMap : fiberToWarningsMap;
609 + const messageMap = fiberMap.get(fiberID);
610 + if (messageMap != null) {
611 + const count = messageMap.get(message) || 0;
612 + messageMap.set(message, count + 1);
613 + } else {
614 + fiberMap.set(fiberID, new Map([[message, 1]]));
615 + }
616 +
617 + // If this Fiber is currently being inspected, mark it as needing an udpate as well.
618 + updateMostRecentlyInspectedElementIfNecessary(fiberID);
619 + }
620 +
621 // Patching the console enables DevTools to do a few useful things:
622 // * Append component stacks to warnings and error messages
623 // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
624 //
625 // Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
626 if (process.env.NODE_ENV !== 'test') {
531 - registerRendererWithConsole(renderer);
627 + registerRendererWithConsole(renderer, onErrorOrWarning);
628
629 // The renderer interface can't read these preferences directly,
630 // because it is stored in localStorage within the context of the extension.
@@ -537,10 +633,13 @@ export function attach(
633 window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
634 const breakOnConsoleErrors =
635 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
636 + const showInlineWarningsAndErrors =
637 + window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ === true;
638 if (appendComponentStack || breakOnConsoleErrors) {
639 patchConsole({
640 appendComponentStack,
641 breakOnConsoleErrors,
642 + showInlineWarningsAndErrors,
643 });
644 }
645 }
@@ -652,8 +751,11 @@ export function attach(
751 // Recursively unmount all roots.
752 hook.getFiberRoots(rendererID).forEach(root => {
753 currentRootID = getFiberID(getPrimaryFiber(root.current));
655 - unmountFiberChildrenRecursively(root.current);
656 - recordUnmount(root.current, false);
754 + // The TREE_OPERATION_REMOVE_ROOT operation serves two purposes:
755 + // 1. It avoids sending unnecessary bridge traffic to clear a root.
756 + // 2. It preserves Fiber IDs when remounting (below) which in turn ID to error/warning mapping.
757 + pushOperation(TREE_OPERATION_REMOVE_ROOT);
758 + flushPendingEvents(root);
759 currentRootID = -1;
760 });
761
@@ -670,6 +772,10 @@ export function attach(
772 flushPendingEvents(root);
773 currentRootID = -1;
774 });
775 +
776 + // Also re-evaluate all error and warning counts given the new filters.
777 + reevaluateErrorsAndWarnings();
778 + flushPendingEvents();
779 }
780
781 // NOTICE Keep in sync with get*ForFiber methods
@@ -1055,7 +1161,64 @@ export function attach(
1161 pendingOperations.push(op);
1162 }
1163
1164 + function reevaluateErrorsAndWarnings() {
1165 + fibersWithChangedErrorOrWarningCounts.clear();
1166 + fiberToErrorsMap.forEach((countMap, fiberID) => {
1167 + fibersWithChangedErrorOrWarningCounts.add(fiberID);
1168 + });
1169 + fiberToWarningsMap.forEach((countMap, fiberID) => {
1170 + fibersWithChangedErrorOrWarningCounts.add(fiberID);
1171 + });
1172 + recordPendingErrorsAndWarnings();
1173 + }
1174 +
1175 + function recordPendingErrorsAndWarnings() {
1176 + fibersWithChangedErrorOrWarningCounts.forEach(fiberID => {
1177 + const fiber = idToFiberMap.get(fiberID);
1178 + if (fiber != null) {
1179 + // Don't send updates for Fibers that didn't mount due to e.g. Suspense or an error boundary.
1180 + // We may also need to clean up after ourselves to avoid leaks.
1181 + // See inline comments in onErrorOrWarning() for more info.
1182 + if (isFiberMountedImpl(fiber) !== MOUNTED) {
1183 + fiberToIDMap.delete(fiber);
1184 + idToFiberMap.delete(fiberID);
1185 + primaryFibers.delete(fiber);
1186 + return;
1187 + }
1188 +
1189 + let errorCount = 0;
1190 + let warningCount = 0;
1191 +
1192 + if (!shouldFilterFiber(fiber)) {
1193 + const errorCountsMap = fiberToErrorsMap.get(fiberID);
1194 + const warningCountsMap = fiberToWarningsMap.get(fiberID);
1195 +
1196 + if (errorCountsMap != null) {
1197 + errorCountsMap.forEach(count => {
1198 + errorCount += count;
1199 + });
1200 + }
1201 + if (warningCountsMap != null) {
1202 + warningCountsMap.forEach(count => {
1203 + warningCount += count;
1204 + });
1205 + }
1206 + }
1207 +
1208 + pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
1209 + pushOperation(fiberID);
1210 + pushOperation(errorCount);
1211 + pushOperation(warningCount);
1212 + }
1213 + });
1214 + fibersWithChangedErrorOrWarningCounts.clear();
1215 + }
1216 +
1217 function flushPendingEvents(root: Object): void {
1218 + // Add any pending errors and warnings to the operations array.
1219 + // We do this just before flushing, so we can ignore errors for no-longer-mounted Fibers.
1220 + recordPendingErrorsAndWarnings();
1221 +
1222 if (
1223 pendingOperations.length === 0 &&
1224 pendingRealUnmountedIDs.length === 0 &&
@@ -2028,17 +2191,42 @@ export function attach(
2191 // https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberTreeReflection.js
2192 function isFiberMountedImpl(fiber: Fiber): number {
2193 let node = fiber;
2194 + let prevNode = null;
2195 if (!fiber.alternate) {
2196 // If there is no alternate, this might be a new tree that isn't inserted
2197 // yet. If it is, then it will have a pending insertion effect on it.
2198 if ((getFiberFlags(node) & Placement) !== NoFlags) {
2199 return MOUNTING;
2200 }
2201 + // This indicates an error during render.
2202 + if ((getFiberFlags(node) & Incomplete) !== NoFlags) {
2203 + return UNMOUNTED;
2204 + }
2205 while (node.return) {
2206 + prevNode = node;
2207 node = node.return;
2208 +
2209 if ((getFiberFlags(node) & Placement) !== NoFlags) {
2210 return MOUNTING;
2211 }
2212 + // This indicates an error during render.
2213 + if ((getFiberFlags(node) & Incomplete) !== NoFlags) {
2214 + return UNMOUNTED;
2215 + }
2216 +
2217 + // If this node is inside of a timed out suspense subtree, we should also ignore errors/warnings.
2218 + const isTimedOutSuspense =
2219 + node.tag === SuspenseComponent && node.memoizedState !== null;
2220 + if (isTimedOutSuspense) {
2221 + // Note that this does not include errors/warnings in the Fallback tree though!
2222 + const primaryChildFragment = node.child;
2223 + const fallbackChildFragment = primaryChildFragment
2224 + ? primaryChildFragment.sibling
2225 + : null;
2226 + if (prevNode !== fallbackChildFragment) {
2227 + return UNMOUNTED;
2228 + }
2229 + }
2230 }
2231 } else {
2232 while (node.return) {
@@ -2455,6 +2643,9 @@ export function attach(
2643 rootType = fiberRoot._debugRootType;
2644 }
2645
2646 + const errors = fiberToErrorsMap.get(id) || new Map();
2647 + const warnings = fiberToWarningsMap.get(id) || new Map();
2648 +
2649 return {
2650 id,
2651
@@ -2497,6 +2688,8 @@ export function attach(
2688 hooks,
2689 props: memoizedProps,
2690 state: usesHooks ? null : memoizedState,
2691 + errors: Array.from(errors.entries()),
2692 + warnings: Array.from(warnings.entries()),
2693
2694 // List of owners
2695 owners,
@@ -3425,6 +3618,9 @@ export function attach(
3618
3619 return {
3620 cleanup,
3621 + clearErrorsAndWarnings,
3622 + clearErrorsForFiberID,
3623 + clearWarningsForFiberID,
3624 copyElementPath,
3625 deletePath,
3626 findNativeNodesForFiberID,
packages/react-devtools-shared/src/backend/types.js
+5
@@ -234,6 +234,8 @@ export type InspectedElement = {|
234 props: Object | null,
235 state: Object | null,
236 key: number | string | null,
237 + errors: Array<[string, number]>,
238 + warnings: Array<[string, number]>,
239
240 // List of owners
241 owners: Array<Owner> | null,
@@ -294,6 +296,9 @@ type Type = 'props' | 'hooks' | 'state' | 'context';
296
297 export type RendererInterface = {
298 cleanup: () => void,
299 + clearErrorsAndWarnings: () => void,
300 + clearErrorsForFiberID: (id: number) => void,
301 + clearWarningsForFiberID: (id: number) => void,
302 copyElementPath: (id: number, path: Array<string | number>) => void,
303 deletePath: (
304 type: Type,
packages/react-devtools-shared/src/backend/utils.js
+49
@@ -134,3 +134,52 @@ export function serializeToString(data: any): string {
134 return value;
135 });
136 }
137 +
138 +// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
139 +// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
140 +// Implements s, d, i and f placeholders
141 +export function format(
142 + maybeMessage: any,
143 + ...inputArgs: $ReadOnlyArray<any>
144 +): string {
145 + if (typeof maybeMessage !== 'string') {
146 + return [maybeMessage, ...inputArgs].join(' ');
147 + }
148 +
149 + const re = /(%?)(%([jds]))/g;
150 + const args = inputArgs.slice();
151 + let formatted: string = maybeMessage;
152 +
153 + if (args.length) {
154 + formatted = formatted.replace(re, (match, escaped, ptn, flag) => {
155 + let arg = args.shift();
156 + switch (flag) {
157 + case 's':
158 + arg += '';
159 + break;
160 + case 'd':
161 + case 'i':
162 + arg = parseInt(arg, 10).toString();
163 + break;
164 + case 'f':
165 + arg = parseFloat(arg).toString();
166 + break;
167 + }
168 + if (!escaped) {
169 + return arg;
170 + }
171 + args.unshift(arg);
172 + return match;
173 + });
174 + }
175 +
176 + // arguments remain after formatting
177 + if (args.length) {
178 + formatted += ' ' + args.join(' ');
179 + }
180 +
181 + // update escaped %% values
182 + formatted = formatted.replace(/%{2,2}/g, '%');
183 +
184 + return '' + formatted;
185 +}
packages/react-devtools-shared/src/bridge.js
+4
@@ -114,6 +114,7 @@ type NativeStyleEditor_SetValueParams = {|
114 type UpdateConsolePatchSettingsParams = {|
115 appendComponentStack: boolean,
116 breakOnConsoleErrors: boolean,
117 + showInlineWarningsAndErrors: boolean,
118 |};
119
120 type BackendEvents = {|
@@ -141,7 +142,10 @@ type BackendEvents = {|
142 |};
143
144 type FrontendEvents = {|
145 + clearErrorsAndWarnings: [{|rendererID: RendererID|}],
146 + clearErrorsForFiberID: [ElementAndRendererID],
147 clearNativeElementHighlight: [],
148 + clearWarningsForFiberID: [ElementAndRendererID],
149 copyElementPath: [CopyElementPathParams],
150 deletePath: [DeletePath],
151 getOwnersList: [ElementAndRendererID],
packages/react-devtools-shared/src/constants.js
+5
@@ -14,6 +14,8 @@ export const TREE_OPERATION_ADD = 1;
14 export const TREE_OPERATION_REMOVE = 2;
15 export const TREE_OPERATION_REORDER_CHILDREN = 3;
16 export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
17 +export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
18 +export const TREE_OPERATION_REMOVE_ROOT = 6;
19
20 export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
21 'React::DevTools::componentFilters';
@@ -33,6 +35,9 @@ export const LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS =
35 export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
36 'React::DevTools::appendComponentStack';
37
38 +export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY =
39 + 'React::DevTools::showInlineWarningsAndErrors';
40 +
41 export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
42 'React::DevTools::traceUpdatesEnabled';
43
packages/react-devtools-shared/src/devtools/store.js
+152
@@ -12,7 +12,9 @@ import {inspect} from 'util';
12 import {
13 TREE_OPERATION_ADD,
14 TREE_OPERATION_REMOVE,
15 + TREE_OPERATION_REMOVE_ROOT,
16 TREE_OPERATION_REORDER_CHILDREN,
17 + TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
18 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
19 } from '../constants';
20 import {ElementTypeRoot} from '../types';
@@ -78,11 +80,22 @@ export default class Store extends EventEmitter<{|
80 |}> {
81 _bridge: FrontendBridge;
82
83 + // Computed whenever _errorsAndWarnings Map changes.
84 + _cachedErrorCount: number = 0;
85 + _cachedWarningCount: number = 0;
86 + _cachedErrorAndWarningTuples: Array<{|id: number, index: number|}> = [];
87 +
88 // Should new nodes be collapsed by default when added to the tree?
89 _collapseNodesByDefault: boolean = true;
90
91 _componentFilters: Array<ComponentFilter>;
92
93 + // Map of ID to number of recorded error and warning message IDs.
94 + _errorsAndWarnings: Map<
95 + number,
96 + {|errorCount: number, warningCount: number|},
97 + > = new Map();
98 +
99 // At least one of the injected renderers contains (DEV only) owner metadata.
100 _hasOwnerMetadata: boolean = false;
101
@@ -289,6 +302,10 @@ export default class Store extends EventEmitter<{|
302 this.emit('componentFilters');
303 }
304
305 + get errorCount(): number {
306 + return this._cachedErrorCount;
307 + }
308 +
309 get hasOwnerMetadata(): boolean {
310 return this._hasOwnerMetadata;
311 }
@@ -357,6 +374,46 @@ export default class Store extends EventEmitter<{|
374 return this._unsupportedRendererVersionDetected;
375 }
376
377 + get warningCount(): number {
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 +
417 containsElement(id: number): boolean {
418 return this._idToElement.get(id) != null;
419 }
@@ -425,6 +482,17 @@ export default class Store extends EventEmitter<{|
482 return element;
483 }
484
485 + // Returns a tuple of [id, index]
486 + getElementsWithErrorsAndWarnings(): Array<{|id: number, index: number|}> {
487 + return this._cachedErrorAndWarningTuples;
488 + }
489 +
490 + getErrorAndWarningCountForElementID(
491 + id: number,
492 + ): {|errorCount: number, warningCount: number|} {
493 + return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};
494 + }
495 +
496 getIndexOfElementID(id: number): number | null {
497 const element = this.getElementByID(id);
498
@@ -709,6 +777,7 @@ export default class Store extends EventEmitter<{|
777 }
778
779 let haveRootsChanged = false;
780 + let haveErrorsOrWarningsChanged = false;
781
782 // The first two values are always rendererID and rootID
783 const rendererID = operations[0];
@@ -910,7 +979,41 @@ export default class Store extends EventEmitter<{|
979 set.delete(id);
980 }
981 }
982 +
983 + if (this._errorsAndWarnings.has(id)) {
984 + this._errorsAndWarnings.delete(id);
985 + haveErrorsOrWarningsChanged = true;
986 + }
987 + }
988 + break;
989 + }
990 + case TREE_OPERATION_REMOVE_ROOT: {
991 + i += 1;
992 +
993 + const id = operations[1];
994 +
995 + if (__DEBUG__) {
996 + debug(`Remove root ${id}`);
997 }
998 +
999 + const recursivelyDeleteElements = elementID => {
1000 + const element = this._idToElement.get(elementID);
1001 + this._idToElement.delete(elementID);
1002 + if (element) {
1003 + // Mostly for Flow's sake
1004 + for (let index = 0; index < element.children.length; index++) {
1005 + recursivelyDeleteElements(element.children[index]);
1006 + }
1007 + }
1008 + };
1009 +
1010 + const root = ((this._idToElement.get(id): any): Element);
1011 + recursivelyDeleteElements(id);
1012 +
1013 + this._rootIDToCapabilities.delete(id);
1014 + this._rootIDToRendererID.delete(id);
1015 + this._roots = this._roots.filter(rootID => rootID !== id);
1016 + this._weightAcrossRoots -= root.weight;
1017 break;
1018 }
1019 case TREE_OPERATION_REORDER_CHILDREN: {
@@ -958,6 +1061,20 @@ export default class Store extends EventEmitter<{|
1061 // The profiler UI uses them lazily in order to generate the tree.
1062 i += 3;
1063 break;
1064 + case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
1065 + const id = operations[i + 1];
1066 + const errorCount = operations[i + 2];
1067 + const warningCount = operations[i + 3];
1068 +
1069 + i += 4;
1070 +
1071 + if (errorCount > 0 || warningCount > 0) {
1072 + this._errorsAndWarnings.set(id, {errorCount, warningCount});
1073 + } else if (this._errorsAndWarnings.has(id)) {
1074 + this._errorsAndWarnings.delete(id);
1075 + }
1076 + haveErrorsOrWarningsChanged = true;
1077 + break;
1078 default:
1079 throw Error(`Unsupported Bridge operation ${operation}`);
1080 }
@@ -965,6 +1082,41 @@ export default class Store extends EventEmitter<{|
1082
1083 this._revision++;
1084
1085 + if (haveErrorsOrWarningsChanged) {
1086 + let errorCount = 0;
1087 + let warningCount = 0;
1088 +
1089 + this._errorsAndWarnings.forEach(entry => {
1090 + errorCount += entry.errorCount;
1091 + warningCount += entry.warningCount;
1092 + });
1093 +
1094 + this._cachedErrorCount = errorCount;
1095 + this._cachedWarningCount = warningCount;
1096 +
1097 + const errorAndWarningTuples: Array<{|id: number, index: number|}> = [];
1098 +
1099 + this._errorsAndWarnings.forEach((_, id) => {
1100 + const index = this.getIndexOfElementID(id);
1101 + if (index !== null) {
1102 + let low = 0;
1103 + let high = errorAndWarningTuples.length;
1104 + while (low < high) {
1105 + const mid = (low + high) >> 1;
1106 + if (errorAndWarningTuples[mid].index > index) {
1107 + high = mid;
1108 + } else {
1109 + low = mid + 1;
1110 + }
1111 + }
1112 +
1113 + errorAndWarningTuples.splice(low, 0, {id, index});
1114 + }
1115 + });
1116 +
1117 + this._cachedErrorAndWarningTuples = errorAndWarningTuples;
1118 + }
1119 +
1120 if (haveRootsChanged) {
1121 const prevSupportsProfiling = this._supportsProfiling;
1122
packages/react-devtools-shared/src/devtools/utils.js
+78 -21
@@ -10,6 +10,7 @@
10 import JSON5 from 'json5';
11
12 import type {Element} from './views/Components/types';
13 +import type {StateContext} from './views/Components/TreeContext';
14 import type Store from './store';
15
16 export function printElement(element: Element, includeWeight: boolean = false) {
@@ -49,40 +50,96 @@ export function printOwnersList(
50 .join('\n');
51 }
52
52 -export function printStore(store: Store, includeWeight: boolean = false) {
53 +export function printStore(
54 + store: Store,
55 + includeWeight: boolean = false,
56 + state: StateContext | null = null,
57 +) {
58 const snapshotLines = [];
59
60 let rootWeight = 0;
61
57 - store.roots.forEach(rootID => {
58 - const {weight} = ((store.getElementByID(rootID): any): Element);
62 + function printSelectedMarker(index: number): string {
63 + if (state === null) {
64 + return '';
65 + }
66 + return state.selectedElementIndex === index ? `→` : ' ';
67 + }
68 +
69 + function printErrorsAndWarnings(element: Element): string {
70 + const {
71 + errorCount,
72 + warningCount,
73 + } = store.getErrorAndWarningCountForElementID(element.id);
74 + if (errorCount === 0 && warningCount === 0) {
75 + return '';
76 + }
77 + return ` ${errorCount > 0 ? '✕' : ''}${warningCount > 0 ? '⚠' : ''}`;
78 + }
79 +
80 + const ownerFlatTree = state !== null ? state.ownerFlatTree : null;
81 + if (ownerFlatTree !== null) {
82 + snapshotLines.push(
83 + '[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''),
84 + );
85 + ownerFlatTree.forEach((element, index) => {
86 + const printedSelectedMarker = printSelectedMarker(index);
87 + const printedElement = printElement(element, false);
88 + const printedErrorsAndWarnings = printErrorsAndWarnings(element);
89 + snapshotLines.push(
90 + `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
91 + );
92 + });
93 + } else {
94 + const errorsAndWarnings = store._errorsAndWarnings;
95 + if (errorsAndWarnings.size > 0) {
96 + let errorCount = 0;
97 + let warningCount = 0;
98 + errorsAndWarnings.forEach(entry => {
99 + errorCount += entry.errorCount;
100 + warningCount += entry.warningCount;
101 + });
102 +
103 + snapshotLines.push(`✕ ${errorCount}, ⚠ ${warningCount}`);
104 + }
105 +
106 + store.roots.forEach(rootID => {
107 + const {weight} = ((store.getElementByID(rootID): any): Element);
108 + const maybeWeightLabel = includeWeight ? ` (${weight})` : '';
109 +
110 + // Store does not (yet) expose a way to get errors/warnings per root.
111 + snapshotLines.push(`[root]${maybeWeightLabel}`);
112
60 - snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : ''));
113 + for (let i = rootWeight; i < rootWeight + weight; i++) {
114 + const element = store.getElementAtIndex(i);
115
62 - for (let i = rootWeight; i < rootWeight + weight; i++) {
63 - const element = store.getElementAtIndex(i);
116 + if (element == null) {
117 + throw Error(`Could not find element at index ${i}`);
118 + }
119
65 - if (element == null) {
66 - throw Error(`Could not find element at index ${i}`);
120 + const printedSelectedMarker = printSelectedMarker(i);
121 + const printedElement = printElement(element, includeWeight);
122 + const printedErrorsAndWarnings = printErrorsAndWarnings(element);
123 + snapshotLines.push(
124 + `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
125 + );
126 }
127
69 - snapshotLines.push(printElement(element, includeWeight));
70 - }
128 + rootWeight += weight;
129 + });
130
72 - rootWeight += weight;
73 - });
131 + // Make sure the pretty-printed test align with the Store's reported number of total rows.
132 + if (rootWeight !== store.numElements) {
133 + throw Error(
134 + `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`,
135 + );
136 + }
137
75 - // Make sure the pretty-printed test align with the Store's reported number of total rows.
76 - if (rootWeight !== store.numElements) {
77 - throw Error(
78 - `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`,
79 - );
138 + // If roots have been unmounted, verify that they've been removed from maps.
139 + // This helps ensure the Store doesn't leak memory.
140 + store.assertExpectedRootMapSizes();
141 }
142
82 - // If roots have been unmounted, verify that they've been removed from maps.
83 - // This helps ensure the Store doesn't leak memory.
84 - store.assertExpectedRootMapSizes();
85 -
143 return snapshotLines.join('\n');
144 }
145
packages/react-devtools-shared/src/devtools/views/Components/Element.css
+18
@@ -73,3 +73,21 @@
73 .Badge {
74 margin-left: 0.25rem;
75 }
76 +
77 +.ErrorIcon,
78 +.ErrorIconContrast,
79 +.WarningIcon,
80 +.WarningIconContrast {
81 + height: 0.75rem !important;
82 + width: 0.75rem !important;
83 + margin-left: 0.25rem;
84 +}
85 +.ErrorIcon {
86 + color: var(--color-console-error-icon);
87 +}
88 +.WarningIcon {
89 + color: var(--color-console-warning-icon);
90 +}
91 +.ErrorIconContrast, .WarningIconContrast {
92 + color: var(--color-component-name);
93 +}
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+47 -5
@@ -14,12 +14,15 @@ import Badge from './Badge';
14 import ButtonIcon from '../ButtonIcon';
15 import {createRegExp} from '../utils';
16 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
17 +import {SettingsContext} from '../Settings/SettingsContext';
18 import {StoreContext} from '../context';
19 +import {useSubscription} from '../hooks';
20
21 import type {ItemData} from './Tree';
20 -import type {Element} from './types';
22 +import type {Element as ElementType} from './types';
23
24 import styles from './Element.css';
25 +import Icon from '../Icon';
26
27 type Props = {
28 data: ItemData,
@@ -28,12 +31,13 @@ type Props = {
31 ...
32 };
33
31 -export default function ElementView({data, index, style}: Props) {
34 +export default function Element({data, index, style}: Props) {
35 const store = useContext(StoreContext);
36 const {ownerFlatTree, ownerID, selectedElementID} = useContext(
37 TreeStateContext,
38 );
39 const dispatch = useContext(TreeDispatcherContext);
40 + const {showInlineWarningsAndErrors} = React.useContext(SettingsContext);
41
42 const element =
43 ownerFlatTree !== null
@@ -46,6 +50,24 @@ export default function ElementView({data, index, style}: Props) {
50 const id = element === null ? null : element.id;
51 const isSelected = selectedElementID === id;
52
53 + const errorsAndWarningsSubscription = useMemo(
54 + () => ({
55 + getCurrentValue: () =>
56 + element === null
57 + ? {errorCount: 0, warningCount: 0}
58 + : store.getErrorAndWarningCountForElementID(element.id),
59 + subscribe: (callback: Function) => {
60 + store.addListener('mutated', callback);
61 + return () => store.removeListener('mutated', callback);
62 + },
63 + }),
64 + [store, element],
65 + );
66 + const {errorCount, warningCount} = useSubscription<{|
67 + errorCount: number,
68 + warningCount: number,
69 + |}>(errorsAndWarningsSubscription);
70 +
71 const handleDoubleClick = () => {
72 if (id !== null) {
73 dispatch({type: 'SELECT_OWNER', payload: id});
@@ -81,7 +103,7 @@ export default function ElementView({data, index, style}: Props) {
103
104 // Handle elements that are removed from the tree while an async render is in progress.
105 if (element == null) {
84 - console.warn(`<ElementView> Could not find element at index ${index}`);
106 + console.warn(`<Element> Could not find element at index ${index}`);
107
108 // This return needs to happen after hooks, since hooks can't be conditional.
109 return null;
@@ -93,7 +115,7 @@ export default function ElementView({data, index, style}: Props) {
115 hocDisplayNames,
116 key,
117 type,
96 - } = ((element: any): Element);
118 + } = ((element: any): ElementType);
119
120 let className = styles.Element;
121 if (isSelected) {
@@ -148,6 +170,26 @@ export default function ElementView({data, index, style}: Props) {
170 />
171 </Badge>
172 ) : null}
173 + {showInlineWarningsAndErrors && errorCount > 0 && (
174 + <Icon
175 + type="error"
176 + className={
177 + isSelected && treeFocused
178 + ? styles.ErrorIconContrast
179 + : styles.ErrorIcon
180 + }
181 + />
182 + )}
183 + {showInlineWarningsAndErrors && warningCount > 0 && (
184 + <Icon
185 + type="warning"
186 + className={
187 + isSelected && treeFocused
188 + ? styles.WarningIconContrast
189 + : styles.WarningIcon
190 + }
191 + />
192 + )}
193 </div>
194 </div>
195 );
@@ -160,7 +202,7 @@ const swallowDoubleClick = event => {
202 };
203
204 type ExpandCollapseToggleProps = {|
163 - element: Element,
205 + element: ElementType,
206 store: Store,
207 |};
208
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
+18
@@ -51,10 +51,13 @@ 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,
62 |};
63
@@ -159,6 +162,15 @@ function InspectedElementContextController({children}: Props) {
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,
@@ -217,6 +229,8 @@ function InspectedElementContextController({children}: Props) {
229 rootType,
230 state,
231 key,
232 + errors,
233 + warnings,
234 } = ((data.value: any): InspectedElementBackend);
235
236 const inspectedElement: InspectedElementFrontend = {
@@ -257,6 +271,8 @@ function InspectedElementContextController({children}: Props) {
271 hooks: hydrateHelper(hooks),
272 props: hydrateHelper(props),
273 state: hydrateHelper(state),
274 + errors,
275 + warnings,
276 };
277
278 element = store.getElementByID(id);
@@ -343,6 +359,7 @@ function InspectedElementContextController({children}: Props) {
359 copyInspectedElementPath,
360 getInspectedElement,
361 getInspectedElementPath,
362 + refreshInspectedElement,
363 storeAsGlobal,
364 }),
365 // InspectedElement is used to invalidate the cache and schedule an update with React.
@@ -351,6 +368,7 @@ function InspectedElementContextController({children}: Props) {
368 currentlyInspectedElement,
369 getInspectedElement,
370 getInspectedElementPath,
371 + refreshInspectedElement,
372 storeAsGlobal,
373 ],
374 );
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.css new
+60
@@ -0,0 +1,60 @@
1 +.ErrorTree, .WarningTree {
2 + padding: 0.25rem 0 0 0;
3 +}
4 +
5 +.HeaderRow {
6 + padding: 0 0.25rem;
7 +}
8 +
9 +.HeaderRow {
10 + padding: 0 0.25rem;
11 +}
12 +
13 +.Error, .Warning {
14 + padding: 0 0.5rem;
15 + display: flex;
16 + align-items: center;
17 +}
18 +
19 +.Error {
20 + border-top: 1px solid var(--color-console-error-border);
21 + background-color: var(--color-console-error-background);
22 + color: var(--color-error-text);
23 + padding: 0 0.5rem;
24 +}
25 +
26 +.Warning {
27 + border-top: 1px solid var(--color-console-warning-border);
28 + background-color: var(--color-console-warning-background);
29 + color: var(--color-warning-text);
30 + padding: 0 0.5rem;
31 +}
32 +
33 +.Message {
34 + overflow-x: hidden;
35 + text-overflow: ellipsis;
36 + white-space: nowrap;
37 +}
38 +
39 +.ErrorBadge,
40 +.WarningBadge {
41 + display: inline-block;
42 + width: 0.75rem;
43 + height: 0.75rem;
44 + flex: 0 0 0.75rem;
45 + line-height: 0.75rem;
46 + text-align: center;
47 + border-radius: 0.25rem;
48 + margin-right: 0.25rem;
49 + font-size: var(--font-size-monospace-small);
50 +}
51 +
52 +.ErrorBadge {
53 + background-color: var(--color-console-error-icon);
54 + color: var(--color-console-error-badge-text);
55 +}
56 +
57 +.WarningBadge {
58 + background-color: var(--color-console-warning-icon);
59 + color: var(--color-console-warning-badge-text);
60 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js new
+157
@@ -0,0 +1,157 @@
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 {useContext} from 'react';
12 +import Button from '../Button';
13 +import ButtonIcon from '../ButtonIcon';
14 +import Store from '../../store';
15 +import sharedStyles from './InspectedElementSharedStyles.css';
16 +import styles from './InspectedElementErrorsAndWarningsTree.css';
17 +import {SettingsContext} from '../Settings/SettingsContext';
18 +import {InspectedElementContext} from './InspectedElementContext';
19 +
20 +import type {InspectedElement} from './types';
21 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
22 +
23 +type Props = {|
24 + bridge: FrontendBridge,
25 + inspectedElement: InspectedElement,
26 + store: Store,
27 +|};
28 +
29 +export default function InspectedElementErrorsAndWarningsTree({
30 + bridge,
31 + inspectedElement,
32 + store,
33 +}: Props) {
34 + const {refreshInspectedElement} = useContext(InspectedElementContext);
35 +
36 + const {showInlineWarningsAndErrors} = useContext(SettingsContext);
37 + if (!showInlineWarningsAndErrors) {
38 + return null;
39 + }
40 +
41 + const {errors, warnings} = inspectedElement;
42 +
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 +
63 + return (
64 + <React.Fragment>
65 + {errors.length > 0 && (
66 + <Tree
67 + badgeClassName={styles.ErrorBadge}
68 + bridge={bridge}
69 + className={styles.ErrorTree}
70 + clearMessages={clearErrors}
71 + entries={errors}
72 + label="errors"
73 + messageClassName={styles.Error}
74 + />
75 + )}
76 + {warnings.length > 0 && (
77 + <Tree
78 + badgeClassName={styles.WarningBadge}
79 + bridge={bridge}
80 + className={styles.WarningTree}
81 + clearMessages={clearWarnings}
82 + entries={warnings}
83 + label="warnings"
84 + messageClassName={styles.Warning}
85 + />
86 + )}
87 + </React.Fragment>
88 + );
89 +}
90 +
91 +type TreeProps = {|
92 + badgeClassName: string,
93 + actions: React$Node,
94 + className: string,
95 + clearMessages: () => {},
96 + entries: Array<[string, number]>,
97 + label: string,
98 + messageClassName: string,
99 +|};
100 +
101 +function Tree({
102 + badgeClassName,
103 + actions,
104 + className,
105 + clearMessages,
106 + entries,
107 + label,
108 + messageClassName,
109 +}: TreeProps) {
110 + if (entries.length === 0) {
111 + return null;
112 + }
113 + return (
114 + <div className={`${sharedStyles.InspectedElementTree} ${className}`}>
115 + <div className={`${sharedStyles.HeaderRow} ${styles.HeaderRow}`}>
116 + <div className={sharedStyles.Header}>{label}</div>
117 + <Button
118 + onClick={clearMessages}
119 + title={`Clear all ${label} for this component`}>
120 + <ButtonIcon type="clear" />
121 + </Button>
122 + </div>
123 + {entries.map(([message, count], index) => (
124 + <ErrorOrWarningView
125 + key={`${label}-${index}`}
126 + badgeClassName={badgeClassName}
127 + className={messageClassName}
128 + count={count}
129 + message={message}
130 + />
131 + ))}
132 + </div>
133 + );
134 +}
135 +
136 +type ErrorOrWarningViewProps = {|
137 + badgeClassName: string,
138 + className: string,
139 + count: number,
140 + message: string,
141 +|};
142 +
143 +function ErrorOrWarningView({
144 + className,
145 + badgeClassName,
146 + count,
147 + message,
148 +}: ErrorOrWarningViewProps) {
149 + return (
150 + <div className={className}>
151 + {count > 1 && <div className={badgeClassName}>{count}</div>}
152 + <div className={styles.Message} title={message}>
153 + {message}
154 + </div>
155 + </div>
156 + );
157 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+2
@@ -12,6 +12,8 @@
12 }
13
14 .Header {
15 + display: flex;
16 + align-items: center;
17 flex: 1 1;
18 font-family: var(--font-family-sans);
19 }
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+8
@@ -19,6 +19,7 @@ import ButtonIcon from '../ButtonIcon';
19 import Icon from '../Icon';
20 import HocBadges from './HocBadges';
21 import InspectedElementContextTree from './InspectedElementContextTree';
22 +import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
23 import InspectedElementHooksTree from './InspectedElementHooksTree';
24 import InspectedElementPropsTree from './InspectedElementPropsTree';
25 import InspectedElementStateTree from './InspectedElementStateTree';
@@ -120,6 +121,13 @@ export default function InspectedElementView({
121 store={store}
122 />
123
124 + <InspectedElementErrorsAndWarningsTree
125 + bridge={bridge}
126 + getInspectedElementPath={getInspectedElementPath}
127 + inspectedElement={inspectedElement}
128 + store={store}
129 + />
130 +
131 <NativeStyleEditor />
132
133 {showRenderedBy && (
packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js
+37 -35
@@ -85,42 +85,44 @@ export default function SearchInput(props: Props) {
85 value={searchText}
86 />
87 {!!searchText && (
88 - <span className={styles.IndexLabel}>
89 - {Math.min(searchIndex + 1, searchResults.length)} |{' '}
90 - {searchResults.length}
91 - </span>
88 + <React.Fragment>
89 + <span className={styles.IndexLabel}>
90 + {Math.min(searchIndex + 1, searchResults.length)} |{' '}
91 + {searchResults.length}
92 + </span>
93 + <div className={styles.LeftVRule} />
94 + <Button
95 + className={styles.IconButton}
96 + disabled={!searchText}
97 + onClick={() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'})}
98 + title={
99 + <React.Fragment>
100 + Scroll to previous search result (<kbd>Shift</kbd> +{' '}
101 + <kbd>Enter</kbd>)
102 + </React.Fragment>
103 + }>
104 + <ButtonIcon type="up" />
105 + </Button>
106 + <Button
107 + className={styles.IconButton}
108 + disabled={!searchText}
109 + onClick={() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'})}
110 + title={
111 + <React.Fragment>
112 + Scroll to next search result (<kbd>Enter</kbd>)
113 + </React.Fragment>
114 + }>
115 + <ButtonIcon type="down" />
116 + </Button>
117 + <Button
118 + className={styles.IconButton}
119 + disabled={!searchText}
120 + onClick={resetSearch}
121 + title="Reset search">
122 + <ButtonIcon type="close" />
123 + </Button>
124 + </React.Fragment>
125 )}
93 - <div className={styles.LeftVRule} />
94 - <Button
95 - className={styles.IconButton}
96 - disabled={!searchText}
97 - onClick={() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'})}
98 - title={
99 - <React.Fragment>
100 - Scroll to previous search result (<kbd>Shift</kbd> +{' '}
101 - <kbd>Enter</kbd>)
102 - </React.Fragment>
103 - }>
104 - <ButtonIcon type="up" />
105 - </Button>
106 - <Button
107 - className={styles.IconButton}
108 - disabled={!searchText}
109 - onClick={() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'})}
110 - title={
111 - <React.Fragment>
112 - Scroll to next search result (<kbd>Enter</kbd>)
113 - </React.Fragment>
114 - }>
115 - <ButtonIcon type="down" />
116 - </Button>
117 - <Button
118 - className={styles.IconButton}
119 - disabled={!searchText}
120 - onClick={resetSearch}
121 - title="Reset search">
122 - <ButtonIcon type="close" />
123 - </Button>
126 </div>
127 );
128 }
packages/react-devtools-shared/src/devtools/views/Components/Tree.css
+21
@@ -44,6 +44,7 @@
44 .VRule {
45 height: 20px;
46 width: 1px;
47 + flex: 0 0 1px;
48 margin: 0 0.5rem;
49 background-color: var(--color-border);
50 }
@@ -58,3 +59,23 @@
59 font-size: var(--font-size-sans-large);
60 color: var(--color-dim);
61 }
62 +
63 +.IconAndCount {
64 + display: flex;
65 + align-items: center;
66 + font-size: var(--font-size-sans-normal);
67 +}
68 +
69 +.ErrorIcon, .WarningIcon {
70 + width: 0.75rem;
71 + height: 0.75rem;
72 + margin-left: 0.25rem;
73 + margin-right: 0.25rem;
74 + flex: 0 0 auto;
75 +}
76 +.ErrorIcon {
77 + color: var(--color-console-error-icon);
78 +}
79 +.WarningIcon {
80 + color: var(--color-console-warning-icon);
81 +}
packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+64 -4
@@ -21,18 +21,21 @@ import {
21 import AutoSizer from 'react-virtualized-auto-sizer';
22 import {FixedSizeList} from 'react-window';
23 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
24 +import Icon from '../Icon';
25 import {SettingsContext} from '../Settings/SettingsContext';
26 import {BridgeContext, StoreContext} from '../context';
26 -import ElementView from './Element';
27 +import Element from './Element';
28 import InspectHostNodesToggle from './InspectHostNodesToggle';
29 import OwnersStack from './OwnersStack';
30 import SearchInput from './SearchInput';
31 import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
32 import SelectedTreeHighlight from './SelectedTreeHighlight';
33 import TreeFocusedContext from './TreeFocusedContext';
33 -import {useHighlightNativeElement} from '../hooks';
34 +import {useHighlightNativeElement, useSubscription} from '../hooks';
35
36 import styles from './Tree.css';
37 +import ButtonIcon from '../ButtonIcon';
38 +import Button from '../Button';
39
40 // Never indent more than this number of pixels (even if we have the room).
41 const DEFAULT_INDENTATION_SIZE = 12;
@@ -71,7 +74,7 @@ export default function Tree(props: Props) {
74
75 const [treeFocused, setTreeFocused] = useState<boolean>(false);
76
74 - const {lineHeight} = useContext(SettingsContext);
77 + const {lineHeight, showInlineWarningsAndErrors} = useContext(SettingsContext);
78
79 // Make sure a newly selected element is visible in the list.
80 // This is helpful for things like the owners list and search.
@@ -301,6 +304,29 @@ export default function Tree(props: Props) {
304 [store],
305 );
306
307 + const handlePreviousErrorOrWarningClick = React.useCallback(() => {
308 + dispatch({type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
309 + }, []);
310 +
311 + const handleNextErrorOrWarningClick = React.useCallback(() => {
312 + dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
313 + }, []);
314 +
315 + const errorsOrWarningsSubscription = useMemo(
316 + () => ({
317 + getCurrentValue: () => ({
318 + errors: store.errorCount,
319 + warnings: store.warningCount,
320 + }),
321 + subscribe: (callback: Function) => {
322 + store.addListener('mutated', callback);
323 + return () => store.removeListener('mutated', callback);
324 + },
325 + }),
326 + [store],
327 + );
328 + const {errors, warnings} = useSubscription(errorsOrWarningsSubscription);
329 +
330 return (
331 <TreeFocusedContext.Provider value={treeFocused}>
332 <div className={styles.Tree} ref={treeRef}>
@@ -315,6 +341,40 @@ export default function Tree(props: Props) {
341 {ownerID !== null ? <OwnersStack /> : <SearchInput />}
342 </Suspense>
343 <div className={styles.VRule} />
344 + {showInlineWarningsAndErrors &&
345 + ownerID === null &&
346 + (errors > 0 || warnings > 0) && (
347 + <React.Fragment>
348 + {errors > 0 && (
349 + <div className={styles.IconAndCount}>
350 + <Icon className={styles.ErrorIcon} type="error" />
351 + {errors}
352 + </div>
353 + )}
354 + {warnings > 0 && (
355 + <div className={styles.IconAndCount}>
356 + <Icon className={styles.WarningIcon} type="warning" />
357 + {warnings}
358 + </div>
359 + )}
360 + <Button
361 + onClick={handlePreviousErrorOrWarningClick}
362 + title="Scroll to previous error or warning">
363 + <ButtonIcon type="up" />
364 + </Button>
365 + <Button
366 + onClick={handleNextErrorOrWarningClick}
367 + title="Scroll to next error or warning">
368 + <ButtonIcon type="down" />
369 + </Button>
370 + <Button
371 + onClick={() => store.clearErrorsAndWarnings()}
372 + title="Clear all errors and warnings">
373 + <ButtonIcon type="clear" />
374 + </Button>
375 + <div className={styles.VRule} />
376 + </React.Fragment>
377 + )}
378 <SettingsModalContextToggle />
379 </div>
380 <div
@@ -339,7 +399,7 @@ export default function Tree(props: Props) {
399 itemSize={lineHeight}
400 ref={listCallbackRef}
401 width={width}>
342 - {ElementView}
402 + {Element}
403 </FixedSizeList>
404 )}
405 </AutoSizer>
packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+87
@@ -93,6 +93,9 @@ type ACTION_SELECT_ELEMENT_BY_ID = {|
93 type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
94 type: 'SELECT_NEXT_ELEMENT_IN_TREE',
95 |};
96 +type ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {|
97 + type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
98 +|};
99 type ACTION_SELECT_NEXT_SIBLING_IN_TREE = {|
100 type: 'SELECT_NEXT_SIBLING_IN_TREE',
101 |};
@@ -106,6 +109,9 @@ type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
109 type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
110 type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
111 |};
112 +type ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {|
113 + type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
114 +|};
115 type ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE = {|
116 type: 'SELECT_PREVIOUS_SIBLING_IN_TREE',
117 |};
@@ -132,10 +138,12 @@ type Action =
138 | ACTION_SELECT_ELEMENT_AT_INDEX
139 | ACTION_SELECT_ELEMENT_BY_ID
140 | ACTION_SELECT_NEXT_ELEMENT_IN_TREE
141 + | ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
142 | ACTION_SELECT_NEXT_SIBLING_IN_TREE
143 | ACTION_SELECT_OWNER
144 | ACTION_SELECT_PARENT_ELEMENT_IN_TREE
145 | ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
146 + | ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
147 | ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
148 | ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
149 | ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
@@ -372,6 +380,83 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
380 }
381 }
382 break;
383 + case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
384 + if (store.errorCount === 0 && store.warningCount === 0) {
385 + return state;
386 + }
387 +
388 + const elementIndicesWithErrorsOrWarnings = store.getElementsWithErrorsAndWarnings();
389 +
390 + let flatIndex = 0;
391 + if (selectedElementIndex !== null) {
392 + // Resume from the current position in the list.
393 + // Otherwise step to the previous item, relative to the current selection.
394 + for (
395 + let i = elementIndicesWithErrorsOrWarnings.length - 1;
396 + i >= 0;
397 + i--
398 + ) {
399 + const {index} = elementIndicesWithErrorsOrWarnings[i];
400 + if (index >= selectedElementIndex) {
401 + flatIndex = i;
402 + } else {
403 + break;
404 + }
405 + }
406 + }
407 +
408 + let prevEntry;
409 + if (flatIndex === 0) {
410 + prevEntry =
411 + elementIndicesWithErrorsOrWarnings[
412 + elementIndicesWithErrorsOrWarnings.length - 1
413 + ];
414 + selectedElementID = prevEntry.id;
415 + selectedElementIndex = prevEntry.index;
416 + } else {
417 + prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
418 + selectedElementID = prevEntry.id;
419 + selectedElementIndex = prevEntry.index;
420 + }
421 +
422 + lookupIDForIndex = false;
423 + break;
424 + }
425 + case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
426 + if (store.errorCount === 0 && store.warningCount === 0) {
427 + return state;
428 + }
429 +
430 + const elementIndicesWithErrorsOrWarnings = store.getElementsWithErrorsAndWarnings();
431 +
432 + let flatIndex = -1;
433 + if (selectedElementIndex !== null) {
434 + // Resume from the current position in the list.
435 + // Otherwise step to the next item, relative to the current selection.
436 + for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
437 + const {index} = elementIndicesWithErrorsOrWarnings[i];
438 + if (index <= selectedElementIndex) {
439 + flatIndex = i;
440 + } else {
441 + break;
442 + }
443 + }
444 + }
445 +
446 + let nextEntry;
447 + if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
448 + nextEntry = elementIndicesWithErrorsOrWarnings[0];
449 + selectedElementID = nextEntry.id;
450 + selectedElementIndex = nextEntry.index;
451 + } else {
452 + nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
453 + selectedElementID = nextEntry.id;
454 + selectedElementIndex = nextEntry.index;
455 + }
456 +
457 + lookupIDForIndex = false;
458 + break;
459 + }
460 default:
461 // React can bailout of no-op updates.
462 return state;
@@ -776,11 +861,13 @@ function TreeContextController({
861 case 'SELECT_ELEMENT_BY_ID':
862 case 'SELECT_CHILD_ELEMENT_IN_TREE':
863 case 'SELECT_NEXT_ELEMENT_IN_TREE':
864 + case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
865 case 'SELECT_NEXT_SIBLING_IN_TREE':
866 case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
867 case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
868 case 'SELECT_PARENT_ELEMENT_IN_TREE':
869 case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
870 + case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
871 case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
872 case 'SELECT_OWNER':
873 case 'UPDATE_INSPECTED_ELEMENT_ID':
packages/react-devtools-shared/src/devtools/views/Components/types.js
+2
@@ -84,6 +84,8 @@ export type InspectedElement = {|
84 props: Object | null,
85 state: Object | null,
86 key: number | string | null,
87 + errors: Array<[string, number]>,
88 + warnings: Array<[string, number]>,
89
90 // List of owners
91 owners: Array<Owner> | null,
packages/react-devtools-shared/src/devtools/views/Icon.js
+13 -1
@@ -16,13 +16,15 @@ export type IconType =
16 | 'code'
17 | 'components'
18 | 'copy'
19 + | 'error'
20 | 'flame-chart'
21 | 'interactions'
22 | 'profiler'
23 | 'ranked-chart'
24 | 'search'
25 | 'settings'
25 - | 'store-as-global-variable';
26 + | 'store-as-global-variable'
27 + | 'warning';
28
29 type Props = {|
30 className?: string,
@@ -47,6 +49,9 @@ export default function Icon({className = '', type}: Props) {
49 case 'copy':
50 pathData = PATH_COPY;
51 break;
52 + case 'error':
53 + pathData = PATH_ERROR;
54 + break;
55 case 'flame-chart':
56 pathData = PATH_FLAME_CHART;
57 break;
@@ -68,6 +73,9 @@ export default function Icon({className = '', type}: Props) {
73 case 'store-as-global-variable':
74 pathData = PATH_STORE_AS_GLOBAL_VARIABLE;
75 break;
76 + case 'warning':
77 + pathData = PATH_WARNING;
78 + break;
79 default:
80 console.warn(`Unsupported type "${type}" specified for Icon`);
81 break;
@@ -107,6 +115,8 @@ const PATH_COPY = `
115 2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z
116 `;
117
118 +const PATH_ERROR = `M16.971 0h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-1.402 16.945l-3.554-3.521-3.518 3.568-1.418-1.418 3.507-3.566-3.586-3.472 1.418-1.417 3.581 3.458 3.539-3.583 1.431 1.431-3.535 3.568 3.566 3.522-1.431 1.43z`;
119 +
120 const PATH_FLAME_CHART = `
121 M10.0650893,21.5040462 C7.14020814,20.6850349 5,18.0558698 5,14.9390244 C5,14.017627
122 5,9.81707317 7.83333333,7.37804878 C7.83333333,7.37804878 7.58333333,11.199187 10,
@@ -154,3 +164,5 @@ const PATH_STORE_AS_GLOBAL_VARIABLE = `
164 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6
165 8h-4v-2h4v2zm0-4h-4v-2h4v2z
166 `;
167 +
168 +const PATH_WARNING = `M12 1l-12 22h24l-12-22zm-1 8h2v7h-2v-7zm1 11.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z`;
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+20
@@ -11,8 +11,10 @@ import {
11 __DEBUG__,
12 TREE_OPERATION_ADD,
13 TREE_OPERATION_REMOVE,
14 + TREE_OPERATION_REMOVE_ROOT,
15 TREE_OPERATION_REORDER_CHILDREN,
16 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
17 + TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
18 } from 'react-devtools-shared/src/constants';
19 import {utfDecodeString} from 'react-devtools-shared/src/utils';
20 import {ElementTypeRoot} from 'react-devtools-shared/src/types';
@@ -294,6 +296,9 @@ function updateTree(
296 }
297 break;
298 }
299 + case TREE_OPERATION_REMOVE_ROOT: {
300 + throw Error('Operation REMOVE_ROOT is not supported while profiling.');
301 + }
302 case TREE_OPERATION_REORDER_CHILDREN: {
303 id = ((operations[i + 1]: any): number);
304 const numChildren = ((operations[i + 2]: any): number);
@@ -329,6 +334,21 @@ function updateTree(
334 i += 3;
335 break;
336 }
337 + case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
338 + id = operations[i + 1];
339 + const numErrors = operations[i + 2];
340 + const numWarnings = operations[i + 3];
341 +
342 + i += 4;
343 +
344 + if (__DEBUG__) {
345 + debug(
346 + 'Warnings and Errors update',
347 + `fiber ${id} has ${numErrors} errors and ${numWarnings} warnings`,
348 + );
349 + }
350 + break;
351 +
352 default:
353 throw Error(`Unsupported Bridge operation ${operation}`);
354 }
packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
+19
@@ -19,6 +19,8 @@ export default function DebuggingSettings(_: {||}) {
19 breakOnConsoleErrors,
20 setAppendComponentStack,
21 setBreakOnConsoleErrors,
22 + setShowInlineWarningsAndErrors,
23 + showInlineWarningsAndErrors,
24 } = useContext(SettingsContext);
25
26 return (
@@ -36,6 +38,19 @@ export default function DebuggingSettings(_: {||}) {
38 </label>
39 </div>
40
41 + <div className={styles.Setting}>
42 + <label>
43 + <input
44 + type="checkbox"
45 + checked={showInlineWarningsAndErrors}
46 + onChange={({currentTarget}) =>
47 + setShowInlineWarningsAndErrors(currentTarget.checked)
48 + }
49 + />{' '}
50 + Show inline warnings and errors.
51 + </label>
52 + </div>
53 +
54 <div className={styles.Setting}>
55 <label>
56 <input
@@ -48,6 +63,10 @@ export default function DebuggingSettings(_: {||}) {
63 Break on warnings
64 </label>
65 </div>
66 +
67 + <div className={styles.ConsoleAPIWarning}>
68 + These settings require DevTools to override native console APIs.
69 + </div>
70 </div>
71 );
72 }
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+41 -1
@@ -21,6 +21,7 @@ import {
21 LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
22 LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
23 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
24 + LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
25 } from 'react-devtools-shared/src/constants';
26 import {useLocalStorage} from '../hooks';
27 import {BridgeContext} from '../context';
@@ -44,6 +45,9 @@ type Context = {|
45 breakOnConsoleErrors: boolean,
46 setBreakOnConsoleErrors: (value: boolean) => void,
47
48 + showInlineWarningsAndErrors: boolean,
49 + setShowInlineWarningsAndErrors: (value: boolean) => void,
50 +
51 theme: Theme,
52 setTheme(value: Theme): void,
53
@@ -90,6 +94,13 @@ function SettingsContextController({
94 LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
95 false,
96 );
97 + const [
98 + showInlineWarningsAndErrors,
99 + setShowInlineWarningsAndErrors,
100 + ] = useLocalStorage<boolean>(
101 + LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
102 + true,
103 + );
104 const [
105 traceUpdatesEnabled,
106 setTraceUpdatesEnabled,
@@ -147,8 +158,14 @@ function SettingsContextController({
158 bridge.send('updateConsolePatchSettings', {
159 appendComponentStack,
160 breakOnConsoleErrors,
161 + showInlineWarningsAndErrors,
162 });
151 - }, [bridge, appendComponentStack, breakOnConsoleErrors]);
163 + }, [
164 + bridge,
165 + appendComponentStack,
166 + breakOnConsoleErrors,
167 + showInlineWarningsAndErrors,
168 + ]);
169
170 useEffect(() => {
171 bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
@@ -168,6 +185,8 @@ function SettingsContextController({
185 setDisplayDensity,
186 setTheme,
187 setTraceUpdatesEnabled,
188 + setShowInlineWarningsAndErrors,
189 + showInlineWarningsAndErrors,
190 theme,
191 traceUpdatesEnabled,
192 }),
@@ -180,6 +199,8 @@ function SettingsContextController({
199 setDisplayDensity,
200 setTheme,
201 setTraceUpdatesEnabled,
202 + setShowInlineWarningsAndErrors,
203 + showInlineWarningsAndErrors,
204 theme,
205 traceUpdatesEnabled,
206 ],
@@ -324,6 +345,25 @@ export function updateThemeVariables(
345 'color-component-badge-count-inverted',
346 documentElements,
347 );
348 + updateStyleHelper(theme, 'color-console-error-badge-text', documentElements);
349 + updateStyleHelper(theme, 'color-console-error-background', documentElements);
350 + updateStyleHelper(theme, 'color-console-error-border', documentElements);
351 + updateStyleHelper(theme, 'color-console-error-icon', documentElements);
352 + updateStyleHelper(theme, 'color-console-error-text', documentElements);
353 + updateStyleHelper(
354 + theme,
355 + 'color-console-warning-badge-text',
356 + documentElements,
357 + );
358 + updateStyleHelper(
359 + theme,
360 + 'color-console-warning-background',
361 + documentElements,
362 + );
363 + updateStyleHelper(theme, 'color-console-warning-border', documentElements);
364 + updateStyleHelper(theme, 'color-console-warning-icon', documentElements);
365 + updateStyleHelper(theme, 'color-console-warning-text', documentElements);
366 + updateStyleHelper(theme, 'color-context-border', documentElements);
367 updateStyleHelper(theme, 'color-context-background', documentElements);
368 updateStyleHelper(theme, 'color-context-background-hover', documentElements);
369 updateStyleHelper(
packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.css
+1 -1
@@ -42,4 +42,4 @@
42 padding: 0.5rem;
43 flex: 0 1 auto;
44 overflow: auto;
45 -}
45 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+2 -1
@@ -128,7 +128,8 @@
128 background-color: var(--color-toggle-text);
129 }
130
131 -.ReleaseNotes {
131 +.ReleaseNotes,
132 +.ConsoleAPIWarning {
133 width: 100%;
134 background-color: var(--color-background-hover);
135 padding: 0.25rem 0.5rem;
packages/react-devtools-shared/src/devtools/views/root.css
+20
@@ -44,6 +44,16 @@
44 --light-color-component-badge-background-inverted: rgba(255, 255, 255, 0.25);
45 --light-color-component-badge-count: #777d88;
46 --light-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
47 + --light-color-console-error-badge-text: #ffffff;
48 + --light-color-console-error-background: #fff0f0;
49 + --light-color-console-error-border: #ffd6d6;
50 + --light-color-console-error-icon: #eb3941;
51 + --light-color-console-error-text: #fe2e31;
52 + --light-color-console-warning-badge-text: #000000;
53 + --light-color-console-warning-background: #fffbe5;
54 + --light-color-console-warning-border: #fff5c1;
55 + --light-color-console-warning-icon: #f4bd00;
56 + --light-color-console-warning-text: #64460c;
57 --light-color-context-background: rgba(0,0,0,.9);
58 --light-color-context-background-hover: rgba(255, 255, 255, 0.1);
59 --light-color-context-background-selected: #178fb9;
@@ -121,6 +131,16 @@
131 --dark-color-component-badge-background-inverted: rgba(0, 0, 0, 0.25);
132 --dark-color-component-badge-count: #8f949d;
133 --dark-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
134 + --dark-color-console-error-badge-text: #000000;
135 + --dark-color-console-error-background: #290000;
136 + --dark-color-console-error-border: #5c0000;
137 + --dark-color-console-error-icon: #eb3941;
138 + --dark-color-console-error-text: #fc7f7f;
139 + --dark-color-console-warning-badge-text: #000000;
140 + --dark-color-console-warning-background: #332b00;
141 + --dark-color-console-warning-border: #665500;
142 + --dark-color-console-warning-icon: #f4bd00;
143 + --dark-color-console-warning-text: #f5f2ed;
144 --dark-color-context-background: rgba(255,255,255,.9);
145 --dark-color-context-background-hover: rgba(0, 136, 250, 0.1);
146 --dark-color-context-background-selected: #0088fa;
packages/react-devtools-shared/src/hook.js
+3
@@ -182,6 +182,8 @@ export function installHook(target: any): DevToolsHook | null {
182 window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
183 const breakOnConsoleErrors =
184 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
185 + const showInlineWarningsAndErrors =
186 + window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ === true;
187
188 // The installHook() function is injected by being stringified in the browser,
189 // so imports outside of this function do not get included.
@@ -195,6 +197,7 @@ export function installHook(target: any): DevToolsHook | null {
197 patchConsole({
198 appendComponentStack,
199 breakOnConsoleErrors,
200 + showInlineWarningsAndErrors,
201 });
202 }
203 } catch (error) {}
packages/react-devtools-shared/src/utils.js
+39
@@ -26,7 +26,9 @@ import {REACT_SUSPENSE_LIST_TYPE as SuspenseList} from 'shared/ReactSymbols';
26 import {
27 TREE_OPERATION_ADD,
28 TREE_OPERATION_REMOVE,
29 + TREE_OPERATION_REMOVE_ROOT,
30 TREE_OPERATION_REORDER_CHILDREN,
31 + TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
32 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
33 } from './constants';
34 import {ElementTypeRoot} from 'react-devtools-shared/src/types';
@@ -34,6 +36,7 @@ import {
36 LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
37 LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
38 LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
39 + LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
40 } from './constants';
41 import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
42 import {
@@ -204,6 +207,12 @@ export function printOperationsArray(operations: Array<number>) {
207 }
208 break;
209 }
210 + case TREE_OPERATION_REMOVE_ROOT: {
211 + i += 1;
212 +
213 + logs.push(`Remove root ${rootID}`);
214 + break;
215 + }
216 case TREE_OPERATION_REORDER_CHILDREN: {
217 const id = ((operations[i + 1]: any): number);
218 const numChildren = ((operations[i + 2]: any): number);
@@ -220,6 +229,17 @@ export function printOperationsArray(operations: Array<number>) {
229 // The profiler UI uses them lazily in order to generate the tree.
230 i += 3;
231 break;
232 + case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
233 + const id = operations[i + 1];
234 + const numErrors = operations[i + 2];
235 + const numWarnings = operations[i + 3];
236 +
237 + i += 4;
238 +
239 + logs.push(
240 + `Node ${id} has ${numErrors} errors and ${numWarnings} warnings`,
241 + );
242 + break;
243 default:
244 throw Error(`Unsupported Bridge operation ${operation}`);
245 }
@@ -293,6 +313,25 @@ export function setBreakOnConsoleErrors(value: boolean): void {
313 );
314 }
315
316 +export function getShowInlineWarningsAndErrors(): boolean {
317 + try {
318 + const raw = localStorageGetItem(
319 + LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
320 + );
321 + if (raw != null) {
322 + return JSON.parse(raw);
323 + }
324 + } catch (error) {}
325 + return true;
326 +}
327 +
328 +export function setShowInlineWarningsAndErrors(value: boolean): void {
329 + localStorageSetItem(
330 + LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
331 + JSON.stringify(value),
332 + );
333 +}
334 +
335 export function separateDisplayNameAndHOCs(
336 displayName: string | null,
337 type: ElementType,
packages/react-devtools-shell/src/app/InlineWarnings/index.js new
+181
@@ -0,0 +1,181 @@
1 +/** @flow */
2 +
3 +import * as React from 'react';
4 +import {Fragment, useEffect, useRef, useState} from 'react';
5 +
6 +function WarnDuringRender({children = null}) {
7 + console.warn('This warning fires during every render');
8 + return children;
9 +}
10 +
11 +function WarnOnMount({children = null}) {
12 + useEffect(() => {
13 + console.warn('This warning fires on initial mount only');
14 + }, []);
15 + return children;
16 +}
17 +
18 +function WarnOnUpdate({children = null}) {
19 + const didMountRef = useRef(false);
20 + useEffect(() => {
21 + if (didMountRef.current) {
22 + console.warn('This warning fires on every update');
23 + } else {
24 + didMountRef.current = true;
25 + }
26 + });
27 + return children;
28 +}
29 +
30 +function WarnOnUnmount({children = null}) {
31 + useEffect(() => {
32 + return () => {
33 + console.warn('This warning fires on unmount');
34 + };
35 + }, []);
36 + return children;
37 +}
38 +
39 +function ErrorDuringRender({children = null}) {
40 + console.error('This error fires during every render');
41 + return children;
42 +}
43 +
44 +function ErrorOnMount({children = null}) {
45 + useEffect(() => {
46 + console.error('This error fires on initial mount only');
47 + }, []);
48 + return children;
49 +}
50 +
51 +function ErrorOnUpdate({children = null}) {
52 + const didMountRef = useRef(false);
53 + useEffect(() => {
54 + if (didMountRef.current) {
55 + console.error('This error fires on every update');
56 + } else {
57 + didMountRef.current = true;
58 + }
59 + });
60 + return children;
61 +}
62 +
63 +function ErrorOnUnmount({children = null}) {
64 + useEffect(() => {
65 + return () => {
66 + console.error('This error fires on unmount');
67 + };
68 + }, []);
69 + return children;
70 +}
71 +
72 +function ErrorAndWarningDuringRender({children = null}) {
73 + console.warn('This warning fires during every render');
74 + console.error('This error fires during every render');
75 + return children;
76 +}
77 +
78 +function ErrorAndWarningOnMount({children = null}) {
79 + useEffect(() => {
80 + console.warn('This warning fires on initial mount only');
81 + console.error('This error fires on initial mount only');
82 + }, []);
83 + return children;
84 +}
85 +
86 +function ErrorAndWarningOnUpdate({children = null}) {
87 + const didMountRef = useRef(false);
88 + useEffect(() => {
89 + if (didMountRef.current) {
90 + console.warn('This warning fires on every update');
91 + console.error('This error fires on every update');
92 + } else {
93 + didMountRef.current = true;
94 + }
95 + });
96 + return children;
97 +}
98 +
99 +function ErrorAndWarningOnUnmount({children = null}) {
100 + useEffect(() => {
101 + return () => {
102 + console.warn('This warning fires on unmount');
103 + console.error('This error fires on unmount');
104 + };
105 + }, []);
106 + return children;
107 +}
108 +
109 +function ReallyLongErrorMessageThatWillCauseTextToBeTruncated({
110 + children = null,
111 +}) {
112 + console.error(
113 + 'This error is a really long error message that should cause the text to be truncated in DevTools',
114 + );
115 + return children;
116 +}
117 +
118 +function ErrorWithMultipleArgs({children = null}) {
119 + console.error('This error', 'passes console', 4, 'arguments');
120 + return children;
121 +}
122 +
123 +function ErrorWithStringSubstitutions({children = null}) {
124 + console.error('This error uses "%s" substitutions', 'string');
125 + return children;
126 +}
127 +
128 +function ReactErrorOnHostComponent({children = null}) {
129 + return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
130 +}
131 +
132 +function DuplicateWarningsAndErrors({children = null}) {
133 + console.warn('this warning is logged twice per render');
134 + console.warn('this warning is logged twice per render');
135 + console.error('this error is logged twice per render');
136 + console.error('this error is logged twice per render');
137 + return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
138 +}
139 +
140 +function MultipleWarningsAndErrors({children = null}) {
141 + console.warn('this is the first warning logged');
142 + console.warn('this is the second warning logged');
143 + console.error('this is the first error logged');
144 + console.error('this is the second error logged');
145 + return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
146 +}
147 +
148 +function ComponentWithMissingKey({children}) {
149 + return [<div />];
150 +}
151 +
152 +export default function ErrorsAndWarnings() {
153 + const [count, setCount] = useState(0);
154 + const handleClick = () => setCount(count + 1);
155 + return (
156 + <Fragment>
157 + <h1>Inline warnings</h1>
158 + <button onClick={handleClick}>Update {count > 0 ? count : ''}</button>
159 + <ComponentWithMissingKey />
160 + <WarnDuringRender />
161 + <WarnOnMount />
162 + <WarnOnUpdate />
163 + {count === 0 ? <WarnOnUnmount /> : null}
164 + {count === 0 ? <WarnOnMount /> : null}
165 + <ErrorDuringRender />
166 + <ErrorOnMount />
167 + <ErrorOnUpdate />
168 + {count === 0 ? <ErrorOnUnmount /> : null}
169 + <ErrorAndWarningDuringRender />
170 + <ErrorAndWarningOnMount />
171 + <ErrorAndWarningOnUpdate />
172 + {count === 0 ? <ErrorAndWarningOnUnmount /> : null}
173 + <ErrorWithMultipleArgs />
174 + <ErrorWithStringSubstitutions />
175 + <ReactErrorOnHostComponent />
176 + <ReallyLongErrorMessageThatWillCauseTextToBeTruncated />
177 + <DuplicateWarningsAndErrors />
178 + <MultipleWarningsAndErrors />
179 + </Fragment>
180 + );
181 +}
packages/react-devtools-shell/src/app/index.js
+2
@@ -12,6 +12,7 @@ import Iframe from './Iframe';
12 import EditableProps from './EditableProps';
13 import ElementTypes from './ElementTypes';
14 import Hydration from './Hydration';
15 +import InlineWarnings from './InlineWarnings';
16 import InspectableElements from './InspectableElements';
17 import InteractionTracing from './InteractionTracing';
18 import PriorityLevels from './PriorityLevels';
@@ -53,6 +54,7 @@ function mountTestApp() {
54 mountHelper(Hydration);
55 mountHelper(ElementTypes);
56 mountHelper(EditableProps);
57 + mountHelper(InlineWarnings);
58 mountHelper(PriorityLevels);
59 mountHelper(ReactNativeWeb);
60 mountHelper(Toggle);
packages/react-devtools/OVERVIEW.md
+30 -1
@@ -141,12 +141,41 @@ While profiling is in progress, we send an extra operation any time a fiber is a
141 For example, updating the base duration for a fiber with an id of 1:
142 ```js
143 [
144 + 4, // update tree base duration operation
145 4, // tree base duration operation
146 1, // fiber id
147 32, // new tree base duration value
148 ]
149 ```
150
151 +#### Updating errors and warnings on a Fiber
152 +
153 +We record calls to `console.warn` and `console.error` in the backend.
154 +Periodically we notify the frontend that the number of recorded calls got updated.
155 +We only send the serialized messages as part of the `inspectElement` event.
156 +
157 +
158 +```js
159 +[
160 + 5, // update error/warning counts operation
161 + 4, // fiber id
162 + 0, // number of calls to console.error from that fiber
163 + 3, // number of calls to console.warn from that fiber
164 +]
165 +```
166 +
167 +#### Removing a root
168 +
169 +Special case of unmounting an entire root (include its decsendants). This specialized message replaces what would otherwise be a series of remove-node operations. It is currently only used in one case: updating component filters. The primary motivation for this is actually to preserve fiber ids for components that are re-added to the tree after the updated filters have been applied. This preserves mappings between the Fiber (id) and things like error and warning logs.
170 +
171 +```js
172 +[
173 + 6, // remove root operation
174 +]
175 +```
176 +
177 +This operation has no additional payload because renderer and root ids are already sent at the beginning of every operations payload.
178 +
179 ## Reconstructing the tree
180
181 The frontend stores its information about the tree in a map of id to objects with the following keys:
@@ -268,4 +297,4 @@ Once profiling is finished, the frontend requests profiling data from the backen
297
298 ### Importing/exporting data
299
271 -Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users.
\ No newline at end of file
300 +Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users.
scripts/jest/config.build-devtools.js
+3
@@ -59,6 +59,9 @@ module.exports = Object.assign({}, baseConfig, {
59 require.resolve(
60 '../../packages/react-devtools-shared/src/__tests__/storeSerializer.js'
61 ),
62 + require.resolve(
63 + '../../packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js'
64 + ),
65 ],
66 setupFiles: [
67 ...baseConfig.setupFiles,