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,