summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components')
-rw-r--r--devtools/client/shared/components/.eslintrc.js11
-rw-r--r--devtools/client/shared/components/Accordion.css87
-rw-r--r--devtools/client/shared/components/Accordion.js257
-rw-r--r--devtools/client/shared/components/AppErrorBoundary.css85
-rw-r--r--devtools/client/shared/components/AppErrorBoundary.js161
-rw-r--r--devtools/client/shared/components/Frame.js401
-rw-r--r--devtools/client/shared/components/HSplitBox.js165
-rw-r--r--devtools/client/shared/components/List.css41
-rw-r--r--devtools/client/shared/components/List.js352
-rw-r--r--devtools/client/shared/components/MdnLink.css33
-rw-r--r--devtools/client/shared/components/MdnLink.js38
-rw-r--r--devtools/client/shared/components/NotificationBox.css130
-rw-r--r--devtools/client/shared/components/NotificationBox.js403
-rw-r--r--devtools/client/shared/components/SearchBox.js269
-rw-r--r--devtools/client/shared/components/SearchBoxAutocompletePopup.js150
-rw-r--r--devtools/client/shared/components/SearchModifiers.css64
-rw-r--r--devtools/client/shared/components/SearchModifiers.js84
-rw-r--r--devtools/client/shared/components/Sidebar.js98
-rw-r--r--devtools/client/shared/components/SidebarToggle.css39
-rw-r--r--devtools/client/shared/components/SidebarToggle.js89
-rw-r--r--devtools/client/shared/components/SmartTrace.css167
-rw-r--r--devtools/client/shared/components/SmartTrace.js308
-rw-r--r--devtools/client/shared/components/StackTrace.js96
-rw-r--r--devtools/client/shared/components/Tree.css86
-rw-r--r--devtools/client/shared/components/Tree.js1055
-rw-r--r--devtools/client/shared/components/VirtualizedTree.js1071
-rw-r--r--devtools/client/shared/components/VisibilityHandler.js57
-rw-r--r--devtools/client/shared/components/menu/MenuButton.js450
-rw-r--r--devtools/client/shared/components/menu/MenuItem.js211
-rw-r--r--devtools/client/shared/components/menu/MenuList.js164
-rw-r--r--devtools/client/shared/components/menu/moz.build12
-rw-r--r--devtools/client/shared/components/menu/utils.js62
-rw-r--r--devtools/client/shared/components/moz.build41
-rw-r--r--devtools/client/shared/components/object-inspector/actions.js225
-rw-r--r--devtools/client/shared/components/object-inspector/components/ObjectInspector.css99
-rw-r--r--devtools/client/shared/components/object-inspector/components/ObjectInspector.js371
-rw-r--r--devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js285
-rw-r--r--devtools/client/shared/components/object-inspector/components/moz.build10
-rw-r--r--devtools/client/shared/components/object-inspector/index.js10
-rw-r--r--devtools/client/shared/components/object-inspector/moz.build16
-rw-r--r--devtools/client/shared/components/object-inspector/reducer.js147
-rw-r--r--devtools/client/shared/components/object-inspector/utils/client.js124
-rw-r--r--devtools/client/shared/components/object-inspector/utils/index.js52
-rw-r--r--devtools/client/shared/components/object-inspector/utils/load-properties.js260
-rw-r--r--devtools/client/shared/components/object-inspector/utils/moz.build13
-rw-r--r--devtools/client/shared/components/object-inspector/utils/node.js1039
-rw-r--r--devtools/client/shared/components/object-inspector/utils/selection.js16
-rw-r--r--devtools/client/shared/components/reps/images/input.svg7
-rw-r--r--devtools/client/shared/components/reps/images/jump-definition.svg8
-rw-r--r--devtools/client/shared/components/reps/images/open-a11y.svg10
-rw-r--r--devtools/client/shared/components/reps/images/open-inspector.svg6
-rw-r--r--devtools/client/shared/components/reps/index.js32
-rw-r--r--devtools/client/shared/components/reps/moz.build14
-rw-r--r--devtools/client/shared/components/reps/reps.css394
-rw-r--r--devtools/client/shared/components/reps/reps/accessible.js197
-rw-r--r--devtools/client/shared/components/reps/reps/accessor.js106
-rw-r--r--devtools/client/shared/components/reps/reps/array.js170
-rw-r--r--devtools/client/shared/components/reps/reps/attribute.js74
-rw-r--r--devtools/client/shared/components/reps/reps/big-int.js57
-rw-r--r--devtools/client/shared/components/reps/reps/comment-node.js76
-rw-r--r--devtools/client/shared/components/reps/reps/constants.js16
-rw-r--r--devtools/client/shared/components/reps/reps/custom-formatter.js256
-rw-r--r--devtools/client/shared/components/reps/reps/date-time.js95
-rw-r--r--devtools/client/shared/components/reps/reps/document-type.js60
-rw-r--r--devtools/client/shared/components/reps/reps/document.js79
-rw-r--r--devtools/client/shared/components/reps/reps/element-node.js310
-rw-r--r--devtools/client/shared/components/reps/reps/error.js330
-rw-r--r--devtools/client/shared/components/reps/reps/event.js115
-rw-r--r--devtools/client/shared/components/reps/reps/function.js264
-rw-r--r--devtools/client/shared/components/reps/reps/grip-array.js255
-rw-r--r--devtools/client/shared/components/reps/reps/grip-entry.js77
-rw-r--r--devtools/client/shared/components/reps/reps/grip-map.js234
-rw-r--r--devtools/client/shared/components/reps/reps/grip.js396
-rw-r--r--devtools/client/shared/components/reps/reps/infinity.js52
-rw-r--r--devtools/client/shared/components/reps/reps/moz.build45
-rw-r--r--devtools/client/shared/components/reps/reps/nan.js51
-rw-r--r--devtools/client/shared/components/reps/reps/null.js59
-rw-r--r--devtools/client/shared/components/reps/reps/number.js63
-rw-r--r--devtools/client/shared/components/reps/reps/object-with-text.js70
-rw-r--r--devtools/client/shared/components/reps/reps/object-with-url.js73
-rw-r--r--devtools/client/shared/components/reps/reps/object.js207
-rw-r--r--devtools/client/shared/components/reps/reps/promise.js101
-rw-r--r--devtools/client/shared/components/reps/reps/prop-rep.js105
-rw-r--r--devtools/client/shared/components/reps/reps/regexp.js66
-rw-r--r--devtools/client/shared/components/reps/reps/rep-utils.js574
-rw-r--r--devtools/client/shared/components/reps/reps/rep.js210
-rw-r--r--devtools/client/shared/components/reps/reps/string.js407
-rw-r--r--devtools/client/shared/components/reps/reps/stylesheet.js78
-rw-r--r--devtools/client/shared/components/reps/reps/symbol.js82
-rw-r--r--devtools/client/shared/components/reps/reps/text-node.js136
-rw-r--r--devtools/client/shared/components/reps/reps/undefined.js59
-rw-r--r--devtools/client/shared/components/reps/reps/window.js102
-rw-r--r--devtools/client/shared/components/reps/shared/dom-node-constants.js31
-rw-r--r--devtools/client/shared/components/reps/shared/grip-length-bubble.js64
-rw-r--r--devtools/client/shared/components/reps/shared/moz.build10
-rw-r--r--devtools/client/shared/components/splitter/Draggable.js106
-rw-r--r--devtools/client/shared/components/splitter/GridElementResizer.css32
-rw-r--r--devtools/client/shared/components/splitter/GridElementWidthResizer.js138
-rw-r--r--devtools/client/shared/components/splitter/SplitBox.css93
-rw-r--r--devtools/client/shared/components/splitter/SplitBox.js351
-rw-r--r--devtools/client/shared/components/splitter/moz.build11
-rw-r--r--devtools/client/shared/components/tabs/TabBar.js378
-rw-r--r--devtools/client/shared/components/tabs/Tabs.css127
-rw-r--r--devtools/client/shared/components/tabs/Tabs.js468
-rw-r--r--devtools/client/shared/components/tabs/moz.build10
-rw-r--r--devtools/client/shared/components/test/browser/browser.ini9
-rw-r--r--devtools/client/shared/components/test/browser/browser_notification_box_basic.js34
-rw-r--r--devtools/client/shared/components/test/browser/browser_reps_stubs.js347
-rw-r--r--devtools/client/shared/components/test/chrome/accordion.snapshots.js176
-rw-r--r--devtools/client/shared/components/test/chrome/chrome.ini46
-rw-r--r--devtools/client/shared/components/test/chrome/head.js379
-rw-r--r--devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html209
-rw-r--r--devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html210
-rw-r--r--devtools/client/shared/components/test/chrome/test_HSplitBox_01.html140
-rw-r--r--devtools/client/shared/components/test/chrome/test_accordion.html141
-rw-r--r--devtools/client/shared/components/test/chrome/test_frame_01.html361
-rw-r--r--devtools/client/shared/components/test/chrome/test_frame_02.html103
-rw-r--r--devtools/client/shared/components/test/chrome/test_list.html127
-rw-r--r--devtools/client/shared/components/test/chrome/test_list_keyboard.html283
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_01.html136
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_02.html73
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_03.html87
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_04.html67
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_05.html63
-rw-r--r--devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html301
-rw-r--r--devtools/client/shared/components/test/chrome/test_searchbox.html74
-rw-r--r--devtools/client/shared/components/test/chrome/test_sidebar_toggle.html59
-rw-r--r--devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html141
-rw-r--r--devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html290
-rw-r--r--devtools/client/shared/components/test/chrome/test_smart-trace.html172
-rw-r--r--devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html98
-rw-r--r--devtools/client/shared/components/test/chrome/test_stack-trace.html100
-rw-r--r--devtools/client/shared/components/test/chrome/test_tabs_accessibility.html82
-rw-r--r--devtools/client/shared/components/test/chrome/test_tabs_menu.html84
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree-view_01.html290
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree-view_02.html136
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_01.html68
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_02.html49
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_03.html50
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_04.html133
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_05.html195
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_06.html340
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_07.html69
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_08.html61
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_09.html85
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_10.html57
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_11.html100
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_12.html146
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_13.html88
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_14.html245
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_15.html99
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_16.html145
-rw-r--r--devtools/client/shared/components/test/node/.eslintrc.js10
-rw-r--r--devtools/client/shared/components/test/node/__mocks__/Services.js14
-rw-r--r--devtools/client/shared/components/test/node/__mocks__/object-front.js55
-rw-r--r--devtools/client/shared/components/test/node/__mocks__/string-front.js15
-rw-r--r--devtools/client/shared/components/test/node/babel.config.js13
-rw-r--r--devtools/client/shared/components/test/node/components/__snapshots__/tree.test.js.snap1171
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/basic.test.js.snap63
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/classnames.test.js.snap351
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/entries.test.js.snap94
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/expand.test.js.snap175
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/getter-setter.test.js.snap51
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/keyboard-navigation.test.js.snap55
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/properties.test.js.snap19
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/proxy.test.js.snap9
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/window.test.js.snap2114
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/basic.test.js439
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/classnames.test.js53
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/create-long-string-front.test.js94
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/create-object-client.test.js114
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/entries.test.js137
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/events.test.js171
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/expand.test.js435
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/function.test.js90
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/getter-setter.test.js106
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/keyboard-navigation.test.js89
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/properties.test.js158
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/proxy.test.js133
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/should-item-update.test.js96
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/component/window.test.js96
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/test-utils.js231
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/__snapshots__/promises.test.js.snap49
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/create-node.test.js87
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/get-children.test.js278
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/get-closest-grip-node.test.js52
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/get-value.test.js91
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/make-node-for-properties.test.js295
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/make-numerical-buckets.test.js138
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/node-has-entries.test.js51
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/node-is-window.test.js20
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/node-supports-numerical-bucketing.test.js72
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/promises.test.js54
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-entries.test.js171
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-full-text.test.js56
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-indexed-properties.test.js259
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-non-indexed-properties.test.js222
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-prototype.test.js218
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-symbols.test.js218
-rw-r--r--devtools/client/shared/components/test/node/components/object-inspector/utils/should-render-roots-in-reps.test.js153
-rw-r--r--devtools/client/shared/components/test/node/components/reps/__snapshots__/accessor.test.js.snap3
-rw-r--r--devtools/client/shared/components/test/node/components/reps/__snapshots__/element-node.test.js.snap42
-rw-r--r--devtools/client/shared/components/test/node/components/reps/__snapshots__/error.test.js.snap1210
-rw-r--r--devtools/client/shared/components/test/node/components/reps/__snapshots__/nan.test.js.snap10
-rw-r--r--devtools/client/shared/components/test/node/components/reps/accessible.test.js321
-rw-r--r--devtools/client/shared/components/test/node/components/reps/accessor.test.js137
-rw-r--r--devtools/client/shared/components/test/node/components/reps/array.test.js117
-rw-r--r--devtools/client/shared/components/test/node/components/reps/attribute.test.js44
-rw-r--r--devtools/client/shared/components/test/node/components/reps/big-int.test.js106
-rw-r--r--devtools/client/shared/components/test/node/components/reps/comment-node.test.js74
-rw-r--r--devtools/client/shared/components/test/node/components/reps/date-time.test.js61
-rw-r--r--devtools/client/shared/components/test/node/components/reps/document-type.test.js51
-rw-r--r--devtools/client/shared/components/test/node/components/reps/document.test.js52
-rw-r--r--devtools/client/shared/components/test/node/components/reps/element-node.test.js654
-rw-r--r--devtools/client/shared/components/test/node/components/reps/error.test.js748
-rw-r--r--devtools/client/shared/components/test/node/components/reps/event.test.js160
-rw-r--r--devtools/client/shared/components/test/node/components/reps/failure.test.js66
-rw-r--r--devtools/client/shared/components/test/node/components/reps/function.test.js584
-rw-r--r--devtools/client/shared/components/test/node/components/reps/grip-array.test.js705
-rw-r--r--devtools/client/shared/components/test/node/components/reps/grip-entry.test.js191
-rw-r--r--devtools/client/shared/components/test/node/components/reps/grip-map.test.js377
-rw-r--r--devtools/client/shared/components/test/node/components/reps/grip.test.js705
-rw-r--r--devtools/client/shared/components/test/node/components/reps/helper-tests.test.js122
-rw-r--r--devtools/client/shared/components/test/node/components/reps/infinity.test.js70
-rw-r--r--devtools/client/shared/components/test/node/components/reps/long-string.test.js135
-rw-r--r--devtools/client/shared/components/test/node/components/reps/nan.test.js43
-rw-r--r--devtools/client/shared/components/test/node/components/reps/null.test.js47
-rw-r--r--devtools/client/shared/components/test/node/components/reps/number.test.js136
-rw-r--r--devtools/client/shared/components/test/node/components/reps/object-with-text.test.js66
-rw-r--r--devtools/client/shared/components/test/node/components/reps/object-with-url.test.js45
-rw-r--r--devtools/client/shared/components/test/node/components/reps/object.test.js356
-rw-r--r--devtools/client/shared/components/test/node/components/reps/promise.test.js216
-rw-r--r--devtools/client/shared/components/test/node/components/reps/regexp.test.js59
-rw-r--r--devtools/client/shared/components/test/node/components/reps/string-with-url.test.js630
-rw-r--r--devtools/client/shared/components/test/node/components/reps/string.test.js257
-rw-r--r--devtools/client/shared/components/test/node/components/reps/stylesheet.test.js41
-rw-r--r--devtools/client/shared/components/test/node/components/reps/symbol.test.js64
-rw-r--r--devtools/client/shared/components/test/node/components/reps/test-helpers.js116
-rw-r--r--devtools/client/shared/components/test/node/components/reps/text-node.test.js186
-rw-r--r--devtools/client/shared/components/test/node/components/reps/undefined.test.js58
-rw-r--r--devtools/client/shared/components/test/node/components/reps/window.test.js131
-rw-r--r--devtools/client/shared/components/test/node/components/tree.test.js911
-rw-r--r--devtools/client/shared/components/test/node/jest.config.js16
-rw-r--r--devtools/client/shared/components/test/node/package.json27
-rw-r--r--devtools/client/shared/components/test/node/setup.js15
-rw-r--r--devtools/client/shared/components/test/node/stubs/object-inspector/grip.js64
-rw-r--r--devtools/client/shared/components/test/node/stubs/object-inspector/map.js154
-rw-r--r--devtools/client/shared/components/test/node/stubs/object-inspector/performance.js784
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/accessible.js74
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/accessor.js85
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/attribute.js36
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/big-int.js196
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js11
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/comment-node.js36
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/date-time.js47
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/document-type.js40
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/document.js39
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/element-node.js292
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/error.js396
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/event.js269
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/failure.js21
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/function.js227
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/grip-array.js1087
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/grip-entry.js16
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/grip-map.js908
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/grip.js1057
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/infinity.js19
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/long-string.js39
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/nan.js15
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/null.js15
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/number.js21
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/object-with-text.js36
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/object-with-url.js22
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/promise.js244
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/regexp.js36
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/stubs.ini19
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/stylesheet.js29
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/symbol.js33
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/text-node.js141
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/undefined.js15
-rw-r--r--devtools/client/shared/components/test/node/stubs/reps/window.js29
-rw-r--r--devtools/client/shared/components/test/node/yarn.lock4209
-rw-r--r--devtools/client/shared/components/throttling/NetworkThrottlingMenu.js100
-rw-r--r--devtools/client/shared/components/throttling/actions.js22
-rw-r--r--devtools/client/shared/components/throttling/moz.build13
-rw-r--r--devtools/client/shared/components/throttling/profiles.js104
-rw-r--r--devtools/client/shared/components/throttling/reducer.js29
-rw-r--r--devtools/client/shared/components/throttling/types.js17
-rw-r--r--devtools/client/shared/components/tree/LabelCell.js76
-rw-r--r--devtools/client/shared/components/tree/ObjectProvider.js86
-rw-r--r--devtools/client/shared/components/tree/TreeCell.js139
-rw-r--r--devtools/client/shared/components/tree/TreeHeader.js120
-rw-r--r--devtools/client/shared/components/tree/TreeRow.js304
-rw-r--r--devtools/client/shared/components/tree/TreeView.css199
-rw-r--r--devtools/client/shared/components/tree/TreeView.js799
-rw-r--r--devtools/client/shared/components/tree/moz.build13
296 files changed, 56231 insertions, 0 deletions
diff --git a/devtools/client/shared/components/.eslintrc.js b/devtools/client/shared/components/.eslintrc.js
new file mode 100644
index 0000000000..b67123ad2c
--- /dev/null
+++ b/devtools/client/shared/components/.eslintrc.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ globals: {
+ define: true,
+ },
+};
diff --git a/devtools/client/shared/components/Accordion.css b/devtools/client/shared/components/Accordion.css
new file mode 100644
index 0000000000..1fbc5c3e3e
--- /dev/null
+++ b/devtools/client/shared/components/Accordion.css
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Accordion */
+
+.accordion {
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+ /* Accordion root has tabindex="-1" to get focus programatically.
+ * This can give it a focus outline when clicked, which we don't want.
+ * The container itself is not in the focus order at all. */
+ outline: none;
+ background-color: var(--theme-sidebar-background);
+}
+
+.accordion-header {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ /* Reserve 1px for the border */
+ min-height: calc(var(--theme-toolbar-height) + 1px);
+ margin: 0;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding: 2px 4px;
+ font-size: inherit;
+ font-weight: normal;
+ user-select: none;
+ cursor: default;
+ background-color: var(--theme-accordion-header-background);
+}
+
+.accordion-header:hover {
+ background-color: var(--theme-accordion-header-hover);
+}
+
+/*
+ Arrow should be a bit closer to the text than to the start edge:
+ - total distance between text and start edge = 20px
+ - arrow width = 10px
+ - distance between arrow and start edge = 6px
+ - distance between arrow and text = 4px
+*/
+.accordion-header .theme-twisty {
+ display: inline-block;
+ flex: none;
+ width: 10px;
+ height: 10px;
+ margin-inline-start: 2px;
+ margin-inline-end: 4px;
+ pointer-events: none;
+}
+
+.accordion-header-label {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 12px;
+ line-height: 16px;
+ color: var(--theme-toolbar-color);
+}
+
+.accordion-header-buttons {
+ flex: none;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ max-width: 50%;
+ margin-inline-start: auto;
+ padding-inline-start: 4px;
+}
+
+.accordion-content {
+ overflow: auto;
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.accordion-content[hidden] {
+ display: none;
+}
+
+.accordion-item:last-child > .accordion-content {
+ border-bottom: none;
+}
diff --git a/devtools/client/shared/components/Accordion.js b/devtools/client/shared/components/Accordion.js
new file mode 100644
index 0000000000..c3f1afa418
--- /dev/null
+++ b/devtools/client/shared/components/Accordion.js
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ Component,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ ul,
+ li,
+ h2,
+ div,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+class Accordion extends Component {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ // A list of all items to be rendered using an Accordion component.
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ buttons: PropTypes.arrayOf(PropTypes.object),
+ className: PropTypes.string,
+ component: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ componentProps: PropTypes.object,
+ contentClassName: PropTypes.string,
+ header: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ onToggle: PropTypes.func,
+ // Determines the initial open state of the accordion item
+ opened: PropTypes.bool.isRequired,
+ // Enables dynamically changing the open state of the accordion
+ // on update.
+ shouldOpen: PropTypes.func,
+ })
+ ).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ opened: {},
+ };
+
+ this.onHeaderClick = this.onHeaderClick.bind(this);
+ this.onHeaderKeyDown = this.onHeaderKeyDown.bind(this);
+ this.setInitialState = this.setInitialState.bind(this);
+ this.updateCurrentState = this.updateCurrentState.bind(this);
+ }
+
+ componentDidMount() {
+ this.setInitialState();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.items !== this.props.items) {
+ this.updateCurrentState();
+ }
+ }
+
+ setInitialState() {
+ /**
+ * Add initial data to the `state.opened` map.
+ * This happens only on initial mount of the accordion.
+ */
+ const newItems = this.props.items.filter(
+ ({ id }) => typeof this.state.opened[id] !== "boolean"
+ );
+
+ if (newItems.length) {
+ const everOpened = { ...this.state.everOpened };
+ const opened = { ...this.state.opened };
+ for (const item of newItems) {
+ everOpened[item.id] = item.opened;
+ opened[item.id] = item.opened;
+ }
+
+ this.setState({ everOpened, opened });
+ }
+ }
+
+ updateCurrentState() {
+ /**
+ * Updates the `state.opened` map based on the
+ * new items that have been added and those that
+ * `item.shouldOpen()` has changed. This happens
+ * on each update.
+ */
+ const updatedItems = this.props.items.filter(item => {
+ const notExist = typeof this.state.opened[item.id] !== "boolean";
+ if (typeof item.shouldOpen == "function") {
+ const currentState = this.state.opened[item.id];
+ return notExist || currentState !== item.shouldOpen(item, currentState);
+ }
+ return notExist;
+ });
+
+ if (updatedItems.length) {
+ const everOpened = { ...this.state.everOpened };
+ const opened = { ...this.state.opened };
+ for (const item of updatedItems) {
+ let itemOpen = item.opened;
+ if (typeof item.shouldOpen == "function") {
+ itemOpen = item.shouldOpen(item, itemOpen);
+ }
+ everOpened[item.id] = itemOpen;
+ opened[item.id] = itemOpen;
+ }
+ this.setState({ everOpened, opened });
+ }
+ }
+
+ /**
+ * @param {Event} event Click event.
+ * @param {Object} item The item to be collapsed/expanded.
+ */
+ onHeaderClick(event, item) {
+ event.preventDefault();
+ // In the Browser Toolbox's Inspector/Layout view, handleHeaderClick is
+ // called twice unless we call stopPropagation, making the accordion item
+ // open-and-close or close-and-open
+ event.stopPropagation();
+ this.toggleItem(item);
+ }
+
+ /**
+ * @param {Event} event Keyboard event.
+ * @param {Object} item The item to be collapsed/expanded.
+ */
+ onHeaderKeyDown(event, item) {
+ if (event.key === " " || event.key === "Enter") {
+ event.preventDefault();
+ this.toggleItem(item);
+ }
+ }
+
+ /**
+ * Expand or collapse an accordion list item.
+ * @param {Object} item The item to be collapsed or expanded.
+ */
+ toggleItem(item) {
+ const opened = !this.state.opened[item.id];
+
+ this.setState({
+ everOpened: {
+ ...this.state.everOpened,
+ [item.id]: true,
+ },
+ opened: {
+ ...this.state.opened,
+ [item.id]: opened,
+ },
+ });
+
+ if (typeof item.onToggle === "function") {
+ item.onToggle(opened, item);
+ }
+ }
+
+ renderItem(item) {
+ const {
+ buttons,
+ className = "",
+ component,
+ componentProps = {},
+ contentClassName = "",
+ header,
+ id,
+ } = item;
+
+ const headerId = `${id}-header`;
+ const opened = this.state.opened[id];
+ let itemContent;
+
+ // Only render content if the accordion item is open or has been opened once before.
+ // This saves us rendering complex components when users are keeping
+ // them closed (e.g. in Inspector/Layout) or may not open them at all.
+ if (this.state.everOpened && this.state.everOpened[id]) {
+ if (typeof component === "function") {
+ itemContent = createElement(component, componentProps);
+ } else if (typeof component === "object") {
+ itemContent = component;
+ }
+ }
+
+ return li(
+ {
+ key: id,
+ id,
+ className: `accordion-item ${
+ opened ? "accordion-open" : ""
+ } ${className} `.trim(),
+ "aria-labelledby": headerId,
+ },
+ h2(
+ {
+ id: headerId,
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": opened,
+ // If the header contains buttons, make sure the heading name only
+ // contains the "header" text and not the button text
+ "aria-label": header,
+ onKeyDown: event => this.onHeaderKeyDown(event, item),
+ onClick: event => this.onHeaderClick(event, item),
+ },
+ span({
+ className: `theme-twisty${opened ? " open" : ""}`,
+ role: "presentation",
+ }),
+ span(
+ {
+ className: "accordion-header-label",
+ },
+ header
+ ),
+ buttons &&
+ span(
+ {
+ className: "accordion-header-buttons",
+ role: "presentation",
+ },
+ buttons
+ )
+ ),
+ div(
+ {
+ className: `accordion-content ${contentClassName}`.trim(),
+ hidden: !opened,
+ role: "presentation",
+ },
+ itemContent
+ )
+ );
+ }
+
+ render() {
+ return ul(
+ {
+ className:
+ "accordion" +
+ (this.props.className ? ` ${this.props.className}` : ""),
+ tabIndex: -1,
+ },
+ this.props.items.map(item => this.renderItem(item))
+ );
+ }
+}
+
+module.exports = Accordion;
diff --git a/devtools/client/shared/components/AppErrorBoundary.css b/devtools/client/shared/components/AppErrorBoundary.css
new file mode 100644
index 0000000000..4ff5964f6a
--- /dev/null
+++ b/devtools/client/shared/components/AppErrorBoundary.css
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Base styles (common to most error boundaries) */
+
+
+/* Container */
+.app-error-panel {
+ color: var(--theme-text-color-strong);
+ display: flex;
+ flex-direction: column;
+ font-family: inherit;
+ margin: 0 0 2rem;
+ overflow-y: scroll;
+ padding: 1rem 3rem;
+ width: 100%;
+ height: 100%;
+}
+
+/* "Has crashed" header */
+.app-error-panel .error-panel-header {
+ align-self: center;
+ font-size: 22px;
+ font-weight: 300;
+}
+
+/* "File a Bug" button */
+.app-error-panel .error-panel-file-button {
+ align-self: center;
+ background-color: var(--blue-60);
+ outline: none;
+ color: white;
+ font-size: 15px;
+ font-weight: 400;
+ margin-bottom: 14.74px;
+ padding: 0.75rem;
+ text-align: center;
+ inline-size: 200px;
+ text-decoration: none;
+}
+
+.app-error-panel .error-panel-file-button:hover {
+ background-color: var(--blue-70);
+}
+
+.app-error-panel .error-panel-file-button:hover:active {
+ background-color: var(--blue-80);
+}
+
+/* Text of the error itself, not the stack trace */
+.app-error-panel .error-panel-error {
+ background-color: var(--theme-body-emphasized-background);
+ border: 1px solid var(--theme-toolbar-separator);
+ border-block-end: 0;
+ font-size: 17px;
+ font-weight: 500;
+ margin: 0;
+ padding: 2rem;
+}
+
+/* Stack trace; composed of <p> elements */
+.app-error-panel .stack-trace-section {
+ background-color: var(--theme-body-emphasized-background);
+ border: 1px solid var(--theme-toolbar-separator);
+ padding: 2rem;
+ margin-bottom: 1rem;
+}
+
+.app-error-panel .stack-trace-section p {
+ color: var(--theme-stack-trace-text);
+ margin: 0;
+ margin-inline-start: 1rem;
+}
+
+.app-error-panel .stack-trace-section p:first-child {
+ margin: 0;
+}
+
+/* Instructions to reopen the toolbox */
+.app-error-panel .error-panel-reload-info {
+ font-size: 15px;
+ font-weight: 400;
+ margin: 2rem 0 1rem;
+}
diff --git a/devtools/client/shared/components/AppErrorBoundary.js b/devtools/client/shared/components/AppErrorBoundary.js
new file mode 100644
index 0000000000..66f839f8a9
--- /dev/null
+++ b/devtools/client/shared/components/AppErrorBoundary.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// React deps
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { div, h1, h2, h3, p, a } = dom;
+
+// Localized strings for (devtools/client/locales/en-US/components.properties)
+loader.lazyGetter(this, "L10N", function () {
+ const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+ return new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+ );
+});
+
+loader.lazyGetter(this, "FILE_BUG_BUTTON", function () {
+ return L10N.getStr("appErrorBoundary.fileBugButton");
+});
+
+loader.lazyGetter(this, "RELOAD_PAGE_INFO", function () {
+ return L10N.getStr("appErrorBoundary.reloadPanelInfo");
+});
+
+// File a bug for the selected component specifically
+const bugLink =
+ "https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component=";
+
+/**
+ * Error boundary that wraps around the a given component.
+ */
+class AppErrorBoundary extends Component {
+ static get propTypes() {
+ return {
+ children: PropTypes.any.isRequired,
+ panel: PropTypes.any.isRequired,
+ componentName: PropTypes.string.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ errorMsg: "No error",
+ errorStack: null,
+ errorInfo: null,
+ };
+ }
+
+ /**
+ * Map the `info` object to a render.
+ * Currently, `info` usually just contains something similar to the
+ * following object (which is provided to componentDidCatch):
+ * componentStack: {"\n in (component) \n in (other component)..."}
+ */
+ renderErrorInfo(info = {}) {
+ if (Object.keys(info).length) {
+ return Object.keys(info).map((obj, outerIdx) => {
+ const traceParts = info[obj]
+ .split("\n")
+ .map((part, idx) => p({ key: `strace${idx}` }, part));
+ return div(
+ { key: `st-div-${outerIdx}`, className: "stack-trace-section" },
+ h3({}, "React Component Stack"),
+ p({ key: `st-p-${outerIdx}` }, obj.toString()),
+ traceParts
+ );
+ });
+ }
+
+ return p({}, "undefined errorInfo");
+ }
+
+ renderStackTrace(stacktrace = "") {
+ const re = /:\d+:\d+/g;
+ const traces = stacktrace
+ .replace(re, "$&,")
+ .split(",")
+ .map((trace, index) => {
+ return p({ key: `rst-${index}` }, trace);
+ });
+
+ return div(
+ { className: "stack-trace-section" },
+ h3({}, "Stacktrace"),
+ traces
+ );
+ }
+
+ // Return a valid object, even if we don't receive one
+ getValidInfo(infoObj) {
+ if (!infoObj.componentStack) {
+ try {
+ return { componentStack: JSON.stringify(infoObj) };
+ } catch (err) {
+ return { componentStack: `Unknown Error: ${err}` };
+ }
+ }
+ return infoObj;
+ }
+
+ // Called when a child component throws an error.
+ componentDidCatch(error, info) {
+ const validInfo = this.getValidInfo(info);
+ this.setState({
+ errorMsg: error.toString(),
+ errorStack: error.stack,
+ errorInfo: validInfo,
+ });
+ }
+
+ getBugLink() {
+ const compStack = this.getValidInfo(this.state.errorInfo).componentStack;
+ const errorMsg = this.state.errorMsg;
+ const errorStack = this.state.errorStack;
+ const msg = `Error: \n${errorMsg}\n\nReact Component Stack: ${compStack}\n\nStacktrace: \n${errorStack}`;
+ return `${bugLink}${this.props.componentName}&comment=${encodeURIComponent(
+ msg
+ )}`;
+ }
+
+ render() {
+ if (this.state.errorInfo !== null) {
+ // "The (componentDesc) has crashed"
+ const errorDescription = L10N.getFormatStr(
+ "appErrorBoundary.description",
+ this.props.panel
+ );
+ return div(
+ {
+ className: `app-error-panel`,
+ },
+ h1({ className: "error-panel-header" }, errorDescription),
+ a(
+ {
+ className: "error-panel-file-button",
+ href: this.getBugLink(),
+ onClick: () => {
+ window.open(this.getBugLink(), "_blank");
+ },
+ },
+ FILE_BUG_BUTTON
+ ),
+ h2({ className: "error-panel-error" }, this.state.errorMsg),
+ div({}, this.renderErrorInfo(this.state.errorInfo)),
+ div({}, this.renderStackTrace(this.state.errorStack)),
+ p({ className: "error-panel-reload-info" }, RELOAD_PAGE_INFO)
+ );
+ }
+ return this.props.children;
+ }
+}
+
+module.exports = AppErrorBoundary;
diff --git a/devtools/client/shared/components/Frame.js b/devtools/client/shared/components/Frame.js
new file mode 100644
index 0000000000..4efc7d3bd6
--- /dev/null
+++ b/devtools/client/shared/components/Frame.js
@@ -0,0 +1,401 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ getUnicodeUrl,
+ getUnicodeUrlPath,
+ getUnicodeHostname,
+} = require("resource://devtools/client/shared/unicode-url.js");
+const {
+ getSourceNames,
+ parseURL,
+ getSourceMappedFile,
+} = require("resource://devtools/client/shared/source-utils.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const {
+ MESSAGE_SOURCE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+);
+const webl10n = new LocalizationHelper(
+ "devtools/client/locales/webconsole.properties"
+);
+
+function savedFrameToLocation(frame) {
+ const { source: url, line, column, sourceId } = frame;
+ return {
+ url,
+ line,
+ column,
+ // The sourceId will be a string if it's a source actor ID, otherwise
+ // it is either a Spidermonkey-internal ID from a SavedFrame or missing,
+ // and in either case we can't use the ID for anything useful.
+ id: typeof sourceId === "string" ? sourceId : null,
+ };
+}
+
+/**
+ * Get the tooltip message.
+ * @param {string|undefined} messageSource
+ * @param {string} url
+ * @returns {string}
+ */
+function getTooltipMessage(messageSource, url) {
+ if (messageSource && messageSource === MESSAGE_SOURCE.CSS) {
+ return l10n.getFormatStr("frame.viewsourceinstyleeditor", url);
+ }
+ return l10n.getFormatStr("frame.viewsourceindebugger", url);
+}
+
+class Frame extends Component {
+ static get propTypes() {
+ return {
+ // Optional className that will be put into the element.
+ className: PropTypes.string,
+ // SavedFrame, or an object containing all the required properties.
+ frame: PropTypes.shape({
+ functionDisplayName: PropTypes.string,
+ // This could be a SavedFrame with a numeric sourceId, or it could
+ // be a SavedFrame-like client-side object, in which case the
+ // "sourceId" will be a source actor ID.
+ sourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ source: PropTypes.string.isRequired,
+ line: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ column: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ }).isRequired,
+ // Clicking on the frame link -- probably should link to the debugger.
+ onClick: PropTypes.func,
+ // Option to display a function name before the source link.
+ showFunctionName: PropTypes.bool,
+ // Option to display a function name even if it's anonymous.
+ showAnonymousFunctionName: PropTypes.bool,
+ // Option to display a host name after the source link.
+ showHost: PropTypes.bool,
+ // Option to display a host name if the filename is empty or just '/'
+ showEmptyPathAsHost: PropTypes.bool,
+ // Option to display a full source instead of just the filename.
+ showFullSourceUrl: PropTypes.bool,
+ // Service to enable the source map feature for console.
+ sourceMapURLService: PropTypes.object,
+ // The source of the message
+ messageSource: PropTypes.string,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ showFunctionName: false,
+ showAnonymousFunctionName: false,
+ showHost: false,
+ showEmptyPathAsHost: false,
+ showFullSourceUrl: false,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ originalLocation: null,
+ };
+ this._locationChanged = this._locationChanged.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ if (this.props.sourceMapURLService) {
+ const location = savedFrameToLocation(this.props.frame);
+ // Many things that make use of this component either:
+ // a) Pass in no sourceId because they have no way to know.
+ // b) Pass in no sourceId because the actor wasn't created when the
+ // server sent its response.
+ //
+ // and due to that, we need to use subscribeByLocation in order to
+ // handle both cases with an without an ID.
+ this.unsubscribeSourceMapURLService =
+ this.props.sourceMapURLService.subscribeByLocation(
+ location,
+ this._locationChanged
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.unsubscribeSourceMapURLService) {
+ this.unsubscribeSourceMapURLService();
+ }
+ }
+
+ _locationChanged(originalLocation) {
+ this.setState({ originalLocation });
+ }
+
+ /**
+ * Get current location's source, line, and column.
+ * @returns {{source: string, line: number|null, column: number|null}}
+ */
+ #getCurrentLocationInfo = () => {
+ const { frame } = this.props;
+ const { originalLocation } = this.state;
+
+ const generatedLocation = savedFrameToLocation(frame);
+ const currentLocation = originalLocation || generatedLocation;
+
+ const source = currentLocation.url || "";
+ const line =
+ currentLocation.line != void 0 ? Number(currentLocation.line) : null;
+ const column =
+ currentLocation.column != void 0 ? Number(currentLocation.column) : null;
+ return {
+ source,
+ line,
+ column,
+ };
+ };
+
+ /**
+ * Get unicode hostname of the source link.
+ * @returns {string}
+ */
+ #getCurrentLocationUnicodeHostName = () => {
+ const { source } = this.#getCurrentLocationInfo();
+
+ const { host } = getSourceNames(source);
+ return host ? getUnicodeHostname(host) : "";
+ };
+
+ /**
+ * Check if the current location is linkable.
+ * @returns {boolean}
+ */
+ #isCurrentLocationLinkable = () => {
+ const { frame } = this.props;
+ const { originalLocation } = this.state;
+
+ const generatedLocation = savedFrameToLocation(frame);
+
+ // Reparse the URL to determine if we should link this; `getSourceNames`
+ // has already cached this indirectly. We don't want to attempt to
+ // link to "self-hosted" and "(unknown)".
+ // Source mapped sources might not necessary linkable, but they
+ // are still valid in the debugger.
+ // If we have a source ID then we can show the source in the debugger.
+ return !!(
+ originalLocation ||
+ generatedLocation.id ||
+ !!parseURL(generatedLocation.url)
+ );
+ };
+
+ /**
+ * Get the props of the top element.
+ */
+ #getTopElementProps = () => {
+ const { className } = this.props;
+
+ const { source, line, column } = this.#getCurrentLocationInfo();
+ const { long } = getSourceNames(source);
+ const props = {
+ "data-url": long,
+ className: "frame-link" + (className ? ` ${className}` : ""),
+ };
+
+ // If we have a line number > 0.
+ if (line) {
+ // Add `data-line` attribute for testing
+ props["data-line"] = line;
+
+ // Intentionally exclude 0
+ if (column) {
+ // Add `data-column` attribute for testing
+ props["data-column"] = column;
+ }
+ }
+ return props;
+ };
+
+ /**
+ * Get the props of the source element.
+ */
+ #getSourceElementsProps = () => {
+ const { frame, onClick, messageSource } = this.props;
+
+ const generatedLocation = savedFrameToLocation(frame);
+ const { source, line, column } = this.#getCurrentLocationInfo();
+ const { long } = getSourceNames(source);
+ let url = getUnicodeUrl(long);
+
+ // Exclude all falsy values, including `0`, as line numbers start with 1.
+ if (line) {
+ url += `:${line}`;
+ // Intentionally exclude 0
+ if (column) {
+ url += `:${column}`;
+ }
+ }
+
+ const isLinkable = this.#isCurrentLocationLinkable();
+
+ // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
+ // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
+ const tooltipMessage = getTooltipMessage(messageSource, url);
+
+ const sourceElConfig = {
+ key: "source",
+ className: "frame-link-source",
+ title: isLinkable ? tooltipMessage : url,
+ };
+
+ if (isLinkable) {
+ return {
+ ...sourceElConfig,
+ onClick: e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ onClick(generatedLocation);
+ },
+ href: source,
+ draggable: false,
+ };
+ }
+
+ return sourceElConfig;
+ };
+
+ /**
+ * Render the source elements.
+ * @returns {React.ReactNode}
+ */
+ #renderSourceElements = () => {
+ const { line, column } = this.#getCurrentLocationInfo();
+
+ const sourceElements = [this.#renderDisplaySource()];
+
+ if (line) {
+ let lineInfo = `:${line}`;
+
+ // Intentionally exclude 0
+ if (column) {
+ lineInfo += `:${column}`;
+ }
+
+ sourceElements.push(
+ dom.span(
+ {
+ key: "line",
+ className: "frame-link-line",
+ },
+ lineInfo
+ )
+ );
+ }
+
+ if (this.#isCurrentLocationLinkable()) {
+ return dom.a(this.#getSourceElementsProps(), sourceElements);
+ }
+ // If source is not a URL (self-hosted, eval, etc.), don't make
+ // it an anchor link, as we can't link to it.
+ return dom.span(this.#getSourceElementsProps(), sourceElements);
+ };
+
+ /**
+ * Render the display source.
+ * @returns {React.ReactNode}
+ */
+ #renderDisplaySource = () => {
+ const { showEmptyPathAsHost, showFullSourceUrl } = this.props;
+ const { originalLocation } = this.state;
+
+ const { source } = this.#getCurrentLocationInfo();
+ const { short, long, host } = getSourceNames(source);
+ const unicodeShort = getUnicodeUrlPath(short);
+ const unicodeLong = getUnicodeUrl(long);
+ let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort;
+ if (originalLocation) {
+ displaySource = getSourceMappedFile(displaySource);
+
+ // In case of pretty-printed HTML file, we would only get the formatted suffix; replace
+ // it with the full URL instead
+ if (showEmptyPathAsHost && displaySource == ":formatted") {
+ displaySource = host + displaySource;
+ }
+ } else if (
+ showEmptyPathAsHost &&
+ (displaySource === "" || displaySource === "/")
+ ) {
+ displaySource = host;
+ }
+
+ return dom.span(
+ {
+ key: "filename",
+ className: "frame-link-filename",
+ },
+ displaySource
+ );
+ };
+
+ /**
+ * Render the function display name.
+ * @returns {React.ReactNode}
+ */
+ #renderFunctionDisplayName = () => {
+ const { frame, showFunctionName, showAnonymousFunctionName } = this.props;
+ if (!showFunctionName) {
+ return null;
+ }
+ const functionDisplayName = frame.functionDisplayName;
+ if (functionDisplayName || showAnonymousFunctionName) {
+ return [
+ dom.span(
+ {
+ key: "function-display-name",
+ className: "frame-link-function-display-name",
+ },
+ functionDisplayName || webl10n.getStr("stacktrace.anonymousFunction")
+ ),
+ " ",
+ ];
+ }
+ return null;
+ };
+
+ render() {
+ const { showHost } = this.props;
+
+ const elements = [
+ this.#renderFunctionDisplayName(),
+ this.#renderSourceElements(),
+ ];
+
+ const unicodeHost = showHost
+ ? this.#getCurrentLocationUnicodeHostName()
+ : null;
+ if (unicodeHost) {
+ elements.push(" ");
+ elements.push(
+ dom.span(
+ {
+ key: "host",
+ className: "frame-link-host",
+ },
+ unicodeHost
+ )
+ );
+ }
+
+ return dom.span(this.#getTopElementProps(), ...elements);
+ }
+}
+
+module.exports = Frame;
diff --git a/devtools/client/shared/components/HSplitBox.js b/devtools/client/shared/components/HSplitBox.js
new file mode 100644
index 0000000000..65dfc0aaf6
--- /dev/null
+++ b/devtools/client/shared/components/HSplitBox.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+"use strict";
+
+// A box with a start and a end pane, separated by a dragable splitter that
+// allows the user to resize the relative widths of the panes.
+//
+// +-----------------------+---------------------+
+// | | |
+// | | |
+// | S |
+// | Start Pane p End Pane |
+// | l |
+// | i |
+// | t |
+// | t |
+// | e |
+// | r |
+// | | |
+// | | |
+// +-----------------------+---------------------+
+
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { assert } = require("resource://devtools/shared/DevToolsUtils.js");
+
+class HSplitBox extends Component {
+ static get propTypes() {
+ return {
+ // The contents of the start pane.
+ start: PropTypes.any.isRequired,
+
+ // The contents of the end pane.
+ end: PropTypes.any.isRequired,
+
+ // The relative width of the start pane, expressed as a number between 0 and
+ // 1. The relative width of the end pane is 1 - startWidth. For example,
+ // with startWidth = .5, both panes are of equal width; with startWidth =
+ // .25, the start panel will take up 1/4 width and the end panel will take
+ // up 3/4 width.
+ startWidth: PropTypes.number,
+
+ // A minimum css width value for the start and end panes.
+ minStartWidth: PropTypes.any,
+ minEndWidth: PropTypes.any,
+
+ // A callback fired when the user drags the splitter to resize the relative
+ // pane widths. The function is passed the startWidth value that would put
+ // the splitter underneath the users mouse.
+ onResize: PropTypes.func.isRequired,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ startWidth: 0.5,
+ minStartWidth: "20px",
+ minEndWidth: "20px",
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ mouseDown: false,
+ };
+
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ }
+
+ componentDidMount() {
+ document.defaultView.top.addEventListener("mouseup", this._onMouseUp);
+ document.defaultView.top.addEventListener("mousemove", this._onMouseMove);
+ }
+
+ componentWillUnmount() {
+ document.defaultView.top.removeEventListener("mouseup", this._onMouseUp);
+ document.defaultView.top.removeEventListener(
+ "mousemove",
+ this._onMouseMove
+ );
+ }
+
+ _onMouseDown(event) {
+ if (event.button !== 0) {
+ return;
+ }
+
+ this.setState({ mouseDown: true });
+ event.preventDefault();
+ }
+
+ _onMouseUp(event) {
+ if (event.button !== 0 || !this.state.mouseDown) {
+ return;
+ }
+
+ this.setState({ mouseDown: false });
+ event.preventDefault();
+ }
+
+ _onMouseMove(event) {
+ if (!this.state.mouseDown) {
+ return;
+ }
+
+ const rect = this.refs.box.getBoundingClientRect();
+ const { left, right } = rect;
+ const width = right - left;
+ const direction = this.refs.box.ownerDocument.dir;
+ const relative =
+ direction == "rtl" ? right - event.clientX : event.clientX - left;
+ this.props.onResize(relative / width);
+
+ event.preventDefault();
+ }
+
+ render() {
+ /* eslint-disable no-shadow */
+ const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
+ assert(
+ startWidth >= 0 && startWidth <= 1,
+ "0 <= this.props.startWidth <= 1"
+ );
+ /* eslint-enable */
+ return dom.div(
+ {
+ className: "h-split-box",
+ ref: "box",
+ },
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: startWidth, minWidth: minStartWidth },
+ },
+ start
+ ),
+
+ dom.div({
+ className: "devtools-side-splitter",
+ onMouseDown: this._onMouseDown,
+ }),
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: 1 - startWidth, minWidth: minEndWidth },
+ },
+ end
+ )
+ );
+ }
+}
+
+module.exports = HSplitBox;
diff --git a/devtools/client/shared/components/List.css b/devtools/client/shared/components/List.css
new file mode 100644
index 0000000000..1d0f668180
--- /dev/null
+++ b/devtools/client/shared/components/List.css
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* List */
+
+.list {
+ background-color: var(--theme-sidebar-background);
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ white-space: nowrap;
+ overflow: auto;
+}
+
+.list:focus, .list .list-item-content:focus {
+ outline: 0;
+}
+
+.list::-moz-focus-inner, .list .list-item-content::-moz-focus-inner {
+ border: 0;
+}
+
+.list li.current {
+ background-color: var(--theme-toolbar-hover);
+}
+
+.list:focus li.current, .list li.active.current {
+ background-color: var(--theme-emphasized-splitter-color);
+}
+
+.list:focus li:not(.current):hover,
+.list:not(:focus) li:not(.active):hover {
+ background-color: var(--theme-selection-background-hover);
+}
+
+.list .list-item-content:not(:empty) {
+ font-size: 12px;
+ overflow: auto;
+}
diff --git a/devtools/client/shared/components/List.js b/devtools/client/shared/components/List.js
new file mode 100644
index 0000000000..95c3ffe4dd
--- /dev/null
+++ b/devtools/client/shared/components/List.js
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ createFactory,
+ createRef,
+ Component,
+ cloneElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ ul,
+ li,
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ scrollIntoView,
+} = require("resource://devtools/client/shared/scroll.js");
+const {
+ preventDefaultAndStopPropagation,
+} = require("resource://devtools/client/shared/events.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["getFocusableElements", "wrapMoveFocus"],
+ "resource://devtools/client/shared/focus.js",
+ true
+);
+
+class ListItemClass extends Component {
+ static get propTypes() {
+ return {
+ active: PropTypes.bool,
+ current: PropTypes.bool,
+ onClick: PropTypes.func,
+ item: PropTypes.shape({
+ key: PropTypes.string,
+ component: PropTypes.object,
+ componentProps: PropTypes.object,
+ className: PropTypes.string,
+ }).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.contentRef = createRef();
+
+ this._setTabbableState = this._setTabbableState.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ this._setTabbableState();
+ }
+
+ componentDidUpdate() {
+ this._setTabbableState();
+ }
+
+ _onKeyDown(event) {
+ const { target, key, shiftKey } = event;
+
+ if (key !== "Tab") {
+ return;
+ }
+
+ const focusMoved = !!wrapMoveFocus(
+ getFocusableElements(this.contentRef.current),
+ target,
+ shiftKey
+ );
+ if (focusMoved) {
+ // Focus was moved to the begining/end of the list, so we need to prevent the
+ // default focus change that would happen here.
+ event.preventDefault();
+ }
+
+ event.stopPropagation();
+ }
+
+ /**
+ * Makes sure that none of the focusable elements inside the list item container are
+ * tabbable if the list item is not active. If the list item is active and focus is
+ * outside its container, focus on the first focusable element inside.
+ */
+ _setTabbableState() {
+ const elms = getFocusableElements(this.contentRef.current);
+ if (elms.length === 0) {
+ return;
+ }
+
+ if (!this.props.active) {
+ elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
+ return;
+ }
+
+ if (!elms.includes(document.activeElement)) {
+ elms[0].focus();
+ }
+ }
+
+ render() {
+ const { active, item, current, onClick } = this.props;
+ const { className, component, componentProps } = item;
+
+ return li(
+ {
+ className: `${className}${current ? " current" : ""}${
+ active ? " active" : ""
+ }`,
+ id: item.key,
+ onClick,
+ onKeyDownCapture: active ? this._onKeyDown : null,
+ },
+ div(
+ {
+ className: "list-item-content",
+ role: "presentation",
+ ref: this.contentRef,
+ },
+ cloneElement(component, componentProps || {})
+ )
+ );
+ }
+}
+
+const ListItem = createFactory(ListItemClass);
+
+class List extends Component {
+ static get propTypes() {
+ return {
+ // A list of all items to be rendered using a List component.
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ component: PropTypes.object,
+ componentProps: PropTypes.object,
+ className: PropTypes.string,
+ key: PropTypes.string.isRequired,
+ })
+ ).isRequired,
+
+ // Note: the two properties below are mutually exclusive. Only one of the
+ // label properties is necessary.
+ // ID of an element whose textual content serves as an accessible label for
+ // a list.
+ labelledBy: PropTypes.string,
+
+ // Accessibility label for a list widget.
+ label: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.listRef = createRef();
+
+ this.state = {
+ active: null,
+ current: null,
+ mouseDown: false,
+ };
+
+ this._setCurrentItem = this._setCurrentItem.bind(this);
+ this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const { active, current, mouseDown } = this.state;
+
+ return (
+ current !== nextState.current ||
+ active !== nextState.active ||
+ mouseDown === nextState.mouseDown
+ );
+ }
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ preventDefaultAndStopPropagation(e);
+ break;
+ }
+ }
+
+ /**
+ * Sets the passed in item to be the current item.
+ *
+ * @param {null|Number} index
+ * The index of the item in to be set as current, or undefined to unset the
+ * current item.
+ */
+ _setCurrentItem(index = -1, options = {}) {
+ const item = this.props.items[index];
+ if (item !== undefined && !options.preventAutoScroll) {
+ const element = document.getElementById(item.key);
+ scrollIntoView(element, {
+ ...options,
+ container: this.listRef.current,
+ });
+ }
+
+ const state = {};
+ if (this.state.active != undefined) {
+ state.active = null;
+ if (this.listRef.current !== document.activeElement) {
+ this.listRef.current.focus();
+ }
+ }
+
+ if (this.state.current !== index) {
+ this.setState({
+ ...state,
+ current: index,
+ });
+ }
+ }
+
+ /**
+ * Handles key down events in the list's container.
+ *
+ * @param {Event} e
+ */
+ _onKeyDown(e) {
+ const { active, current } = this.state;
+ if (current == null) {
+ return;
+ }
+
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+
+ const { length } = this.props.items;
+ switch (e.key) {
+ case "ArrowUp":
+ current > 0 && this._setCurrentItem(current - 1, { alignTo: "top" });
+ break;
+
+ case "ArrowDown":
+ current < length - 1 &&
+ this._setCurrentItem(current + 1, { alignTo: "bottom" });
+ break;
+
+ case "Home":
+ this._setCurrentItem(0, { alignTo: "top" });
+ break;
+
+ case "End":
+ this._setCurrentItem(length - 1, { alignTo: "bottom" });
+ break;
+
+ case "Enter":
+ case " ":
+ // On space or enter make current list item active. This means keyboard focus
+ // handling is passed on to the component within the list item.
+ if (document.activeElement === this.listRef.current) {
+ preventDefaultAndStopPropagation(e);
+ if (active !== current) {
+ this.setState({ active: current });
+ }
+ }
+ break;
+
+ case "Escape":
+ // If current list item is active, make it inactive and let keyboard focusing be
+ // handled normally.
+ preventDefaultAndStopPropagation(e);
+ if (active != null) {
+ this.setState({ active: null });
+ }
+
+ this.listRef.current.focus();
+ break;
+ }
+ }
+
+ render() {
+ const { active, current } = this.state;
+ const { items } = this.props;
+
+ return ul(
+ {
+ ref: this.listRef,
+ className: "list",
+ tabIndex: 0,
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ onMouseDown: () => this.setState({ mouseDown: true }),
+ onMouseUp: () => this.setState({ mouseDown: false }),
+ onFocus: () => {
+ if (current != null || this.state.mouseDown) {
+ return;
+ }
+
+ // Only set default current to the first list item if current item is
+ // not yet set and the focus event is not the result of a mouse
+ // interarction.
+ this._setCurrentItem(0);
+ },
+ onClick: () => {
+ // Focus should always remain on the list container itself.
+ this.listRef.current.focus();
+ },
+ onBlur: e => {
+ if (active != null) {
+ const { relatedTarget } = e;
+ if (!this.listRef.current.contains(relatedTarget)) {
+ this.setState({ active: null });
+ }
+ }
+ },
+ "aria-label": this.props.label,
+ "aria-labelledby": this.props.labelledBy,
+ "aria-activedescendant": current != null ? items[current].key : null,
+ },
+ items.map((item, index) => {
+ return ListItem({
+ item,
+ current: index === current,
+ active: index === active,
+ // We make a key unique depending on whether the list item is in active or
+ // inactive state to make sure that it is actually replaced and the tabbable
+ // state is reset.
+ key: `${item.key}-${index === active ? "active" : "inactive"}`,
+ // Since the user just clicked the item, there's no need to check if it should
+ // be scrolled into view.
+ onClick: () =>
+ this._setCurrentItem(index, { preventAutoScroll: true }),
+ });
+ })
+ );
+ }
+}
+
+module.exports = {
+ ListItem: ListItemClass,
+ List,
+};
diff --git a/devtools/client/shared/components/MdnLink.css b/devtools/client/shared/components/MdnLink.css
new file mode 100644
index 0000000000..0fef9c0bba
--- /dev/null
+++ b/devtools/client/shared/components/MdnLink.css
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Learn more links */
+
+.network-monitor .learn-more-link {
+ display: inline-block;
+ line-height: 16px;
+}
+
+.network-monitor .learn-more-link::before {
+ background-image: url(chrome://devtools/skin/images/help.svg);
+ background-size: contain;
+}
+
+.network-monitor .tree-container .learn-more-link {
+ position: absolute;
+ top: 0;
+ inset-inline-start: 2px;
+ /* Override devtools-button styles to make this button 20x20,
+ * so that the icon is vertically centered in the table row */
+ padding: 1px 0;
+}
+
+.network-monitor .tree-container tr:not(:hover) .learn-more-link {
+ opacity: 0.4;
+}
+
+.network-monitor .tabpanel-summary-value.status {
+ display: flex;
+ align-items: center;
+}
diff --git a/devtools/client/shared/components/MdnLink.js b/devtools/client/shared/components/MdnLink.js
new file mode 100644
index 0000000000..344143f54c
--- /dev/null
+++ b/devtools/client/shared/components/MdnLink.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { a } = dom;
+
+loader.lazyRequireGetter(
+ this,
+ "openDocLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+
+function MDNLink({ url, title }) {
+ return a({
+ className: "devtools-button learn-more-link",
+ title,
+ onClick: e => onLearnMoreClick(e, url),
+ });
+}
+
+MDNLink.displayName = "MDNLink";
+
+MDNLink.propTypes = {
+ url: PropTypes.string.isRequired,
+};
+
+function onLearnMoreClick(e, url) {
+ e.stopPropagation();
+ e.preventDefault();
+ openDocLink(url);
+}
+
+module.exports = MDNLink;
diff --git a/devtools/client/shared/components/NotificationBox.css b/devtools/client/shared/components/NotificationBox.css
new file mode 100644
index 0000000000..f2ff550f46
--- /dev/null
+++ b/devtools/client/shared/components/NotificationBox.css
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Layout */
+
+.notificationbox .notificationInner {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.notificationInner .messageText {
+ flex: 1;
+ width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.notificationInner .messageImage,
+.notificationbox .notificationButton,
+.notificationbox .messageCloseButton {
+ flex: none;
+}
+
+.notificationbox .notificationInner:dir(rtl) {
+ flex-direction: row-reverse;
+}
+
+/* Style */
+
+.notificationbox .notification {
+ color: var(--theme-toolbar-color);
+ background-color: var(--theme-body-background);
+ text-shadow: none;
+ border-color: var(--theme-splitter-color);
+ border-style: solid;
+ border-width: 0;
+}
+
+.notificationbox.border-top .notification {
+ border-top-width: 1px;
+}
+
+.notificationbox.border-bottom .notification {
+ border-bottom-width: 1px;
+}
+
+.notificationbox .notification[data-type="info"] {
+ color: -moz-DialogText;
+ background-color: -moz-Dialog;
+}
+
+.notificationbox .notification[data-type="new"] {
+ color: var(--theme-contrast-color);
+ background-color: var(--theme-body-alternate-emphasized-background);
+}
+
+/**
+ * Remove button borders for notifications highlighting New features.
+ */
+.notification[data-type="new"] .notificationButton {
+ border-radius: 2px;
+ border-width: 0;
+ padding: 4px;
+}
+
+.notificationbox .notification[data-type="critical"] {
+ color: white;
+ background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0));
+}
+
+.notificationbox .messageImage {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin: 6px;
+}
+
+/* Default icons for notifications */
+
+.notificationbox .messageImage[data-type="info"] {
+ background-image: url("chrome://devtools/skin/images/info.svg");
+}
+
+.notificationbox .messageImage[data-type="new"] {
+ background-image: url("chrome://global/skin/icons/whatsnew.svg");
+ fill: var(--theme-highlight-blue);
+}
+
+.notificationbox .messageImage[data-type="warning"] {
+ background-image: url("chrome://devtools/skin/images/alert.svg");
+ /* Keep the icon colored to make it more eye-catching */
+ fill: #ffbf00;
+}
+
+.notificationbox .messageImage[data-type="critical"] {
+ background-image: url("chrome://devtools/skin/images/error.svg");
+}
+
+/* Close button */
+
+.notificationbox .messageCloseButton {
+ width: 24px;
+ height: 24px;
+ margin: 2px 4px;
+ background-image: url("chrome://devtools/skin/images/close.svg");
+ background-position: center;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ border-radius: 2px;
+ border-width: 0;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-color);
+}
+
+.notificationbox .messageCloseButton:hover {
+ background-color: var(--theme-button-active-background);
+}
+
+.notificationbox .messageCloseButton:active {
+ background-color: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
+}
+
+.notificationbox.wrapping .notificationInner .messageText {
+ white-space: normal;
+}
diff --git a/devtools/client/shared/components/NotificationBox.js b/devtools/client/shared/components/NotificationBox.js
new file mode 100644
index 0000000000..53e1073d7a
--- /dev/null
+++ b/devtools/client/shared/components/NotificationBox.js
@@ -0,0 +1,403 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+);
+const { div, span, button } = dom;
+loader.lazyGetter(this, "MDNLink", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/MdnLink.js")
+ );
+});
+
+// Priority Levels
+const PriorityLevels = {
+ PRIORITY_INFO_LOW: 1,
+ PRIORITY_INFO_MEDIUM: 2,
+ PRIORITY_INFO_HIGH: 3,
+ // Type NEW should be used to highlight new features, and should be more
+ // eye-catchy than INFO level notifications.
+ PRIORITY_NEW: 4,
+ PRIORITY_WARNING_LOW: 5,
+ PRIORITY_WARNING_MEDIUM: 6,
+ PRIORITY_WARNING_HIGH: 7,
+ PRIORITY_CRITICAL_LOW: 8,
+ PRIORITY_CRITICAL_MEDIUM: 9,
+ PRIORITY_CRITICAL_HIGH: 10,
+ PRIORITY_CRITICAL_BLOCK: 11,
+};
+
+/**
+ * This component represents Notification Box - HTML alternative for
+ * <xul:notificationbox> binding.
+ *
+ * See also MDN for more info about <xul:notificationbox>:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
+ *
+ * This component can maintain its own state (list of notifications)
+ * as well as consume list of notifications provided as a prop
+ * (coming e.g. from Redux store).
+ */
+class NotificationBox extends Component {
+ static get propTypes() {
+ return {
+ // Optional box ID (used for mounted node ID attribute)
+ id: PropTypes.string,
+ /**
+ * List of notifications appended into the box. Each item of the map is an object
+ * of the following shape:
+ * - {String} label: Label to appear on the notification.
+ * - {String} value: Value used to identify the notification. Should be the same
+ * as the map key used for this notification.
+ * - {String} image: URL of image to appear on the notification. If "" then an
+ * appropriate icon for the priority level is used.
+ * - {Number} priority: Notification priority; see Priority Levels.
+ * - {Function} eventCallback: A function to call to notify you of interesting
+ things that happen with the notification box.
+ - {String} type: One of "info", "warning", or "critical" used to determine
+ what styling and icon are used for the notification.
+ * - {Array<Object>} buttons: Array of button descriptions to appear on the
+ * notification. Should be of the following shape:
+ * - {Function} callback: This function is passed 3 arguments:
+ 1) the NotificationBox component
+ the button is associated with.
+ 2) the button description as passed
+ to appendNotification.
+ 3) the element which was the target
+ of the button press event.
+ If the return value from this function
+ is not true, then the notification is
+ closed. The notification is also not
+ closed if an error is thrown.
+ - {String} label: The label to appear on the button.
+ - {String} accesskey: The accesskey attribute set on the
+ <button> element.
+ - {String} mdnUrl: URL to MDN docs. Optional but if set
+ turns button into a MDNLink and supersedes
+ all other properties. Uses Label as the title
+ for the link.
+ */
+ notifications: PropTypes.instanceOf(Map),
+ // Message that should be shown when hovering over the close button
+ closeButtonTooltip: PropTypes.string,
+ // Wraps text when passed from console window as wrapping: true
+ wrapping: PropTypes.bool,
+ // Display a top border (default to false)
+ displayBorderTop: PropTypes.bool,
+ // Display a bottom border (default to true)
+ displayBorderBottom: PropTypes.bool,
+ // Display a close button (default to true)
+ displayCloseButton: PropTypes.bool,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip"),
+ displayBorderTop: false,
+ displayBorderBottom: true,
+ displayCloseButton: true,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ notifications: new Map(),
+ };
+
+ this.appendNotification = this.appendNotification.bind(this);
+ this.removeNotification = this.removeNotification.bind(this);
+ this.getNotificationWithValue = this.getNotificationWithValue.bind(this);
+ this.getCurrentNotification = this.getCurrentNotification.bind(this);
+ this.close = this.close.bind(this);
+ this.renderButton = this.renderButton.bind(this);
+ this.renderNotification = this.renderNotification.bind(this);
+ }
+
+ /**
+ * Create a new notification and display it. If another notification is
+ * already present with a higher priority, the new notification will be
+ * added behind it. See `propTypes` for arguments description.
+ */
+ appendNotification(
+ label,
+ value,
+ image,
+ priority,
+ buttons = [],
+ eventCallback
+ ) {
+ const newState = appendNotification(this.state, {
+ label,
+ value,
+ image,
+ priority,
+ buttons,
+ eventCallback,
+ });
+
+ this.setState(newState);
+ }
+
+ /**
+ * Remove specific notification from the list.
+ */
+ removeNotification(notification) {
+ if (notification) {
+ this.close(this.state.notifications.get(notification.value));
+ }
+ }
+
+ /**
+ * Returns an object that represents a notification. It can be
+ * used to close it.
+ */
+ getNotificationWithValue(value) {
+ const notification = this.state.notifications.get(value);
+ if (!notification) {
+ return null;
+ }
+
+ // Return an object that can be used to remove the notification
+ // later (using `removeNotification` method) or directly close it.
+ return Object.assign({}, notification, {
+ close: () => {
+ this.close(notification);
+ },
+ });
+ }
+
+ getCurrentNotification() {
+ return getHighestPriorityNotification(this.state.notifications);
+ }
+
+ /**
+ * Close specified notification.
+ */
+ close(notification) {
+ if (!notification) {
+ return;
+ }
+
+ if (notification.eventCallback) {
+ notification.eventCallback("removed");
+ }
+
+ if (!this.state.notifications.get(notification.value)) {
+ return;
+ }
+
+ const newNotifications = new Map(this.state.notifications);
+ newNotifications.delete(notification.value);
+ this.setState({
+ notifications: newNotifications,
+ });
+ }
+
+ /**
+ * Render a button. A notification can have a set of custom buttons.
+ * These are used to execute custom callback. Will render a MDNLink
+ * if mdnUrl property is set.
+ */
+ renderButton(props, notification) {
+ if (props.mdnUrl != null) {
+ return MDNLink({
+ url: props.mdnUrl,
+ title: props.label,
+ });
+ }
+ const onClick = event => {
+ if (props.callback) {
+ const result = props.callback(this, props, event.target);
+ if (!result) {
+ this.close(notification);
+ }
+ event.stopPropagation();
+ }
+ };
+
+ return button(
+ {
+ key: props.label,
+ className: "notificationButton",
+ accesskey: props.accesskey,
+ onClick,
+ },
+ props.label
+ );
+ }
+
+ /**
+ * Render a notification.
+ */
+ renderNotification(notification) {
+ return div(
+ {
+ key: notification.value,
+ className: "notification",
+ "data-key": notification.value,
+ "data-type": notification.type,
+ },
+ div(
+ { className: "notificationInner" },
+ div({
+ className: "messageImage",
+ "data-type": notification.type,
+ }),
+ span(
+ {
+ className: "messageText",
+ title: notification.label,
+ },
+ notification.label
+ ),
+ notification.buttons.map(props =>
+ this.renderButton(props, notification)
+ ),
+ this.props.displayCloseButton
+ ? button({
+ className: "messageCloseButton",
+ title: this.props.closeButtonTooltip,
+ onClick: this.close.bind(this, notification),
+ })
+ : null
+ )
+ );
+ }
+
+ /**
+ * Render the top (highest priority) notification. Only one
+ * notification is rendered at a time.
+ */
+ render() {
+ const notifications = this.props.notifications || this.state.notifications;
+ const notification = getHighestPriorityNotification(notifications);
+ const content = notification ? this.renderNotification(notification) : null;
+
+ const classNames = ["notificationbox"];
+ if (this.props.wrapping) {
+ classNames.push("wrapping");
+ }
+
+ if (this.props.displayBorderBottom) {
+ classNames.push("border-bottom");
+ }
+
+ if (this.props.displayBorderTop) {
+ classNames.push("border-top");
+ }
+
+ return div(
+ {
+ className: classNames.join(" "),
+ id: this.props.id,
+ },
+ content
+ );
+ }
+}
+
+// Helpers
+
+/**
+ * Create a new notification. If another notification is already present with
+ * a higher priority, the new notification will be added behind it.
+ * See `propTypes` for arguments description.
+ */
+function appendNotification(state, props) {
+ const { label, value, image, priority, buttons, eventCallback } = props;
+
+ // Priority level must be within expected interval
+ // (see priority levels at the top of this file).
+ if (
+ priority < PriorityLevels.PRIORITY_INFO_LOW ||
+ priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK
+ ) {
+ throw new Error("Invalid notification priority " + priority);
+ }
+
+ // Custom image URL is not supported yet.
+ if (image) {
+ throw new Error("Custom image URL is not supported yet");
+ }
+
+ let type = "warning";
+ if (priority == PriorityLevels.PRIORITY_NEW) {
+ type = "new";
+ } else if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
+ type = "critical";
+ } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
+ type = "info";
+ }
+
+ if (!state.notifications) {
+ state.notifications = new Map();
+ }
+
+ const notifications = new Map(state.notifications);
+ notifications.set(value, {
+ label,
+ value,
+ image,
+ priority,
+ type,
+ buttons: Array.isArray(buttons) ? buttons : [],
+ eventCallback,
+ });
+
+ return {
+ notifications,
+ };
+}
+
+function getNotificationWithValue(notifications, value) {
+ return notifications ? notifications.get(value) : null;
+}
+
+function removeNotificationWithValue(notifications, value) {
+ const newNotifications = new Map(notifications);
+ newNotifications.delete(value);
+
+ return {
+ notifications: newNotifications,
+ };
+}
+
+function getHighestPriorityNotification(notifications) {
+ if (!notifications) {
+ return null;
+ }
+
+ let currentNotification = null;
+ // High priorities must be on top.
+ for (const [, notification] of notifications) {
+ if (
+ !currentNotification ||
+ notification.priority > currentNotification.priority
+ ) {
+ currentNotification = notification;
+ }
+ }
+
+ return currentNotification;
+}
+
+module.exports.NotificationBox = NotificationBox;
+module.exports.PriorityLevels = PriorityLevels;
+module.exports.appendNotification = appendNotification;
+module.exports.getNotificationWithValue = getNotificationWithValue;
+module.exports.removeNotificationWithValue = removeNotificationWithValue;
diff --git a/devtools/client/shared/components/SearchBox.js b/devtools/client/shared/components/SearchBox.js
new file mode 100644
index 0000000000..2733cfdc49
--- /dev/null
+++ b/devtools/client/shared/components/SearchBox.js
@@ -0,0 +1,269 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global window */
+
+"use strict";
+
+const {
+ createFactory,
+ createRef,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+loader.lazyGetter(this, "SearchBoxAutocompletePopup", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/SearchBoxAutocompletePopup.js")
+ );
+});
+loader.lazyGetter(this, "MDNLink", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/MdnLink.js")
+ );
+});
+
+loader.lazyRequireGetter(
+ this,
+ "KeyShortcuts",
+ "resource://devtools/client/shared/key-shortcuts.js"
+);
+
+class SearchBox extends PureComponent {
+ static get propTypes() {
+ return {
+ autocompleteProvider: PropTypes.func,
+ delay: PropTypes.number,
+ keyShortcut: PropTypes.string,
+ learnMoreTitle: PropTypes.string,
+ learnMoreUrl: PropTypes.string,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func.isRequired,
+ onClearButtonClick: PropTypes.func,
+ onFocus: PropTypes.func,
+ // Optional function that will be called on the focus keyboard shortcut, before
+ // setting the focus to the input. If the function returns false, the input won't
+ // get focused.
+ onFocusKeyboardShortcut: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ placeholder: PropTypes.string.isRequired,
+ summary: PropTypes.string,
+ summaryTooltip: PropTypes.string,
+ type: PropTypes.string,
+ value: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ value: props.value || "",
+ focused: false,
+ };
+
+ this.autocompleteRef = createRef();
+ this.inputRef = createRef();
+
+ this.onBlur = this.onBlur.bind(this);
+ this.onChange = this.onChange.bind(this);
+ this.onClearButtonClick = this.onClearButtonClick.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ if (!this.props.keyShortcut) {
+ return;
+ }
+
+ this.shortcuts = new KeyShortcuts({
+ window,
+ });
+ this.shortcuts.on(this.props.keyShortcut, event => {
+ if (this.props.onFocusKeyboardShortcut?.(event)) {
+ return;
+ }
+
+ event.preventDefault();
+ this.focus();
+ });
+ }
+
+ componentWillUnmount() {
+ if (this.shortcuts) {
+ this.shortcuts.destroy();
+ }
+
+ // Clean up an existing timeout.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+ }
+
+ focus() {
+ if (this.inputRef) {
+ this.inputRef.current.focus();
+ }
+ }
+
+ onChange(inputValue = "") {
+ if (this.state.value !== inputValue) {
+ this.setState({
+ focused: true,
+ value: inputValue,
+ });
+ }
+
+ if (!this.props.delay) {
+ this.props.onChange(inputValue);
+ return;
+ }
+
+ // Clean up an existing timeout before creating a new one.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+
+ // Execute the search after a timeout. It makes the UX
+ // smoother if the user is typing quickly.
+ this.searchTimeout = setTimeout(() => {
+ this.searchTimeout = null;
+ this.props.onChange(this.state.value);
+ }, this.props.delay);
+ }
+
+ onClearButtonClick() {
+ this.onChange("");
+
+ if (this.props.onClearButtonClick) {
+ this.props.onClearButtonClick();
+ }
+ }
+
+ onFocus() {
+ if (this.props.onFocus) {
+ this.props.onFocus();
+ }
+
+ this.setState({ focused: true });
+ }
+
+ onBlur() {
+ if (this.props.onBlur) {
+ this.props.onBlur();
+ }
+
+ this.setState({ focused: false });
+ }
+
+ onKeyDown(e) {
+ if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+
+ const autocomplete = this.autocompleteRef.current;
+ if (!autocomplete || autocomplete.state.list.length <= 0) {
+ return;
+ }
+
+ switch (e.key) {
+ case "ArrowDown":
+ e.preventDefault();
+ autocomplete.jumpBy(1);
+ break;
+ case "ArrowUp":
+ e.preventDefault();
+ autocomplete.jumpBy(-1);
+ break;
+ case "PageDown":
+ e.preventDefault();
+ autocomplete.jumpBy(5);
+ break;
+ case "PageUp":
+ e.preventDefault();
+ autocomplete.jumpBy(-5);
+ break;
+ case "Enter":
+ case "Tab":
+ e.preventDefault();
+ autocomplete.select();
+ break;
+ case "Escape":
+ e.preventDefault();
+ this.onBlur();
+ break;
+ case "Home":
+ e.preventDefault();
+ autocomplete.jumpToTop();
+ break;
+ case "End":
+ e.preventDefault();
+ autocomplete.jumpToBottom();
+ break;
+ }
+ }
+
+ render() {
+ const {
+ autocompleteProvider,
+ summary,
+ summaryTooltip,
+ learnMoreTitle,
+ learnMoreUrl,
+ placeholder,
+ type = "search",
+ } = this.props;
+ const { value } = this.state;
+ const showAutocomplete =
+ autocompleteProvider && this.state.focused && value !== "";
+ const showLearnMoreLink = learnMoreUrl && value === "";
+
+ const inputClassList = [`devtools-${type}input`];
+
+ return dom.div(
+ { className: "devtools-searchbox" },
+ dom.input({
+ className: inputClassList.join(" "),
+ onBlur: this.onBlur,
+ onChange: e => this.onChange(e.target.value),
+ onFocus: this.onFocus,
+ onKeyDown: this.onKeyDown,
+ placeholder,
+ ref: this.inputRef,
+ value,
+ type: "search",
+ }),
+ showLearnMoreLink &&
+ MDNLink({
+ title: learnMoreTitle,
+ url: learnMoreUrl,
+ }),
+ summary
+ ? dom.span(
+ {
+ className: "devtools-searchinput-summary",
+ title: summaryTooltip || "",
+ },
+ summary
+ )
+ : null,
+ dom.button({
+ className: "devtools-searchinput-clear",
+ hidden: value === "",
+ onClick: this.onClearButtonClick,
+ }),
+ showAutocomplete &&
+ SearchBoxAutocompletePopup({
+ autocompleteProvider,
+ filter: value,
+ onItemSelected: itemValue => this.onChange(itemValue),
+ ref: this.autocompleteRef,
+ })
+ );
+ }
+}
+
+module.exports = SearchBox;
diff --git a/devtools/client/shared/components/SearchBoxAutocompletePopup.js b/devtools/client/shared/components/SearchBoxAutocompletePopup.js
new file mode 100644
index 0000000000..08aad18872
--- /dev/null
+++ b/devtools/client/shared/components/SearchBoxAutocompletePopup.js
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+class SearchBoxAutocompletePopup extends Component {
+ static get propTypes() {
+ return {
+ /**
+ * autocompleteProvider takes search-box's entire input text as `filter` argument
+ * ie. "is:cached pr"
+ * returned value is array of objects like below
+ * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
+ * `value` is used to update the search-box input box for given item
+ * `displayValue` is used to render the autocomplete list
+ */
+ autocompleteProvider: PropTypes.func.isRequired,
+ filter: PropTypes.string.isRequired,
+ onItemSelected: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props, context) {
+ super(props, context);
+ this.state = this.computeState(props);
+ this.computeState = this.computeState.bind(this);
+ this.jumpToTop = this.jumpToTop.bind(this);
+ this.jumpToBottom = this.jumpToBottom.bind(this);
+ this.jumpBy = this.jumpBy.bind(this);
+ this.select = this.select.bind(this);
+ this.onMouseDown = this.onMouseDown.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (this.props.filter === nextProps.filter) {
+ return;
+ }
+ this.setState(this.computeState(nextProps));
+ }
+
+ componentDidUpdate() {
+ if (this.refs.selected) {
+ this.refs.selected.scrollIntoView(false);
+ }
+ }
+
+ computeState({ autocompleteProvider, filter }) {
+ const list = autocompleteProvider(filter);
+ const selectedIndex = list.length ? 0 : -1;
+
+ return { list, selectedIndex };
+ }
+
+ /**
+ * Use this method to select the top-most item
+ * This method is public, called outside of the autocomplete-popup component.
+ */
+ jumpToTop() {
+ this.setState({ selectedIndex: 0 });
+ }
+
+ /**
+ * Use this method to select the bottom-most item
+ * This method is public.
+ */
+ jumpToBottom() {
+ this.setState({ selectedIndex: this.state.list.length - 1 });
+ }
+
+ /**
+ * Increment the selected index with the provided increment value. Will cycle to the
+ * beginning/end of the list if the index exceeds the list boundaries.
+ * This method is public.
+ *
+ * @param {number} increment - No. of hops in the direction
+ */
+ jumpBy(increment = 1) {
+ const { list, selectedIndex } = this.state;
+ let nextIndex = selectedIndex + increment;
+ if (increment > 0) {
+ // Positive cycling
+ nextIndex = nextIndex > list.length - 1 ? 0 : nextIndex;
+ } else if (increment < 0) {
+ // Inverse cycling
+ nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex;
+ }
+ this.setState({ selectedIndex: nextIndex });
+ }
+
+ /**
+ * Submit the currently selected item to the onItemSelected callback
+ * This method is public.
+ */
+ select() {
+ if (this.refs.selected) {
+ this.props.onItemSelected(this.refs.selected.dataset.value);
+ }
+ }
+
+ onMouseDown(e) {
+ e.preventDefault();
+ this.setState(
+ { selectedIndex: Number(e.target.dataset.index) },
+ this.select
+ );
+ }
+
+ render() {
+ const { list } = this.state;
+
+ return (
+ !!list.length &&
+ dom.div(
+ { className: "devtools-autocomplete-popup devtools-monospace" },
+ dom.ul(
+ { className: "devtools-autocomplete-listbox" },
+ list.map((item, i) => {
+ const isSelected = this.state.selectedIndex == i;
+ const itemClassList = ["autocomplete-item"];
+
+ if (isSelected) {
+ itemClassList.push("autocomplete-selected");
+ }
+ return dom.li(
+ {
+ key: i,
+ "data-index": i,
+ "data-value": item.value,
+ className: itemClassList.join(" "),
+ ref: isSelected ? "selected" : null,
+ onMouseDown: this.onMouseDown,
+ },
+ item.displayValue
+ );
+ })
+ )
+ )
+ );
+ }
+}
+
+module.exports = SearchBoxAutocompletePopup;
diff --git a/devtools/client/shared/components/SearchModifiers.css b/devtools/client/shared/components/SearchModifiers.css
new file mode 100644
index 0000000000..b92f12b1f9
--- /dev/null
+++ b/devtools/client/shared/components/SearchModifiers.css
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Search Modifiers */
+
+.search-modifiers {
+ display: flex;
+ align-items: center;
+}
+
+.search-modifiers button {
+ padding: 2px;
+ margin: 0 3px;
+ border: none;
+ background: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 2px;
+}
+
+.search-modifiers .pipe-divider {
+ flex: none;
+ align-self: stretch;
+ width: 1px;
+ vertical-align: middle;
+ margin: 4px;
+ background-color: var(--theme-splitter-color);
+}
+
+.search-modifiers button > span {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ vertical-align: middle;
+ background-color: transparent;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ /* do not let images shrink when used as flex children */
+ flex-shrink: 0;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.search-modifiers button > span.case-match {
+ background-image: url(chrome://devtools/content/debugger/images/case-match.svg);
+}
+
+.search-modifiers button > span.regex-match {
+ background-image: url(chrome://devtools/content/debugger/images/regex-match.svg);
+}
+
+.search-modifiers button > span.whole-word-match {
+ background-image: url(chrome://devtools/content/debugger/images/whole-word-match.svg);
+}
+
+.search-modifiers button:hover {
+ fill: var(--theme-toolbar-background-hover);
+}
+
+.search-modifiers button.active > span {
+ fill: var(--theme-icon-checked-color);
+}
diff --git a/devtools/client/shared/components/SearchModifiers.js b/devtools/client/shared/components/SearchModifiers.js
new file mode 100644
index 0000000000..fb66076b83
--- /dev/null
+++ b/devtools/client/shared/components/SearchModifiers.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ span,
+ button,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+loader.lazyGetter(this, "l10n", function () {
+ const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+ return new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+ );
+});
+
+const modifierOptions = [
+ {
+ value: "regexMatch",
+ className: "regex-match-btn",
+ svgName: "regex-match",
+ tooltip: l10n.getStr("searchModifier.regExpModifier"),
+ },
+ {
+ value: "caseSensitive",
+ className: "case-sensitive-btn",
+ svgName: "case-match",
+ tooltip: l10n.getStr("searchModifier.caseSensitiveModifier"),
+ },
+ {
+ value: "wholeWord",
+ className: "whole-word-btn",
+ svgName: "whole-word-match",
+ tooltip: l10n.getStr("searchModifier.wholeWordModifier"),
+ },
+];
+
+class SearchModifiers extends Component {
+ static get propTypes() {
+ return {
+ modifiers: PropTypes.object.isRequired,
+ onToggleSearchModifier: PropTypes.func.isRequired,
+ };
+ }
+
+ #renderSearchModifier({ value, className, svgName, tooltip }) {
+ const { modifiers, onToggleSearchModifier } = this.props;
+
+ return button(
+ {
+ className: `${className} ${modifiers?.[value] ? "active" : ""}`,
+ onMouseDown: () => {
+ modifiers[value] = !modifiers[value];
+ onToggleSearchModifier(modifiers);
+ },
+ onKeyDown: e => {
+ if (e.key === "Enter") {
+ modifiers[value] = !modifiers[value];
+ onToggleSearchModifier(modifiers);
+ }
+ },
+ title: tooltip,
+ },
+ span({ className: svgName })
+ );
+ }
+
+ render() {
+ return div(
+ { className: "search-modifiers" },
+ span({ className: "pipe-divider" }),
+ modifierOptions.map(options => this.#renderSearchModifier(options))
+ );
+ }
+}
+
+module.exports = SearchModifiers;
diff --git a/devtools/client/shared/components/Sidebar.js b/devtools/client/shared/components/Sidebar.js
new file mode 100644
index 0000000000..bf9ef9938d
--- /dev/null
+++ b/devtools/client/shared/components/Sidebar.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const SidebarToggle = createFactory(
+ require("resource://devtools/client/shared/components/SidebarToggle.js")
+);
+const Tabs = createFactory(
+ require("resource://devtools/client/shared/components/tabs/Tabs.js").Tabs
+);
+
+class Sidebar extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.element])
+ .isRequired,
+ onAfterChange: PropTypes.func,
+ onAllTabsMenuClick: PropTypes.func,
+ renderOnlySelected: PropTypes.bool,
+ showAllTabsMenu: PropTypes.bool,
+ allTabsMenuButtonTooltip: PropTypes.string,
+ sidebarToggleButton: PropTypes.shape({
+ collapsed: PropTypes.bool.isRequired,
+ collapsePaneTitle: PropTypes.string.isRequired,
+ expandPaneTitle: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+ alignRight: PropTypes.bool,
+ canVerticalSplit: PropTypes.bool,
+ }),
+ activeTab: PropTypes.number,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.renderSidebarToggle = this.renderSidebarToggle.bind(this);
+ }
+
+ renderSidebarToggle() {
+ if (!this.props.sidebarToggleButton) {
+ return null;
+ }
+
+ const {
+ collapsed,
+ collapsePaneTitle,
+ expandPaneTitle,
+ onClick,
+ alignRight,
+ canVerticalSplit,
+ } = this.props.sidebarToggleButton;
+
+ return SidebarToggle({
+ collapsed,
+ collapsePaneTitle,
+ expandPaneTitle,
+ onClick,
+ alignRight,
+ canVerticalSplit,
+ });
+ }
+
+ render() {
+ const { renderSidebarToggle } = this;
+ const {
+ children,
+ onAfterChange,
+ onAllTabsMenuClick,
+ renderOnlySelected,
+ showAllTabsMenu,
+ allTabsMenuButtonTooltip,
+ activeTab,
+ } = this.props;
+
+ return Tabs(
+ {
+ onAfterChange,
+ onAllTabsMenuClick,
+ renderOnlySelected,
+ renderSidebarToggle,
+ showAllTabsMenu,
+ allTabsMenuButtonTooltip,
+ activeTab,
+ },
+ children
+ );
+ }
+}
+
+module.exports = Sidebar;
diff --git a/devtools/client/shared/components/SidebarToggle.css b/devtools/client/shared/components/SidebarToggle.css
new file mode 100644
index 0000000000..f715816d8c
--- /dev/null
+++ b/devtools/client/shared/components/SidebarToggle.css
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.sidebar-toggle {
+ display: block;
+}
+
+.sidebar-toggle::before,
+.sidebar-toggle.pane-collapsed:dir(rtl)::before {
+ background-image: url(chrome://devtools/skin/images/pane-collapse.svg);
+}
+
+.sidebar-toggle.pane-collapsed::before,
+.sidebar-toggle:dir(rtl)::before {
+ background-image: url(chrome://devtools/skin/images/pane-expand.svg);
+}
+
+.sidebar-toggle.alignRight::before {
+ transform: scaleX(-1);
+}
+
+.sidebar-toggle.alignRight {
+ order: 10
+}
+
+/* Rotate button icon 90deg if the toolbox container is
+ in vertical mode (sidebar displayed under the main panel) */
+@media (max-width: 700px) {
+ .sidebar-toggle:not(.disableVerticalBehaviour)::before {
+ transform: rotate(90deg);
+ }
+
+ /* Since RTL swaps the used images, we need to flip them
+ the other way round */
+ .sidebar-toggle:not(.disableVerticalBehaviour):dir(rtl)::before {
+ transform: rotate(-90deg);
+ }
+}
diff --git a/devtools/client/shared/components/SidebarToggle.js b/devtools/client/shared/components/SidebarToggle.js
new file mode 100644
index 0000000000..3cb2a28438
--- /dev/null
+++ b/devtools/client/shared/components/SidebarToggle.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+// Shortcuts
+const { button } = dom;
+
+/**
+ * Sidebar toggle button. This button is used to exapand
+ * and collapse Sidebar.
+ */
+class SidebarToggle extends Component {
+ static get propTypes() {
+ return {
+ // Set to true if collapsed.
+ collapsed: PropTypes.bool.isRequired,
+ // Tooltip text used when the button indicates expanded state.
+ collapsePaneTitle: PropTypes.string.isRequired,
+ // Tooltip text used when the button indicates collapsed state.
+ expandPaneTitle: PropTypes.string.isRequired,
+ // Click callback
+ onClick: PropTypes.func.isRequired,
+ // align toggle button to right
+ alignRight: PropTypes.bool,
+ // if set to true toggle-button rotate 90
+ canVerticalSplit: PropTypes.bool,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ alignRight: false,
+ canVerticalSplit: true,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ collapsed: props.collapsed,
+ };
+
+ this.onClick = this.onClick.bind(this);
+ }
+
+ // Events
+
+ onClick(event) {
+ event.stopPropagation();
+ this.setState({ collapsed: !this.state.collapsed });
+ this.props.onClick(event);
+ }
+
+ // Rendering
+
+ render() {
+ const title = this.state.collapsed
+ ? this.props.expandPaneTitle
+ : this.props.collapsePaneTitle;
+
+ const classNames = ["devtools-button", "sidebar-toggle"];
+ if (this.state.collapsed) {
+ classNames.push("pane-collapsed");
+ }
+ if (this.props.alignRight) {
+ classNames.push("alignRight");
+ }
+ if (!this.props.canVerticalSplit) {
+ classNames.push("disableVerticalBehaviour");
+ }
+
+ return button({
+ className: classNames.join(" "),
+ title,
+ onClick: this.onClick,
+ });
+ }
+}
+
+module.exports = SidebarToggle;
diff --git a/devtools/client/shared/components/SmartTrace.css b/devtools/client/shared/components/SmartTrace.css
new file mode 100644
index 0000000000..5ddfceb9aa
--- /dev/null
+++ b/devtools/client/shared/components/SmartTrace.css
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * SmartTrace Component
+ * Styles for React component at `devtools/client/shared/components/SmartTrace.js`
+ */
+
+
+.frames-group .frame{
+ display: block;
+ padding-inline-start: 16px;
+}
+
+.img.annotation-logo{
+ background-color: var(--theme-body-color);
+}
+
+
+.frames [role="list"]{
+ display: inline-grid;
+ grid-template-columns: auto 1fr;
+ grid-column-gap: 8px;
+}
+
+.frames .frame {
+ display: contents;
+ cursor: pointer;
+ white-space: normal;
+}
+
+.frames .title {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ grid-column: 1 / 2;
+ color: var(--console-output-color);
+}
+
+.frames .location {
+ color: var(--frame-link-source);
+ grid-column: -1 / -2;
+ /* Force the location to be on one line and crop at start */
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ direction: rtl;
+ text-align: end;
+}
+
+.frames .location .line {
+ color: var(--frame-link-line-color);
+}
+
+.frames .frames-list .frame:hover .location {
+ text-decoration: underline;
+}
+
+.frames .location-async-cause {
+ grid-column: 1 / -1;
+ color: var(--theme-comment);
+}
+
+/******* Group styles *******/
+.frames-group {
+ grid-column:1 / -1;
+}
+
+.frames .frames-group .group {
+ display: flex;
+}
+
+.group-description {
+ display: flex;
+ align-items: center;
+ color: var(--console-output-color);
+}
+
+.frames .frames-group .frames-list {
+ grid-column: 1 / -1;
+ margin-block-start: 2px;
+ /*
+ * We want to display each frame name on its own row, without having new lines in the
+ * clipboard when copying it. This does the trick.
+ */
+ display: grid;
+ grid-template-columns: 1fr;
+}
+
+.frames .frames-group .frames-list .frame {
+ padding-inline-start: 16px;
+ text-overflow: ellipsis;
+}
+
+.frames-group .frames-list .title {
+ grid-column: -1 / 1;
+ padding-inline-start: 16px;
+}
+
+.frames .frames-group .frames-list .frame:first-of-type {
+ border-top: 1px solid var(--theme-splitter-color);
+ padding-block-start: 4px;
+}
+
+.frames .frames-group .frames-list .frame:last-of-type {
+ margin-block-end: 4px;
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.badge {
+ background: var(--theme-toolbar-background-hover);
+ color: var(--theme-body-color);
+ border-radius: 8px;
+ padding: 1px 4px;
+ font-size: 0.9em;
+ display: inline-block;
+ text-align: center;
+ cursor: default;
+ margin-inline-start: 4px;
+}
+
+.frames .frames-group.expanded .group-description,
+.frames .frames-group.expanded .badge {
+ color: var(--theme-highlight-blue);
+}
+
+/** Images **/
+
+.frames .img.annotation-logo {
+ /* FIXME: In order to display the Framework icons, we need to find a way to share CSS
+ * from the debugger, where the background images are defined.
+ * See https://github.com/firefox-devtools/debugger.html/issues/7782.
+ */
+ display: none;
+ /*
+ background-color:var(--theme-body-color);
+ display: inline-block;
+ width: 12px;
+ height:12px;
+ vertical-align: middle;
+ margin-inline-end:4px;
+ */
+}
+
+.expanded .img.annotation-logo {
+ background-color: currentColor;
+}
+
+.group .img.arrow {
+ mask: url("chrome://devtools/content/debugger/images/arrow.svg");
+ margin-inline-end: 4px;
+ background-color: var(--theme-icon-dimmed-color);
+ width: 10px;
+ height: 10px;
+ mask-size: 100%;
+ display: inline-block;
+ transform: rotate(-90deg);
+ transition: transform 0.18s ease;
+}
+
+.group .img.arrow.expanded {
+ transform: rotate(0);
+}
+
+/* Frameworks */
+:root.theme-dark .annotation-logo:not(.angular) {
+ background-color: var(--theme-highlight-blue);
+}
diff --git a/devtools/client/shared/components/SmartTrace.js b/devtools/client/shared/components/SmartTrace.js
new file mode 100644
index 0000000000..02205ef5c5
--- /dev/null
+++ b/devtools/client/shared/components/SmartTrace.js
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+);
+const dbgL10n = new LocalizationHelper(
+ "devtools/client/locales/debugger.properties"
+);
+const Frames = createFactory(
+ require("resource://devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js")
+ .Frames
+);
+const {
+ annotateFrames,
+} = require("resource://devtools/client/debugger/src/utils/pause/frames/annotateFrames.js");
+const {
+ getDisplayURL,
+} = require("resource://devtools/client/debugger/src/utils/sources-tree/getURL.js");
+
+class SmartTrace extends Component {
+ static get propTypes() {
+ return {
+ stacktrace: PropTypes.array.isRequired,
+ onViewSource: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ // Service to enable the source map feature.
+ sourceMapURLService: PropTypes.object,
+ // A number in ms (defaults to 100) which we'll wait before doing the first actual
+ // render of this component, in order to avoid shifting layout rapidly in case the
+ // page is using sourcemap.
+ // Setting it to 0 or anything else than a number will force the first render to
+ // happen immediatly, without any delay.
+ initialRenderDelay: PropTypes.number,
+ onSourceMapResultDebounceDelay: PropTypes.number,
+ // Function that will be called when the SmartTrace is ready, i.e. once it was
+ // rendered.
+ onReady: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ initialRenderDelay: 100,
+ onSourceMapResultDebounceDelay: 200,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ hasError: false,
+ // If a sourcemap service is passed, we want to introduce a small delay in rendering
+ // so we can have the results from the sourcemap service, or render if they're not
+ // available yet.
+ ready: !props.sourceMapURLService || !this.hasInitialRenderDelay(),
+ updateCount: 0,
+ // Original positions for each indexed position
+ originalLocations: null,
+ };
+ }
+
+ getChildContext() {
+ return { l10n: dbgL10n };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ if (this.props.sourceMapURLService) {
+ this.sourceMapURLServiceUnsubscriptions = [];
+ const sourceMapInit = Promise.all(
+ this.props.stacktrace.map(
+ ({ filename, sourceId, lineNumber, columnNumber }, index) =>
+ new Promise(resolve => {
+ const callback = originalLocation => {
+ this.onSourceMapServiceChange(originalLocation, index);
+ resolve();
+ };
+
+ this.sourceMapURLServiceUnsubscriptions.push(
+ this.props.sourceMapURLService.subscribeByLocation(
+ {
+ id: sourceId,
+ url: filename.split(" -> ").pop(),
+ line: lineNumber,
+ column: columnNumber,
+ },
+ callback
+ )
+ );
+ })
+ )
+ );
+
+ // Without initial render delay, we don't have to do anything; if the frames are
+ // sourcemapped, we will get new renders from onSourceMapServiceChange.
+ if (!this.hasInitialRenderDelay()) {
+ return;
+ }
+
+ const delay = new Promise(res => {
+ this.initialRenderDelayTimeoutId = setTimeout(
+ res,
+ this.props.initialRenderDelay
+ );
+ });
+
+ // We wait either for the delay to be over (if it exists), or the sourcemapService
+ // results to be available, before setting the state as initialized.
+ Promise.race([delay, sourceMapInit]).then(() => {
+ if (this.initialRenderDelayTimeoutId) {
+ clearTimeout(this.initialRenderDelayTimeoutId);
+ }
+ this.setState(state => ({
+ // Force-update so that the ready state is detected.
+ updateCount: state.updateCount + 1,
+ ready: true,
+ }));
+ });
+ }
+ }
+
+ componentDidMount() {
+ if (this.props.onReady && this.state.ready) {
+ this.props.onReady();
+ }
+ }
+
+ shouldComponentUpdate(_, nextState) {
+ if (this.state.updateCount !== nextState.updateCount) {
+ return true;
+ }
+
+ return false;
+ }
+
+ componentDidUpdate(_, previousState) {
+ if (this.props.onReady && !previousState.ready && this.state.ready) {
+ this.props.onReady();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.initialRenderDelayTimeoutId) {
+ clearTimeout(this.initialRenderDelayTimeoutId);
+ }
+
+ if (this.onFrameLocationChangedTimeoutId) {
+ clearTimeout(this.initialRenderDelayTimeoutId);
+ }
+
+ if (this.sourceMapURLServiceUnsubscriptions) {
+ this.sourceMapURLServiceUnsubscriptions.forEach(unsubscribe => {
+ unsubscribe();
+ });
+ }
+ }
+
+ componentDidCatch(error, info) {
+ console.error(
+ "Error while rendering stacktrace:",
+ error,
+ info,
+ "props:",
+ this.props
+ );
+ this.setState(state => ({
+ // Force-update so the error is detected.
+ updateCount: state.updateCount + 1,
+ hasError: true,
+ }));
+ }
+
+ onSourceMapServiceChange(originalLocation, index) {
+ this.setState(({ originalLocations }) => {
+ if (!originalLocations) {
+ originalLocations = Array.from({
+ length: this.props.stacktrace.length,
+ });
+ }
+ return {
+ originalLocations: [
+ ...originalLocations.slice(0, index),
+ originalLocation,
+ ...originalLocations.slice(index + 1),
+ ],
+ };
+ });
+
+ if (this.onFrameLocationChangedTimeoutId) {
+ clearTimeout(this.onFrameLocationChangedTimeoutId);
+ }
+
+ // Since a trace may have many original positions, we don't want to
+ // constantly re-render every time one becomes available. To avoid this,
+ // we only update the component after an initial timeout, and on a
+ // debounce edge as more positions load after that.
+ if (this.state.ready === true) {
+ this.onFrameLocationChangedTimeoutId = setTimeout(() => {
+ this.setState(state => ({
+ updateCount: state.updateCount + 1,
+ }));
+ }, this.props.onSourceMapResultDebounceDelay);
+ }
+ }
+
+ hasInitialRenderDelay() {
+ return (
+ Number.isFinite(this.props.initialRenderDelay) &&
+ this.props.initialRenderDelay > 0
+ );
+ }
+
+ render() {
+ if (
+ this.state.hasError ||
+ (this.hasInitialRenderDelay() && !this.state.ready)
+ ) {
+ return null;
+ }
+
+ const { onViewSourceInDebugger, onViewSource, stacktrace } = this.props;
+ const { originalLocations } = this.state;
+
+ const frames = annotateFrames(
+ stacktrace.map(
+ (
+ {
+ filename,
+ sourceId,
+ lineNumber,
+ columnNumber,
+ functionName,
+ asyncCause,
+ },
+ i
+ ) => {
+ const generatedLocation = {
+ sourceUrl: filename.split(" -> ").pop(),
+ sourceId,
+ line: lineNumber,
+ column: columnNumber,
+ };
+ let location = generatedLocation;
+
+ const originalLocation = originalLocations?.[i];
+ if (originalLocation) {
+ location = {
+ sourceUrl: originalLocation.url,
+ line: originalLocation.line,
+ column: originalLocation.column,
+ };
+ }
+
+ return {
+ id: "fake-frame-id-" + i,
+ displayName: functionName,
+ asyncCause,
+ generatedLocation,
+ location,
+ source: {
+ url: location.sourceUrl,
+ displayURL: getDisplayURL(location.sourceUrl),
+ },
+ };
+ }
+ )
+ );
+
+ return Frames({
+ frames,
+ selectFrame: (cx, { generatedLocation }) => {
+ const viewSource = onViewSourceInDebugger || onViewSource;
+
+ viewSource({
+ id: generatedLocation.sourceId,
+ url: generatedLocation.sourceUrl,
+ line: generatedLocation.line,
+ column: generatedLocation.column,
+ });
+ },
+ getFrameTitle: url => {
+ return l10n.getFormatStr("frame.viewsourceindebugger", url);
+ },
+ disableFrameTruncate: true,
+ disableContextMenu: true,
+ frameworkGroupingOn: true,
+ displayFullUrl: !this.state || !this.state.originalLocations,
+ panel: "webconsole",
+ });
+ }
+}
+
+SmartTrace.childContextTypes = {
+ l10n: PropTypes.object,
+};
+
+module.exports = SmartTrace;
diff --git a/devtools/client/shared/components/StackTrace.js b/devtools/client/shared/components/StackTrace.js
new file mode 100644
index 0000000000..e89e79b344
--- /dev/null
+++ b/devtools/client/shared/components/StackTrace.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const Frame = createFactory(
+ require("resource://devtools/client/shared/components/Frame.js")
+);
+
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/webconsole.properties"
+);
+
+class AsyncFrameClass extends Component {
+ static get propTypes() {
+ return {
+ asyncCause: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { asyncCause } = this.props;
+
+ return dom.span(
+ { className: "frame-link-async-cause" },
+ l10n.getFormatStr("stacktrace.asyncStack", asyncCause)
+ );
+ }
+}
+
+class StackTrace extends Component {
+ static get propTypes() {
+ return {
+ stacktrace: PropTypes.array.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ // Service to enable the source map feature.
+ sourceMapURLService: PropTypes.object,
+ };
+ }
+
+ render() {
+ const { stacktrace, onViewSourceInDebugger, sourceMapURLService } =
+ this.props;
+
+ if (!stacktrace || !stacktrace.length) {
+ return null;
+ }
+
+ const frames = [];
+ stacktrace.forEach((s, i) => {
+ if (s.asyncCause) {
+ frames.push(
+ "\t",
+ AsyncFrame({
+ key: `${i}-asyncframe`,
+ asyncCause: s.asyncCause,
+ }),
+ "\n"
+ );
+ }
+
+ frames.push(
+ "\t",
+ Frame({
+ key: `${i}-frame`,
+ frame: {
+ functionDisplayName: s.functionName,
+ source: s.filename,
+ line: s.lineNumber,
+ column: s.columnNumber,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true,
+ showFullSourceUrl: true,
+ onClick: onViewSourceInDebugger,
+ sourceMapURLService,
+ }),
+ "\n"
+ );
+ });
+
+ return dom.div({ className: "stack-trace" }, frames);
+ }
+}
+
+const AsyncFrame = createFactory(AsyncFrameClass);
+
+module.exports = StackTrace;
diff --git a/devtools/client/shared/components/Tree.css b/devtools/client/shared/components/Tree.css
new file mode 100644
index 0000000000..3a0667cd5c
--- /dev/null
+++ b/devtools/client/shared/components/Tree.css
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* We can remove the outline since we do add our own focus style on nodes */
+.tree:focus {
+ outline: none;
+}
+
+.tree.inline {
+ display: inline-block;
+}
+
+.tree.nowrap {
+ white-space: nowrap;
+}
+
+.tree.noselect {
+ user-select: none;
+}
+
+.tree .tree-node {
+ display: flex;
+}
+
+.tree .tree-node:not(.focused):hover {
+ background-color: var(--theme-selection-background-hover);
+}
+
+.tree-indent {
+ display: inline-block;
+ width: 12px;
+ margin-inline-start: 3px;
+ border-inline-start: 1px solid #a2d1ff;
+ flex-shrink: 0;
+ height: 0;
+}
+
+.tree-node[data-expandable="false"] .tree-last-indent {
+ /* The 13px value is taken from the total width and margins of the arrow
+ element of expandables nodes (10px width + 3px margins). That way the
+ node's text are indented the same for both expandable and non-expandable
+ nodes */
+ margin-inline-end: 13px;
+}
+
+.tree .tree-node[data-expandable="true"] {
+ cursor: default;
+}
+
+.tree-node button.arrow {
+ mask: url("chrome://devtools/content/debugger/images/arrow.svg") no-repeat center;
+ mask-size: 10px;
+ vertical-align: -1px;
+ width: 10px;
+ height: 10px;
+ border: 0;
+ padding: 0;
+ margin-inline-end: 4px;
+ transform-origin: center center;
+ transition: transform 125ms var(--animation-curve);
+ background-color: var(--theme-icon-dimmed-color);
+}
+
+.tree-node button.arrow:not(.expanded) {
+ transform: rotate(-90deg);
+}
+
+html[dir="rtl"] .tree-node button.arrow:not(.expanded) {
+ transform: rotate(90deg);
+}
+
+.tree .tree-node.focused {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+/* Invert text selection color in selected rows */
+.tree .tree-node.focused ::selection {
+ color: var(--theme-selection-background);
+ background-color: var(--theme-selection-color);
+}
+
+.tree-node.focused button.arrow {
+ background-color: currentColor;
+}
diff --git a/devtools/client/shared/components/Tree.js b/devtools/client/shared/components/Tree.js
new file mode 100644
index 0000000000..8c6c8fdd2f
--- /dev/null
+++ b/devtools/client/shared/components/Tree.js
@@ -0,0 +1,1055 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const React = require("resource://devtools/client/shared/vendor/react.js");
+const { Component, createFactory } = React;
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+// depth
+const AUTO_EXPAND_DEPTH = 0;
+
+// Simplied selector targetting elements that can receive the focus,
+// full version at https://stackoverflow.com/questions/1599660.
+const FOCUSABLE_SELECTOR = [
+ "a[href]:not([tabindex='-1'])",
+ "button:not([disabled], [tabindex='-1'])",
+ "iframe:not([tabindex='-1'])",
+ "input:not([disabled], [tabindex='-1'])",
+ "select:not([disabled], [tabindex='-1'])",
+ "textarea:not([disabled], [tabindex='-1'])",
+ "[tabindex]:not([tabindex='-1'])",
+].join(", ");
+
+/**
+ * An arrow that displays whether its node is expanded (â–¼) or collapsed
+ * (â–¶). When its node has no children, it is hidden.
+ */
+class ArrowExpander extends Component {
+ static get propTypes() {
+ return {
+ expanded: PropTypes.bool,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.expanded !== nextProps.expanded;
+ }
+
+ render() {
+ const { expanded } = this.props;
+
+ const classNames = ["arrow"];
+ if (expanded) {
+ classNames.push("expanded");
+ }
+ return dom.button({
+ className: classNames.join(" "),
+ });
+ }
+}
+
+const treeIndent = dom.span({ className: "tree-indent" }, "\u200B");
+const treeLastIndent = dom.span(
+ { className: "tree-indent tree-last-indent" },
+ "\u200B"
+);
+
+class TreeNode extends Component {
+ static get propTypes() {
+ return {
+ id: PropTypes.any.isRequired,
+ index: PropTypes.number.isRequired,
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ active: PropTypes.bool.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ item: PropTypes.any.isRequired,
+ isExpandable: PropTypes.bool.isRequired,
+ onClick: PropTypes.func,
+ shouldItemUpdate: PropTypes.func,
+ renderItem: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.treeNodeRef = React.createRef();
+
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ // Make sure that none of the focusable elements inside the tree node
+ // container are tabbable if the tree node is not active. If the tree node
+ // is active and focus is outside its container, focus on the first
+ // focusable element inside.
+ const elms = this.getFocusableElements();
+ if (this.props.active) {
+ const doc = this.treeNodeRef.current.ownerDocument;
+ if (elms.length && !elms.includes(doc.activeElement)) {
+ elms[0].focus();
+ }
+ } else {
+ elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
+ }
+ }
+
+ shouldComponentUpdate(nextProps) {
+ return (
+ this.props.item !== nextProps.item ||
+ (this.props.shouldItemUpdate &&
+ this.props.shouldItemUpdate(this.props.item, nextProps.item)) ||
+ this.props.focused !== nextProps.focused ||
+ this.props.expanded !== nextProps.expanded
+ );
+ }
+
+ /**
+ * Get a list of all elements that are focusable with a keyboard inside the
+ * tree node.
+ */
+ getFocusableElements() {
+ return this.treeNodeRef.current
+ ? Array.from(
+ this.treeNodeRef.current.querySelectorAll(FOCUSABLE_SELECTOR)
+ )
+ : [];
+ }
+
+ /**
+ * Wrap and move keyboard focus to first/last focusable element inside the
+ * tree node to prevent the focus from escaping the tree node boundaries.
+ * element).
+ *
+ * @param {DOMNode} current currently focused element
+ * @param {Boolean} back direction
+ * @return {Boolean} true there is a newly focused element.
+ */
+ _wrapMoveFocus(current, back) {
+ const elms = this.getFocusableElements();
+ let next;
+
+ if (elms.length === 0) {
+ return false;
+ }
+
+ if (back) {
+ if (elms.indexOf(current) === 0) {
+ next = elms[elms.length - 1];
+ next.focus();
+ }
+ } else if (elms.indexOf(current) === elms.length - 1) {
+ next = elms[0];
+ next.focus();
+ }
+
+ return !!next;
+ }
+
+ _onKeyDown(e) {
+ const { target, key, shiftKey } = e;
+
+ if (key !== "Tab") {
+ return;
+ }
+
+ const focusMoved = this._wrapMoveFocus(target, shiftKey);
+ if (focusMoved) {
+ // Focus was moved to the begining/end of the list, so we need to prevent
+ // the default focus change that would happen here.
+ e.preventDefault();
+ }
+
+ e.stopPropagation();
+ }
+
+ render() {
+ const {
+ depth,
+ id,
+ item,
+ focused,
+ active,
+ expanded,
+ renderItem,
+ isExpandable,
+ } = this.props;
+
+ const arrow = isExpandable
+ ? ArrowExpanderFactory({
+ item,
+ expanded,
+ })
+ : null;
+
+ let ariaExpanded;
+ if (this.props.isExpandable) {
+ ariaExpanded = false;
+ }
+ if (this.props.expanded) {
+ ariaExpanded = true;
+ }
+
+ const indents = Array.from({ length: depth }, (_, i) => {
+ if (i == depth - 1) {
+ return treeLastIndent;
+ }
+ return treeIndent;
+ });
+
+ const items = indents.concat(
+ renderItem(item, depth, focused, arrow, expanded)
+ );
+
+ return dom.div(
+ {
+ id,
+ className: `tree-node${focused ? " focused" : ""}${
+ active ? " active" : ""
+ }`,
+ onClick: this.props.onClick,
+ onKeyDownCapture: active ? this._onKeyDown : null,
+ role: "treeitem",
+ ref: this.treeNodeRef,
+ "aria-level": depth + 1,
+ "aria-expanded": ariaExpanded,
+ "data-expandable": this.props.isExpandable,
+ },
+ ...items
+ );
+ }
+}
+
+const ArrowExpanderFactory = createFactory(ArrowExpander);
+const TreeNodeFactory = createFactory(TreeNode);
+
+/**
+ * Create a function that calls the given function `fn` only once per animation
+ * frame.
+ *
+ * @param {Function} fn
+ * @param {Object} options: object that contains the following properties:
+ * - {Function} getDocument: A function that return the document
+ * the component is rendered in.
+ * @returns {Function}
+ */
+function oncePerAnimationFrame(fn, { getDocument }) {
+ let animationId = null;
+ let argsToPass = null;
+ return function (...args) {
+ argsToPass = args;
+ if (animationId !== null) {
+ return;
+ }
+
+ const doc = getDocument();
+ if (!doc) {
+ return;
+ }
+
+ animationId = doc.defaultView.requestAnimationFrame(() => {
+ fn.call(this, ...argsToPass);
+ animationId = null;
+ argsToPass = null;
+ });
+ };
+}
+
+/**
+ * A generic tree component. See propTypes for the public API.
+ *
+ * This tree component doesn't make any assumptions about the structure of your
+ * tree data. Whether children are computed on demand, or stored in an array in
+ * the parent's `_children` property, it doesn't matter. We only require the
+ * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded`
+ * functions.
+ *
+ * This tree component is well tested and reliable. See the tests in ./tests
+ * and its usage in the memory panel in mozilla-central.
+ *
+ * This tree component doesn't make any assumptions about how to render items in
+ * the tree. You provide a `renderItem` function, and this component will ensure
+ * that only those items whose parents are expanded and which are visible in the
+ * viewport are rendered. The `renderItem` function could render the items as a
+ * "traditional" tree or as rows in a table or anything else. It doesn't
+ * restrict you to only one certain kind of tree.
+ *
+ * The tree comes with basic styling for the indent, the arrow, as well as
+ * hovered and focused styles which can be override in CSS.
+ *
+ * ### Example Usage
+ *
+ * Suppose we have some tree data where each item has this form:
+ *
+ * {
+ * id: Number,
+ * label: String,
+ * parent: Item or null,
+ * children: Array of child items,
+ * expanded: bool,
+ * }
+ *
+ * Here is how we could render that data with this component:
+ *
+ * class MyTree extends Component {
+ * static get propTypes() {
+ * // The root item of the tree, with the form described above.
+ * return {
+ * root: PropTypes.object.isRequired
+ * };
+ * },
+ *
+ * render() {
+ * return Tree({
+ * itemHeight: 20, // px
+ *
+ * getRoots: () => [this.props.root],
+ *
+ * getParent: item => item.parent,
+ * getChildren: item => item.children,
+ * getKey: item => item.id,
+ * isExpanded: item => item.expanded,
+ *
+ * renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ * let className = "my-tree-item";
+ * if (isFocused) {
+ * className += " focused";
+ * }
+ * return dom.div({
+ * className,
+ * },
+ * arrow,
+ * // And here is the label for this item.
+ * dom.span({ className: "my-tree-item-label" }, item.label)
+ * );
+ * },
+ *
+ * onExpand: item => dispatchExpandActionToRedux(item),
+ * onCollapse: item => dispatchCollapseActionToRedux(item),
+ * });
+ * }
+ * }
+ */
+class Tree extends Component {
+ static get propTypes() {
+ return {
+ // Required props
+
+ // A function to get an item's parent, or null if it is a root.
+ //
+ // Type: getParent(item: Item) -> Maybe<Item>
+ //
+ // Example:
+ //
+ // // The parent of this item is stored in its `parent` property.
+ // getParent: item => item.parent
+ getParent: PropTypes.func.isRequired,
+
+ // A function to get an item's children.
+ //
+ // Type: getChildren(item: Item) -> [Item]
+ //
+ // Example:
+ //
+ // // This item's children are stored in its `children` property.
+ // getChildren: item => item.children
+ getChildren: PropTypes.func.isRequired,
+
+ // A function to check if the tree node for the item should be updated.
+ //
+ // Type: shouldItemUpdate(prevItem: Item, nextItem: Item) -> Boolean
+ //
+ // Example:
+ //
+ // // This item should be updated if it's type is a long string
+ // shouldItemUpdate: (prevItem, nextItem) =>
+ // nextItem.type === "longstring"
+ shouldItemUpdate: PropTypes.func,
+
+ // A function which takes an item and ArrowExpander component instance and
+ // returns a component, or text, or anything else that React considers
+ // renderable.
+ //
+ // Type: renderItem(item: Item,
+ // depth: Number,
+ // isFocused: Boolean,
+ // arrow: ReactComponent,
+ // isExpanded: Boolean) -> ReactRenderable
+ //
+ // Example:
+ //
+ // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ // let className = "my-tree-item";
+ // if (isFocused) {
+ // className += " focused";
+ // }
+ // return dom.div(
+ // {
+ // className,
+ // style: { marginLeft: depth * 10 + "px" }
+ // },
+ // arrow,
+ // dom.span({ className: "my-tree-item-label" }, item.label)
+ // );
+ // },
+ renderItem: PropTypes.func.isRequired,
+
+ // A function which returns the roots of the tree (forest).
+ //
+ // Type: getRoots() -> [Item]
+ //
+ // Example:
+ //
+ // // In this case, we only have one top level, root item. You could
+ // // return multiple items if you have many top level items in your
+ // // tree.
+ // getRoots: () => [this.props.rootOfMyTree]
+ getRoots: PropTypes.func.isRequired,
+
+ // A function to get a unique key for the given item. This helps speed up
+ // React's rendering a *TON*.
+ //
+ // Type: getKey(item: Item) -> String
+ //
+ // Example:
+ //
+ // getKey: item => `my-tree-item-${item.uniqueId}`
+ getKey: PropTypes.func.isRequired,
+
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ //
+ // Type: isExpanded(item: Item) -> Boolean
+ //
+ // Example:
+ //
+ // isExpanded: item => item.expanded,
+ isExpanded: PropTypes.func.isRequired,
+
+ // Optional props
+
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
+
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
+ // Should auto expand all new items or just the new items under the first
+ // root item.
+ autoExpandAll: PropTypes.bool,
+
+ // Auto expand a node only if number of its children
+ // are less than autoExpandNodeChildrenLimit
+ autoExpandNodeChildrenLimit: PropTypes.number,
+
+ // Note: the two properties below are mutually exclusive. Only one of the
+ // label properties is necessary.
+ // ID of an element whose textual content serves as an accessible label
+ // for a tree.
+ labelledby: PropTypes.string,
+ // Accessibility label for a tree widget.
+ label: PropTypes.string,
+
+ // Optional event handlers for when items are expanded or collapsed.
+ // Useful for dispatching redux events and updating application state,
+ // maybe lazily loading subtrees from a worker, etc.
+ //
+ // Type:
+ // onExpand(item: Item)
+ // onCollapse(item: Item)
+ //
+ // Example:
+ //
+ // onExpand: item => dispatchExpandActionToRedux(item)
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ // The currently active (keyboard) item, if any such item exists.
+ active: PropTypes.any,
+ // Optional event handler called with the current focused node when the
+ // Enter key is pressed. Can be useful to allow further keyboard actions
+ // within the tree node.
+ onActivate: PropTypes.func,
+ isExpandable: PropTypes.func,
+ // Additional classes to add to the root element.
+ className: PropTypes.string,
+ // style object to be applied to the root element.
+ style: PropTypes.object,
+ // Prevents blur when Tree loses focus
+ preventBlur: PropTypes.bool,
+ initiallyExpanded: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ autoExpandDepth: AUTO_EXPAND_DEPTH,
+ autoExpandAll: true,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ autoExpanded: new Set(),
+ };
+
+ this.treeRef = React.createRef();
+
+ const opaf = fn =>
+ oncePerAnimationFrame(fn, {
+ getDocument: () =>
+ this.treeRef.current && this.treeRef.current.ownerDocument,
+ });
+
+ this._onExpand = opaf(this._onExpand).bind(this);
+ this._onCollapse = opaf(this._onCollapse).bind(this);
+ this._focusPrevNode = opaf(this._focusPrevNode).bind(this);
+ this._focusNextNode = opaf(this._focusNextNode).bind(this);
+ this._focusParentNode = opaf(this._focusParentNode).bind(this);
+ this._focusFirstNode = opaf(this._focusFirstNode).bind(this);
+ this._focusLastNode = opaf(this._focusLastNode).bind(this);
+
+ this._autoExpand = this._autoExpand.bind(this);
+ this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
+ this._preventEvent = this._preventEvent.bind(this);
+ this._dfs = this._dfs.bind(this);
+ this._dfsFromRoots = this._dfsFromRoots.bind(this);
+ this._focus = this._focus.bind(this);
+ this._activate = this._activate.bind(this);
+ this._scrollNodeIntoView = this._scrollNodeIntoView.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ this._nodeIsExpandable = this._nodeIsExpandable.bind(this);
+ }
+
+ componentDidMount() {
+ this._autoExpand();
+ if (this.props.focused) {
+ this._scrollNodeIntoView(this.props.focused);
+ }
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this._autoExpand();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.props.focused && prevProps.focused !== this.props.focused) {
+ this._scrollNodeIntoView(this.props.focused);
+ }
+ }
+
+ _autoExpand() {
+ const { autoExpandDepth, autoExpandNodeChildrenLimit, initiallyExpanded } =
+ this.props;
+
+ if (!autoExpandDepth && !initiallyExpanded) {
+ return;
+ }
+
+ // Automatically expand the first autoExpandDepth levels for new items. Do
+ // not use the usual DFS infrastructure because we don't want to ignore
+ // collapsed nodes. Any initially expanded items will be expanded regardless
+ // of how deep they are.
+ const autoExpand = (item, currentDepth) => {
+ const initial = initiallyExpanded && initiallyExpanded(item);
+
+ if (!initial && currentDepth >= autoExpandDepth) {
+ return;
+ }
+
+ const children = this.props.getChildren(item);
+ if (
+ !initial &&
+ autoExpandNodeChildrenLimit &&
+ children.length > autoExpandNodeChildrenLimit
+ ) {
+ return;
+ }
+
+ if (!this.state.autoExpanded.has(item)) {
+ this.props.onExpand(item);
+ this.state.autoExpanded.add(item);
+ }
+
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(children[i], currentDepth + 1);
+ }
+ };
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ if (this.props.autoExpandAll) {
+ for (let i = 0; i < length; i++) {
+ autoExpand(roots[i], 0);
+ }
+ } else if (length != 0) {
+ autoExpand(roots[0], 0);
+
+ if (initiallyExpanded) {
+ for (let i = 1; i < length; i++) {
+ if (initiallyExpanded(roots[i])) {
+ autoExpand(roots[i], 0);
+ }
+ }
+ }
+ }
+ }
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ this._preventEvent(e);
+ break;
+ }
+ }
+
+ _preventEvent(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.nativeEvent) {
+ if (e.nativeEvent.preventDefault) {
+ e.nativeEvent.preventDefault();
+ }
+ if (e.nativeEvent.stopPropagation) {
+ e.nativeEvent.stopPropagation();
+ }
+ }
+ }
+
+ /**
+ * Perform a pre-order depth-first search from item.
+ */
+ _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
+ traversal.push({ item, depth: _depth });
+
+ if (!this.props.isExpanded(item)) {
+ return traversal;
+ }
+
+ const nextDepth = _depth + 1;
+
+ if (nextDepth > maxDepth) {
+ return traversal;
+ }
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(children[i], maxDepth, traversal, nextDepth);
+ }
+
+ return traversal;
+ }
+
+ /**
+ * Perform a pre-order depth-first search over the whole forest.
+ */
+ _dfsFromRoots(maxDepth = Infinity) {
+ const traversal = [];
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(roots[i], maxDepth, traversal);
+ }
+
+ return traversal;
+ }
+
+ /**
+ * Expands current row.
+ *
+ * @param {Object} item
+ * @param {Boolean} expandAllChildren
+ */
+ _onExpand(item, expandAllChildren) {
+ if (this.props.onExpand) {
+ this.props.onExpand(item);
+
+ if (expandAllChildren) {
+ const children = this._dfs(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this.props.onExpand(children[i].item);
+ }
+ }
+ }
+ }
+
+ /**
+ * Collapses current row.
+ *
+ * @param {Object} item
+ */
+ _onCollapse(item) {
+ if (this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ }
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Object|undefined} item
+ * The item to be focused, or undefined to focus no item.
+ *
+ * @param {Object|undefined} options
+ * An options object which can contain:
+ * - dir: "up" or "down" to indicate if we should scroll the element
+ * to the top or the bottom of the scrollable container when
+ * the element is off canvas.
+ */
+ _focus(item, options = {}) {
+ const { preventAutoScroll } = options;
+ if (item && !preventAutoScroll) {
+ this._scrollNodeIntoView(item, options);
+ }
+
+ if (this.props.active != undefined) {
+ this._activate(undefined);
+ const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
+ if (this.treeRef.current !== doc.activeElement) {
+ this.treeRef.current.focus();
+ }
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ }
+
+ /**
+ * Sets the passed in item to be the active item.
+ *
+ * @param {Object|undefined} item
+ * The item to be activated, or undefined to activate no item.
+ */
+ _activate(item) {
+ if (this.props.onActivate) {
+ this.props.onActivate(item);
+ }
+ }
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Object|undefined} item
+ * The item to be scrolled to.
+ *
+ * @param {Object|undefined} options
+ * An options object which can contain:
+ * - dir: "up" or "down" to indicate if we should scroll the element
+ * to the top or the bottom of the scrollable container when
+ * the element is off canvas.
+ */
+ _scrollNodeIntoView(item, options = {}) {
+ if (item !== undefined) {
+ const treeElement = this.treeRef.current;
+ const doc = treeElement && treeElement.ownerDocument;
+ const element = doc.getElementById(this.props.getKey(item));
+
+ if (element) {
+ const { top, bottom } = element.getBoundingClientRect();
+ const closestScrolledParent = node => {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.scrollHeight > node.clientHeight) {
+ return node;
+ }
+ return closestScrolledParent(node.parentNode);
+ };
+ const scrolledParent = closestScrolledParent(treeElement);
+ const scrolledParentRect = scrolledParent
+ ? scrolledParent.getBoundingClientRect()
+ : null;
+ const isVisible =
+ !scrolledParent ||
+ (top >= scrolledParentRect.top &&
+ bottom <= scrolledParentRect.bottom);
+
+ if (!isVisible) {
+ const { alignTo } = options;
+ const scrollToTop = alignTo
+ ? alignTo === "top"
+ : !scrolledParentRect || top < scrolledParentRect.top;
+ element.scrollIntoView(scrollToTop);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the state to have no focused item.
+ */
+ _onBlur(e) {
+ if (this.props.active != undefined) {
+ const { relatedTarget } = e;
+ if (!this.treeRef.current.contains(relatedTarget)) {
+ this._activate(undefined);
+ }
+ } else if (!this.props.preventBlur) {
+ this._focus(undefined);
+ }
+ }
+
+ /**
+ * Handles key down events in the tree's container.
+ *
+ * @param {Event} e
+ */
+ // eslint-disable-next-line complexity
+ _onKeyDown(e) {
+ if (this.props.focused == null) {
+ return;
+ }
+
+ // Allow parent nodes to use navigation arrows with modifiers.
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+ const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
+
+ switch (e.key) {
+ case "ArrowUp":
+ this._focusPrevNode();
+ return;
+
+ case "ArrowDown":
+ this._focusNextNode();
+ return;
+
+ case "ArrowLeft":
+ if (
+ this.props.isExpanded(this.props.focused) &&
+ this._nodeIsExpandable(this.props.focused)
+ ) {
+ this._onCollapse(this.props.focused);
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case "ArrowRight":
+ if (
+ this._nodeIsExpandable(this.props.focused) &&
+ !this.props.isExpanded(this.props.focused)
+ ) {
+ this._onExpand(this.props.focused);
+ } else {
+ this._focusNextNode();
+ }
+ return;
+
+ case "Home":
+ this._focusFirstNode();
+ return;
+
+ case "End":
+ this._focusLastNode();
+ return;
+
+ case "Enter":
+ case " ":
+ if (this.treeRef.current === doc.activeElement) {
+ this._preventEvent(e);
+ if (this.props.active !== this.props.focused) {
+ this._activate(this.props.focused);
+ }
+ }
+ return;
+
+ case "Escape":
+ this._preventEvent(e);
+ if (this.props.active != undefined) {
+ this._activate(undefined);
+ }
+
+ if (this.treeRef.current !== doc.activeElement) {
+ this.treeRef.current.focus();
+ }
+ }
+ }
+
+ /**
+ * Sets the previous node relative to the currently focused item, to focused.
+ */
+ _focusPrevNode() {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the previous node in the DFS, if it exists. If it
+ // doesn't exist, we're at the first node already.
+
+ let prev;
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ for (let i = 0; i < length; i++) {
+ const item = traversal[i].item;
+ if (item === this.props.focused) {
+ break;
+ }
+ prev = item;
+ }
+ if (prev === undefined) {
+ return;
+ }
+
+ this._focus(prev, { alignTo: "top" });
+ }
+
+ /**
+ * Handles the down arrow key which will focus either the next child
+ * or sibling row.
+ */
+ _focusNextNode() {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the next node in the DFS, if it exists. If it
+ // doesn't exist, we're at the last node already.
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let i = 0;
+
+ while (i < length) {
+ if (traversal[i].item === this.props.focused) {
+ break;
+ }
+ i++;
+ }
+
+ if (i + 1 < traversal.length) {
+ this._focus(traversal[i + 1].item, { alignTo: "bottom" });
+ }
+ }
+
+ /**
+ * Handles the left arrow key, going back up to the current rows'
+ * parent row.
+ */
+ _focusParentNode() {
+ const parent = this.props.getParent(this.props.focused);
+ if (!parent) {
+ this._focusPrevNode(this.props.focused);
+ return;
+ }
+
+ this._focus(parent, { alignTo: "top" });
+ }
+
+ _focusFirstNode() {
+ const traversal = this._dfsFromRoots();
+ this._focus(traversal[0].item, { alignTo: "top" });
+ }
+
+ _focusLastNode() {
+ const traversal = this._dfsFromRoots();
+ const lastIndex = traversal.length - 1;
+ this._focus(traversal[lastIndex].item, { alignTo: "bottom" });
+ }
+
+ _nodeIsExpandable(item) {
+ return this.props.isExpandable
+ ? this.props.isExpandable(item)
+ : !!this.props.getChildren(item).length;
+ }
+
+ render() {
+ const traversal = this._dfsFromRoots();
+ const { active, focused } = this.props;
+
+ const nodes = traversal.map((v, i) => {
+ const { item, depth } = traversal[i];
+ const key = this.props.getKey(item, i);
+ const focusedKey = focused ? this.props.getKey(focused, i) : null;
+ return TreeNodeFactory({
+ // We make a key unique depending on whether the tree node is in active
+ // or inactive state to make sure that it is actually replaced and the
+ // tabbable state is reset.
+ key: `${key}-${active === item ? "active" : "inactive"}`,
+ id: key,
+ index: i,
+ item,
+ depth,
+ shouldItemUpdate: this.props.shouldItemUpdate,
+ renderItem: this.props.renderItem,
+ focused: focusedKey === key,
+ active: active === item,
+ expanded: this.props.isExpanded(item),
+ isExpandable: this._nodeIsExpandable(item),
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onClick: e => {
+ // We can stop the propagation since click handler on the node can be
+ // created in `renderItem`.
+ e.stopPropagation();
+
+ // Since the user just clicked the node, there's no need to check if
+ // it should be scrolled into view.
+ this._focus(item, { preventAutoScroll: true });
+ if (this.props.isExpanded(item)) {
+ this.props.onCollapse(item, e.altKey);
+ } else {
+ this.props.onExpand(item, e.altKey);
+ }
+
+ // Focus should always remain on the tree container itself.
+ this.treeRef.current.focus();
+ },
+ });
+ });
+
+ const style = Object.assign({}, this.props.style || {});
+
+ return dom.div(
+ {
+ className: `tree ${this.props.className ? this.props.className : ""}`,
+ ref: this.treeRef,
+ role: "tree",
+ tabIndex: "0",
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ onFocus: ({ nativeEvent }) => {
+ if (focused || !nativeEvent || !this.treeRef.current) {
+ return;
+ }
+
+ const { explicitOriginalTarget } = nativeEvent;
+ // Only set default focus to the first tree node if the focus came
+ // from outside the tree (e.g. by tabbing to the tree from other
+ // external elements).
+ if (
+ explicitOriginalTarget !== this.treeRef.current &&
+ !this.treeRef.current.contains(explicitOriginalTarget)
+ ) {
+ this._focus(traversal[0].item);
+ }
+ },
+ onBlur: this._onBlur,
+ "aria-label": this.props.label,
+ "aria-labelledby": this.props.labelledby,
+ "aria-activedescendant": focused && this.props.getKey(focused),
+ style,
+ },
+ nodes
+ );
+ }
+}
+
+module.exports = Tree;
diff --git a/devtools/client/shared/components/VirtualizedTree.js b/devtools/client/shared/components/VirtualizedTree.js
new file mode 100644
index 0000000000..4f8dab1bd5
--- /dev/null
+++ b/devtools/client/shared/components/VirtualizedTree.js
@@ -0,0 +1,1071 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-env browser */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ scrollIntoView,
+} = require("resource://devtools/client/shared/scroll.js");
+const {
+ preventDefaultAndStopPropagation,
+} = require("resource://devtools/client/shared/events.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["wrapMoveFocus", "getFocusableElements"],
+ "resource://devtools/client/shared/focus.js",
+ true
+);
+
+const AUTO_EXPAND_DEPTH = 0;
+const NUMBER_OF_OFFSCREEN_ITEMS = 1;
+
+/**
+ * A fast, generic, expandable and collapsible tree component.
+ *
+ * This tree component is fast: it can handle trees with *many* items. It only
+ * renders the subset of those items which are visible in the viewport. It's
+ * been battle tested on huge trees in the memory panel. We've optimized tree
+ * traversal and rendering, even in the presence of cross-compartment wrappers.
+ *
+ * This tree component doesn't make any assumptions about the structure of your
+ * tree data. Whether children are computed on demand, or stored in an array in
+ * the parent's `_children` property, it doesn't matter. We only require the
+ * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded`
+ * functions.
+ *
+ * This tree component is well tested and reliable. See
+ * devtools/client/shared/components/test/mochitest/test_tree_* and its usage in
+ * the performance and memory panels.
+ *
+ * This tree component doesn't make any assumptions about how to render items in
+ * the tree. You provide a `renderItem` function, and this component will ensure
+ * that only those items whose parents are expanded and which are visible in the
+ * viewport are rendered. The `renderItem` function could render the items as a
+ * "traditional" tree or as rows in a table or anything else. It doesn't
+ * restrict you to only one certain kind of tree.
+ *
+ * The only requirement is that every item in the tree render as the same
+ * height. This is required in order to compute which items are visible in the
+ * viewport in constant time.
+ *
+ * ### Example Usage
+ *
+ * Suppose we have some tree data where each item has this form:
+ *
+ * {
+ * id: Number,
+ * label: String,
+ * parent: Item or null,
+ * children: Array of child items,
+ * expanded: bool,
+ * }
+ *
+ * Here is how we could render that data with this component:
+ *
+ * class MyTree extends Component {
+ * static get propTypes() {
+ * // The root item of the tree, with the form described above.
+ * return {
+ * root: PropTypes.object.isRequired
+ * };
+ * }
+ *
+ * render() {
+ * return Tree({
+ * itemHeight: 20, // px
+ *
+ * getRoots: () => [this.props.root],
+ *
+ * getParent: item => item.parent,
+ * getChildren: item => item.children,
+ * getKey: item => item.id,
+ * isExpanded: item => item.expanded,
+ *
+ * renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ * let className = "my-tree-item";
+ * if (isFocused) {
+ * className += " focused";
+ * }
+ * return dom.div(
+ * {
+ * className,
+ * // Apply 10px nesting per expansion depth.
+ * style: { marginLeft: depth * 10 + "px" }
+ * },
+ * // Here is the expando arrow so users can toggle expansion and
+ * // collapse state.
+ * arrow,
+ * // And here is the label for this item.
+ * dom.span({ className: "my-tree-item-label" }, item.label)
+ * );
+ * },
+ *
+ * onExpand: item => dispatchExpandActionToRedux(item),
+ * onCollapse: item => dispatchCollapseActionToRedux(item),
+ * });
+ * }
+ * }
+ */
+class Tree extends Component {
+ static get propTypes() {
+ return {
+ // Required props
+
+ // A function to get an item's parent, or null if it is a root.
+ //
+ // Type: getParent(item: Item) -> Maybe<Item>
+ //
+ // Example:
+ //
+ // // The parent of this item is stored in its `parent` property.
+ // getParent: item => item.parent
+ getParent: PropTypes.func.isRequired,
+
+ // A function to get an item's children.
+ //
+ // Type: getChildren(item: Item) -> [Item]
+ //
+ // Example:
+ //
+ // // This item's children are stored in its `children` property.
+ // getChildren: item => item.children
+ getChildren: PropTypes.func.isRequired,
+
+ // A function which takes an item and ArrowExpander component instance and
+ // returns a component, or text, or anything else that React considers
+ // renderable.
+ //
+ // Type: renderItem(item: Item,
+ // depth: Number,
+ // isFocused: Boolean,
+ // arrow: ReactComponent,
+ // isExpanded: Boolean) -> ReactRenderable
+ //
+ // Example:
+ //
+ // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ // let className = "my-tree-item";
+ // if (isFocused) {
+ // className += " focused";
+ // }
+ // return dom.div(
+ // {
+ // className,
+ // style: { marginLeft: depth * 10 + "px" }
+ // },
+ // arrow,
+ // dom.span({ className: "my-tree-item-label" }, item.label)
+ // );
+ // },
+ renderItem: PropTypes.func.isRequired,
+
+ // A function which returns the roots of the tree (forest).
+ //
+ // Type: getRoots() -> [Item]
+ //
+ // Example:
+ //
+ // // In this case, we only have one top level, root item. You could
+ // // return multiple items if you have many top level items in your
+ // // tree.
+ // getRoots: () => [this.props.rootOfMyTree]
+ getRoots: PropTypes.func.isRequired,
+
+ // A function to get a unique key for the given item. This helps speed up
+ // React's rendering a *TON*.
+ //
+ // Type: getKey(item: Item) -> String
+ //
+ // Example:
+ //
+ // getKey: item => `my-tree-item-${item.uniqueId}`
+ getKey: PropTypes.func.isRequired,
+
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ //
+ // Type: isExpanded(item: Item) -> Boolean
+ //
+ // Example:
+ //
+ // isExpanded: item => item.expanded,
+ isExpanded: PropTypes.func.isRequired,
+
+ // The height of an item in the tree including margin and padding, in
+ // pixels.
+ itemHeight: PropTypes.number.isRequired,
+
+ // Optional props
+
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
+
+ // The currently active (keyboard) item, if any such item exists.
+ active: PropTypes.any,
+
+ // Handle when item is activated with a keyboard (using Space or Enter)
+ onActivate: PropTypes.func,
+
+ // The currently shown item, if any such item exists.
+ shown: PropTypes.any,
+
+ // Indicates if pressing ArrowRight key should only expand expandable node
+ // or if the selection should also move to the next node.
+ preventNavigationOnArrowRight: PropTypes.bool,
+
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
+
+ // Note: the two properties below are mutually exclusive. Only one of the
+ // label properties is necessary.
+ // ID of an element whose textual content serves as an accessible label for
+ // a tree.
+ labelledby: PropTypes.string,
+ // Accessibility label for a tree widget.
+ label: PropTypes.string,
+
+ // Optional event handlers for when items are expanded or collapsed. Useful
+ // for dispatching redux events and updating application state, maybe lazily
+ // loading subtrees from a worker, etc.
+ //
+ // Type:
+ // onExpand(item: Item)
+ // onCollapse(item: Item)
+ //
+ // Example:
+ //
+ // onExpand: item => dispatchExpandActionToRedux(item)
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ autoExpandDepth: AUTO_EXPAND_DEPTH,
+ preventNavigationOnArrowRight: true,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ scroll: 0,
+ height: window.innerHeight,
+ seen: new Set(),
+ mouseDown: false,
+ };
+
+ this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
+ this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
+ this._onScroll = oncePerAnimationFrame(this._onScroll).bind(this);
+ this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
+ this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
+ this._focusParentNode = oncePerAnimationFrame(this._focusParentNode).bind(
+ this
+ );
+ this._focusFirstNode = oncePerAnimationFrame(this._focusFirstNode).bind(
+ this
+ );
+ this._focusLastNode = oncePerAnimationFrame(this._focusLastNode).bind(this);
+
+ this._autoExpand = this._autoExpand.bind(this);
+ this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
+ this._updateHeight = this._updateHeight.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._dfs = this._dfs.bind(this);
+ this._dfsFromRoots = this._dfsFromRoots.bind(this);
+ this._focus = this._focus.bind(this);
+ this._activate = this._activate.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ window.addEventListener("resize", this._onResize);
+ this._autoExpand();
+ this._updateHeight();
+ this._scrollItemIntoView();
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this._autoExpand();
+ this._updateHeight();
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const { scroll, height, seen, mouseDown } = this.state;
+
+ return (
+ scroll !== nextState.scroll ||
+ height !== nextState.height ||
+ seen !== nextState.seen ||
+ mouseDown === nextState.mouseDown
+ );
+ }
+
+ componentDidUpdate() {
+ this._scrollItemIntoView();
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this._onResize);
+ }
+
+ _scrollItemIntoView() {
+ const { shown } = this.props;
+ if (!shown) {
+ return;
+ }
+
+ this._scrollIntoView(shown);
+ }
+
+ _autoExpand() {
+ if (!this.props.autoExpandDepth) {
+ return;
+ }
+
+ // Automatically expand the first autoExpandDepth levels for new items. Do
+ // not use the usual DFS infrastructure because we don't want to ignore
+ // collapsed nodes.
+ const autoExpand = (item, currentDepth) => {
+ if (
+ currentDepth >= this.props.autoExpandDepth ||
+ this.state.seen.has(item)
+ ) {
+ return;
+ }
+
+ this.props.onExpand(item);
+ this.state.seen.add(item);
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(children[i], currentDepth + 1);
+ }
+ };
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(roots[i], 0);
+ }
+ }
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ preventDefaultAndStopPropagation(e);
+ break;
+ }
+ }
+
+ /**
+ * Updates the state's height based on clientHeight.
+ */
+ _updateHeight() {
+ this.setState({ height: this.refs.tree.clientHeight });
+ }
+
+ /**
+ * Perform a pre-order depth-first search from item.
+ */
+ _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
+ traversal.push({ item, depth: _depth });
+
+ if (!this.props.isExpanded(item)) {
+ return traversal;
+ }
+
+ const nextDepth = _depth + 1;
+
+ if (nextDepth > maxDepth) {
+ return traversal;
+ }
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(children[i], maxDepth, traversal, nextDepth);
+ }
+
+ return traversal;
+ }
+
+ /**
+ * Perform a pre-order depth-first search over the whole forest.
+ */
+ _dfsFromRoots(maxDepth = Infinity) {
+ const traversal = [];
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(roots[i], maxDepth, traversal);
+ }
+
+ return traversal;
+ }
+
+ /**
+ * Expands current row.
+ *
+ * @param {Object} item
+ * @param {Boolean} expandAllChildren
+ */
+ _onExpand(item, expandAllChildren) {
+ if (this.props.onExpand) {
+ this.props.onExpand(item);
+
+ if (expandAllChildren) {
+ const children = this._dfs(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this.props.onExpand(children[i].item);
+ }
+ }
+ }
+ }
+
+ /**
+ * Collapses current row.
+ *
+ * @param {Object} item
+ */
+ _onCollapse(item) {
+ if (this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ }
+
+ /**
+ * Scroll item into view. Depending on whether the item is already rendered,
+ * we might have to calculate the position of the item based on its index and
+ * the item height.
+ *
+ * @param {Object} item
+ * The item to be scrolled into view.
+ * @param {Number|undefined} index
+ * The index of the item in a full DFS traversal (ignoring collapsed
+ * nodes) or undefined.
+ * @param {Object} options
+ * Optional information regarding item's requested alignement when
+ * scrolling.
+ */
+ _scrollIntoView(item, index, options = {}) {
+ const treeElement = this.refs.tree;
+ if (!treeElement) {
+ return;
+ }
+
+ const element = document.getElementById(this.props.getKey(item));
+ if (element) {
+ scrollIntoView(element, { ...options, container: treeElement });
+ return;
+ }
+
+ if (index == null) {
+ // If index is not provided, determine item index from traversal.
+ const traversal = this._dfsFromRoots();
+ index = traversal.findIndex(({ item: i }) => i === item);
+ }
+
+ if (index == null || index < 0) {
+ return;
+ }
+
+ const { itemHeight } = this.props;
+ const { clientHeight, scrollTop } = treeElement;
+ const elementTop = index * itemHeight;
+ let scrollTo;
+ if (scrollTop >= elementTop + itemHeight) {
+ scrollTo = elementTop;
+ } else if (scrollTop + clientHeight <= elementTop) {
+ scrollTo = elementTop + itemHeight - clientHeight;
+ }
+
+ if (scrollTo != undefined) {
+ treeElement.scrollTo({
+ left: 0,
+ top: scrollTo,
+ });
+ }
+ }
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Number} index
+ * The index of the item in a full DFS traversal (ignoring collapsed
+ * nodes). Ignored if `item` is undefined.
+ *
+ * @param {Object|undefined} item
+ * The item to be focused, or undefined to focus no item.
+ */
+ _focus(index, item, options = {}) {
+ if (item !== undefined && !options.preventAutoScroll) {
+ this._scrollIntoView(item, index, options);
+ }
+
+ if (this.props.active != null) {
+ this._activate(null);
+ if (this.refs.tree !== this.activeElement) {
+ this.refs.tree.focus();
+ }
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ }
+
+ _activate(item) {
+ if (this.props.onActivate) {
+ this.props.onActivate(item);
+ }
+ }
+
+ /**
+ * Update state height and tree's scrollTop if necessary.
+ */
+ _onResize() {
+ // When tree size changes without direct user action, scroll top cat get re-set to 0
+ // (for example, when tree height changes via CSS rule change). We need to ensure that
+ // the tree's scrollTop is in sync with the scroll state.
+ if (this.state.scroll !== this.refs.tree.scrollTop) {
+ this.refs.tree.scrollTo({ left: 0, top: this.state.scroll });
+ }
+
+ this._updateHeight();
+ }
+
+ /**
+ * Fired on a scroll within the tree's container, updates
+ * the stored position of the view port to handle virtual view rendering.
+ *
+ * @param {Event} e
+ */
+ _onScroll(e) {
+ this.setState({
+ scroll: Math.max(this.refs.tree.scrollTop, 0),
+ height: this.refs.tree.clientHeight,
+ });
+ }
+
+ /**
+ * Handles key down events in the tree's container.
+ *
+ * @param {Event} e
+ */
+ // eslint-disable-next-line complexity
+ _onKeyDown(e) {
+ if (this.props.focused == null) {
+ return;
+ }
+
+ // Allow parent nodes to use navigation arrows with modifiers.
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+
+ switch (e.key) {
+ case "ArrowUp":
+ this._focusPrevNode();
+ break;
+
+ case "ArrowDown":
+ this._focusNextNode();
+ break;
+
+ case "ArrowLeft":
+ if (
+ this.props.isExpanded(this.props.focused) &&
+ this.props.getChildren(this.props.focused).length
+ ) {
+ this._onCollapse(this.props.focused);
+ } else {
+ this._focusParentNode();
+ }
+ break;
+
+ case "ArrowRight":
+ if (
+ this.props.getChildren(this.props.focused).length &&
+ !this.props.isExpanded(this.props.focused)
+ ) {
+ this._onExpand(this.props.focused);
+ } else if (!this.props.preventNavigationOnArrowRight) {
+ this._focusNextNode();
+ }
+ break;
+
+ case "Home":
+ this._focusFirstNode();
+ break;
+
+ case "End":
+ this._focusLastNode();
+ break;
+
+ case "Enter":
+ case " ":
+ // On space or enter make focused tree node active. This means keyboard focus
+ // handling is passed on to the tree node itself.
+ if (this.refs.tree === this.activeElement) {
+ preventDefaultAndStopPropagation(e);
+ if (this.props.active !== this.props.focused) {
+ this._activate(this.props.focused);
+ }
+ }
+ break;
+
+ case "Escape":
+ preventDefaultAndStopPropagation(e);
+ if (this.props.active != null) {
+ this._activate(null);
+ }
+
+ if (this.refs.tree !== this.activeElement) {
+ this.refs.tree.focus();
+ }
+ break;
+ }
+ }
+
+ get activeElement() {
+ return this.refs.tree.ownerDocument.activeElement;
+ }
+
+ _focusFirstNode() {
+ const traversal = this._dfsFromRoots();
+ this._focus(0, traversal[0].item, { alignTo: "top" });
+ }
+
+ _focusLastNode() {
+ const traversal = this._dfsFromRoots();
+ const lastIndex = traversal.length - 1;
+ this._focus(lastIndex, traversal[lastIndex].item, { alignTo: "bottom" });
+ }
+
+ /**
+ * Sets the previous node relative to the currently focused item, to focused.
+ */
+ _focusPrevNode() {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the previous node in the DFS, if it exists. If it
+ // doesn't exist, we're at the first node already.
+
+ let prev;
+ let prevIndex;
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ for (let i = 0; i < length; i++) {
+ const item = traversal[i].item;
+ if (item === this.props.focused) {
+ break;
+ }
+ prev = item;
+ prevIndex = i;
+ }
+
+ if (prev === undefined) {
+ return;
+ }
+
+ this._focus(prevIndex, prev, { alignTo: "top" });
+ }
+
+ /**
+ * Handles the down arrow key which will focus either the next child
+ * or sibling row.
+ */
+ _focusNextNode() {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the next node in the DFS, if it exists. If it
+ // doesn't exist, we're at the last node already.
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let i = 0;
+
+ while (i < length) {
+ if (traversal[i].item === this.props.focused) {
+ break;
+ }
+ i++;
+ }
+
+ if (i + 1 < traversal.length) {
+ this._focus(i + 1, traversal[i + 1].item, { alignTo: "bottom" });
+ }
+ }
+
+ /**
+ * Handles the left arrow key, going back up to the current rows'
+ * parent row.
+ */
+ _focusParentNode() {
+ const parent = this.props.getParent(this.props.focused);
+ if (!parent) {
+ return;
+ }
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let parentIndex = 0;
+ for (; parentIndex < length; parentIndex++) {
+ if (traversal[parentIndex].item === parent) {
+ break;
+ }
+ }
+
+ this._focus(parentIndex, parent, { alignTo: "top" });
+ }
+
+ render() {
+ const traversal = this._dfsFromRoots();
+
+ // 'begin' and 'end' are the index of the first (at least partially) visible item
+ // and the index after the last (at least partially) visible item, respectively.
+ // `NUMBER_OF_OFFSCREEN_ITEMS` is removed from `begin` and added to `end` so that
+ // the top and bottom of the page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS`
+ // previous and next items respectively, which helps the user to see fewer empty
+ // gaps when scrolling quickly.
+ const { itemHeight, active, focused } = this.props;
+ const { scroll, height } = this.state;
+ const begin = Math.max(
+ ((scroll / itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS,
+ 0
+ );
+ const end =
+ Math.ceil((scroll + height) / itemHeight) + NUMBER_OF_OFFSCREEN_ITEMS;
+ const toRender = traversal.slice(begin, end);
+ const topSpacerHeight = begin * itemHeight;
+ const bottomSpacerHeight = Math.max(traversal.length - end, 0) * itemHeight;
+
+ const nodes = [
+ dom.div({
+ key: "top-spacer",
+ role: "presentation",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: topSpacerHeight + "px",
+ },
+ }),
+ ];
+
+ for (let i = 0; i < toRender.length; i++) {
+ const index = begin + i;
+ const first = index == 0;
+ const last = index == traversal.length - 1;
+ const { item, depth } = toRender[i];
+ const key = this.props.getKey(item);
+ nodes.push(
+ TreeNode({
+ // We make a key unique depending on whether the tree node is in active or
+ // inactive state to make sure that it is actually replaced and the tabbable
+ // state is reset.
+ key: `${key}-${active === item ? "active" : "inactive"}`,
+ index,
+ first,
+ last,
+ item,
+ depth,
+ id: key,
+ renderItem: this.props.renderItem,
+ focused: focused === item,
+ active: active === item,
+ expanded: this.props.isExpanded(item),
+ hasChildren: !!this.props.getChildren(item).length,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ // Since the user just clicked the node, there's no need to check if
+ // it should be scrolled into view.
+ onClick: () =>
+ this._focus(begin + i, item, { preventAutoScroll: true }),
+ })
+ );
+ }
+
+ nodes.push(
+ dom.div({
+ key: "bottom-spacer",
+ role: "presentation",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: bottomSpacerHeight + "px",
+ },
+ })
+ );
+
+ return dom.div(
+ {
+ className: "tree",
+ ref: "tree",
+ role: "tree",
+ tabIndex: "0",
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ onScroll: this._onScroll,
+ onMouseDown: () => this.setState({ mouseDown: true }),
+ onMouseUp: () => this.setState({ mouseDown: false }),
+ onFocus: () => {
+ if (focused || this.state.mouseDown) {
+ return;
+ }
+
+ // Only set default focus to the first tree node if focused node is
+ // not yet set and the focus event is not the result of a mouse
+ // interarction.
+ this._focus(begin, toRender[0].item);
+ },
+ onBlur: e => {
+ if (active != null) {
+ const { relatedTarget } = e;
+ if (!this.refs.tree.contains(relatedTarget)) {
+ this._activate(null);
+ }
+ }
+ },
+ onClick: () => {
+ // Focus should always remain on the tree container itself.
+ this.refs.tree.focus();
+ },
+ "aria-label": this.props.label,
+ "aria-labelledby": this.props.labelledby,
+ "aria-activedescendant": focused && this.props.getKey(focused),
+ style: {
+ padding: 0,
+ margin: 0,
+ },
+ },
+ nodes
+ );
+ }
+}
+
+/**
+ * An arrow that displays whether its node is expanded (â–¼) or collapsed
+ * (â–¶). When its node has no children, it is hidden.
+ */
+class ArrowExpanderClass extends Component {
+ static get propTypes() {
+ return {
+ item: PropTypes.any.isRequired,
+ visible: PropTypes.bool.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.item !== nextProps.item ||
+ this.props.visible !== nextProps.visible ||
+ this.props.expanded !== nextProps.expanded
+ );
+ }
+
+ render() {
+ const attrs = {
+ className: "arrow theme-twisty",
+ // To collapse/expand the tree rows use left/right arrow keys.
+ tabIndex: "-1",
+ "aria-hidden": true,
+ onClick: this.props.expanded
+ ? () => this.props.onCollapse(this.props.item)
+ : e => this.props.onExpand(this.props.item, e.altKey),
+ };
+
+ if (this.props.expanded) {
+ attrs.className += " open";
+ }
+
+ if (!this.props.visible) {
+ attrs.style = {
+ visibility: "hidden",
+ };
+ }
+
+ return dom.div(attrs);
+ }
+}
+
+class TreeNodeClass extends Component {
+ static get propTypes() {
+ return {
+ id: PropTypes.any.isRequired,
+ focused: PropTypes.bool.isRequired,
+ active: PropTypes.bool.isRequired,
+ item: PropTypes.any.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ hasChildren: PropTypes.bool.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ index: PropTypes.number.isRequired,
+ first: PropTypes.bool,
+ last: PropTypes.bool,
+ onClick: PropTypes.func,
+ onCollapse: PropTypes.func.isRequired,
+ depth: PropTypes.number.isRequired,
+ renderItem: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ // Make sure that none of the focusable elements inside the tree node container are
+ // tabbable if the tree node is not active. If the tree node is active and focus is
+ // outside its container, focus on the first focusable element inside.
+ const elms = getFocusableElements(this.refs.treenode);
+ if (elms.length === 0) {
+ return;
+ }
+
+ if (!this.props.active) {
+ elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
+ return;
+ }
+
+ if (!elms.includes(this.refs.treenode.ownerDocument.activeElement)) {
+ elms[0].focus();
+ }
+ }
+
+ _onKeyDown(e) {
+ const { target, key, shiftKey } = e;
+
+ if (key !== "Tab") {
+ return;
+ }
+
+ const focusMoved = !!wrapMoveFocus(
+ getFocusableElements(this.refs.treenode),
+ target,
+ shiftKey
+ );
+ if (focusMoved) {
+ // Focus was moved to the begining/end of the list, so we need to prevent the
+ // default focus change that would happen here.
+ e.preventDefault();
+ }
+
+ e.stopPropagation();
+ }
+
+ render() {
+ const arrow = ArrowExpander({
+ item: this.props.item,
+ expanded: this.props.expanded,
+ visible: this.props.hasChildren,
+ onExpand: this.props.onExpand,
+ onCollapse: this.props.onCollapse,
+ });
+
+ const classList = ["tree-node", "div"];
+ if (this.props.index % 2) {
+ classList.push("tree-node-odd");
+ }
+ if (this.props.first) {
+ classList.push("tree-node-first");
+ }
+ if (this.props.last) {
+ classList.push("tree-node-last");
+ }
+ if (this.props.active) {
+ classList.push("tree-node-active");
+ }
+
+ let ariaExpanded;
+ if (this.props.hasChildren) {
+ ariaExpanded = false;
+ }
+ if (this.props.expanded) {
+ ariaExpanded = true;
+ }
+
+ return dom.div(
+ {
+ id: this.props.id,
+ className: classList.join(" "),
+ role: "treeitem",
+ ref: "treenode",
+ "aria-level": this.props.depth + 1,
+ onClick: this.props.onClick,
+ onKeyDownCapture: this.props.active ? this._onKeyDown : undefined,
+ "aria-expanded": ariaExpanded,
+ "data-expanded": this.props.expanded ? "" : undefined,
+ "data-depth": this.props.depth,
+ style: {
+ padding: 0,
+ margin: 0,
+ },
+ },
+
+ this.props.renderItem(
+ this.props.item,
+ this.props.depth,
+ this.props.focused,
+ arrow,
+ this.props.expanded
+ )
+ );
+ }
+}
+
+const ArrowExpander = createFactory(ArrowExpanderClass);
+const TreeNode = createFactory(TreeNodeClass);
+
+/**
+ * Create a function that calls the given function `fn` only once per animation
+ * frame.
+ *
+ * @param {Function} fn
+ * @returns {Function}
+ */
+function oncePerAnimationFrame(fn) {
+ let animationId = null;
+ let argsToPass = null;
+ return function (...args) {
+ argsToPass = args;
+ if (animationId !== null) {
+ return;
+ }
+
+ animationId = requestAnimationFrame(() => {
+ fn.call(this, ...argsToPass);
+ animationId = null;
+ argsToPass = null;
+ });
+ };
+}
+
+module.exports = Tree;
diff --git a/devtools/client/shared/components/VisibilityHandler.js b/devtools/client/shared/components/VisibilityHandler.js
new file mode 100644
index 0000000000..be1f3c5f93
--- /dev/null
+++ b/devtools/client/shared/components/VisibilityHandler.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Helper class to disable panel rendering when it is in background.
+ *
+ * Toolbox code hides the iframes when switching to another panel
+ * and triggers `visibilitychange` events.
+ *
+ * See devtools/client/framework/toolbox.js:setIframeVisible().
+ */
+
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+class VisibilityHandler extends Component {
+ static get propTypes() {
+ return {
+ children: PropTypes.element.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onVisibilityChange = this.onVisibilityChange.bind(this);
+ }
+
+ componentDidMount() {
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
+ }
+
+ shouldComponentUpdate() {
+ return document.visibilityState == "visible";
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
+ }
+
+ onVisibilityChange() {
+ if (document.visibilityState == "visible") {
+ this.forceUpdate();
+ }
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+module.exports = VisibilityHandler;
diff --git a/devtools/client/shared/components/menu/MenuButton.js b/devtools/client/shared/components/menu/MenuButton.js
new file mode 100644
index 0000000000..3367987c3c
--- /dev/null
+++ b/devtools/client/shared/components/menu/MenuButton.js
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+"use strict";
+
+// A button that toggles a doorhanger menu.
+
+const flags = require("resource://devtools/shared/flags.js");
+const {
+ createRef,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { button } = dom;
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+loader.lazyRequireGetter(
+ this,
+ "HTMLTooltip",
+ "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "focusableSelector",
+ "resource://devtools/client/shared/focus.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "createPortal",
+ "resource://devtools/client/shared/vendor/react-dom.js",
+ true
+);
+
+// Return a copy of |obj| minus |fields|.
+const omit = (obj, fields) => {
+ const objCopy = { ...obj };
+ for (const field of fields) {
+ delete objCopy[field];
+ }
+ return objCopy;
+};
+
+class MenuButton extends PureComponent {
+ static get propTypes() {
+ return {
+ // The toolbox document that will be used for rendering the menu popup.
+ toolboxDoc: PropTypes.object.isRequired,
+
+ // A text content for the button.
+ label: PropTypes.string,
+
+ // URL of the icon to associate with the MenuButton. (Optional)
+ // e.g. chrome://devtools/skin/image/foo.svg
+ icon: PropTypes.string,
+
+ // An optional ID to assign to the menu's container tooltip object.
+ menuId: PropTypes.string,
+
+ // The preferred side of the anchor element to display the menu.
+ // Defaults to "bottom".
+ menuPosition: PropTypes.string.isRequired,
+
+ // The offset of the menu from the anchor element.
+ // Defaults to -5.
+ menuOffset: PropTypes.number.isRequired,
+
+ // The menu content.
+ children: PropTypes.any,
+
+ // Callback function to be invoked when the button is clicked.
+ onClick: PropTypes.func,
+
+ // Callback function to be invoked when the child panel is closed.
+ onCloseButton: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ menuPosition: "bottom",
+ menuOffset: -5,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.showMenu = this.showMenu.bind(this);
+ this.hideMenu = this.hideMenu.bind(this);
+ this.toggleMenu = this.toggleMenu.bind(this);
+ this.onHidden = this.onHidden.bind(this);
+ this.onClick = this.onClick.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onTouchStart = this.onTouchStart.bind(this);
+
+ this.buttonRef = createRef();
+
+ this.state = {
+ expanded: false,
+ // In tests, initialize the menu immediately.
+ isMenuInitialized: flags.testing || false,
+ win: props.toolboxDoc.defaultView.top,
+ };
+ this.ignoreNextClick = false;
+
+ this.initializeTooltip();
+ }
+
+ componentDidMount() {
+ if (!this.state.isMenuInitialized) {
+ // Initialize the menu when the button is focused or moused over.
+ for (const event of ["focus", "mousemove"]) {
+ this.buttonRef.current.addEventListener(
+ event,
+ () => {
+ if (!this.state.isMenuInitialized) {
+ this.setState({ isMenuInitialized: true });
+ }
+ },
+ { once: true }
+ );
+ }
+ }
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ // If the window changes, we need to regenerate the HTMLTooltip or else the
+ // XUL wrapper element will appear above (in terms of z-index) the old
+ // window, and not the new.
+ const win = nextProps.toolboxDoc.defaultView.top;
+ if (
+ nextProps.toolboxDoc !== this.props.toolboxDoc ||
+ this.state.win !== win ||
+ nextProps.menuId !== this.props.menuId
+ ) {
+ this.setState({ win });
+ this.resetTooltip();
+ this.initializeTooltip();
+ }
+ }
+
+ componentDidUpdate() {
+ // The MenuButton creates the child panel when initializing the MenuButton.
+ // If the children function is called during the rendering process,
+ // this child list size might change. So we need to adjust content size here.
+ if (typeof this.props.children === "function") {
+ this.resizeContent();
+ }
+ }
+
+ componentWillUnmount() {
+ this.resetTooltip();
+ }
+
+ initializeTooltip() {
+ const tooltipProps = {
+ type: "doorhanger",
+ useXulWrapper: true,
+ isMenuTooltip: true,
+ };
+
+ if (this.props.menuId) {
+ tooltipProps.id = this.props.menuId;
+ }
+
+ this.tooltip = new HTMLTooltip(this.props.toolboxDoc, tooltipProps);
+ this.tooltip.on("hidden", this.onHidden);
+ }
+
+ async resetTooltip() {
+ if (!this.tooltip) {
+ return;
+ }
+
+ // Mark the menu as closed since the onHidden callback may not be called in
+ // this case.
+ this.setState({ expanded: false });
+ this.tooltip.off("hidden", this.onHidden);
+ this.tooltip.destroy();
+ this.tooltip = null;
+ }
+
+ async showMenu(anchor) {
+ this.setState({
+ expanded: true,
+ });
+
+ if (!this.tooltip) {
+ return;
+ }
+
+ await this.tooltip.show(anchor, {
+ position: this.props.menuPosition,
+ y: this.props.menuOffset,
+ });
+ }
+
+ async hideMenu() {
+ this.setState({
+ expanded: false,
+ });
+
+ if (!this.tooltip) {
+ return;
+ }
+
+ await this.tooltip.hide();
+ }
+
+ async toggleMenu(anchor) {
+ return this.state.expanded ? this.hideMenu() : this.showMenu(anchor);
+ }
+
+ // Used by the call site to indicate that the menu content has changed so
+ // its container should be updated.
+ resizeContent() {
+ if (!this.state.expanded || !this.tooltip || !this.buttonRef.current) {
+ return;
+ }
+
+ this.tooltip.show(this.buttonRef.current, {
+ position: this.props.menuPosition,
+ y: this.props.menuOffset,
+ });
+ }
+
+ // When we are closing the menu we will get a 'hidden' event before we get
+ // a 'click' event. We want to re-enable the pointer-events: auto setting we
+ // use on the button while the menu is visible, but we don't want to do it
+ // until after the subsequent click event since otherwise we will end up
+ // re-opening the menu.
+ //
+ // For mouse events, we achieve this by using setTimeout(..., 0) to schedule
+ // a separate task to run after the click event, but in the case of touch
+ // events the event order differs and the setTimeout callback will run before
+ // the click event.
+ //
+ // In order to prevent that we detect touch events and set a flag to ignore
+ // the next click event. However, we need to differentiate between touch drag
+ // events and long press events (which don't generate a 'click') and "taps"
+ // (which do). We do that by looking for a 'touchmove' event and clearing the
+ // flag if we get one.
+ onTouchStart(evt) {
+ const touchend = () => {
+ const anchorRect = this.buttonRef.current.getClientRects()[0];
+ const { clientX, clientY } = evt.changedTouches[0];
+ // We need to check that the click is inside the bounds since when the
+ // menu is being closed the button will currently have
+ // pointer-events: none (and if we don't check the bounds we will end up
+ // ignoring unrelated clicks).
+ if (
+ anchorRect.x <= clientX &&
+ clientX <= anchorRect.x + anchorRect.width &&
+ anchorRect.y <= clientY &&
+ clientY <= anchorRect.y + anchorRect.height
+ ) {
+ this.ignoreNextClick = true;
+ }
+ };
+
+ const touchmove = () => {
+ this.state.win.removeEventListener("touchend", touchend);
+ };
+
+ this.state.win.addEventListener("touchend", touchend, { once: true });
+ this.state.win.addEventListener("touchmove", touchmove, { once: true });
+ }
+
+ onHidden() {
+ this.setState({ expanded: false });
+ // While the menu is open, if we click _anywhere_ outside the menu, it will
+ // automatically close. This is performed by the XUL wrapper before we get
+ // any chance to see any event. To avoid immediately re-opening the menu
+ // when we process the subsequent click event on this button, we set
+ // 'pointer-events: none' on the button while the menu is open.
+ //
+ // After the menu is closed we need to remove the pointer-events style (so
+ // the button works again) but we don't want to do it immediately since the
+ // "popuphidden" event which triggers this callback might be dispatched
+ // before the "click" event that we want to ignore. As a result, we queue
+ // up a task using setTimeout() to run after the "click" event.
+ this.state.win.setTimeout(() => {
+ if (this.buttonRef.current) {
+ this.buttonRef.current.style.pointerEvents = "auto";
+ }
+ this.state.win.removeEventListener("touchstart", this.onTouchStart, true);
+ }, 0);
+
+ this.state.win.addEventListener("touchstart", this.onTouchStart, true);
+
+ if (this.props.onCloseButton) {
+ this.props.onCloseButton();
+ }
+ }
+
+ async onClick(e) {
+ if (this.ignoreNextClick) {
+ this.ignoreNextClick = false;
+ return;
+ }
+
+ if (e.target === this.buttonRef.current) {
+ // On Mac, even after clicking the button it doesn't get focus.
+ // Force focus to the button so that our keydown handlers get called.
+ this.buttonRef.current.focus();
+
+ if (this.props.onClick) {
+ this.props.onClick(e);
+ }
+
+ if (!e.defaultPrevented) {
+ const wasKeyboardEvent = e.screenX === 0 && e.screenY === 0;
+ // If the popup menu will be shown, disable this button in order to
+ // prevent reopening the popup menu. See extended comment in onHidden().
+ // above.
+ //
+ // Also, we should _not_ set 'pointer-events: none' if
+ // ui.popup.disable_autohide pref is in effect since, in that case,
+ // there's no redundant hiding behavior and we actually want clicking
+ // the button to close the menu.
+ if (
+ !this.state.expanded &&
+ !Services.prefs.getBoolPref("ui.popup.disable_autohide", false)
+ ) {
+ this.buttonRef.current.style.pointerEvents = "none";
+ }
+ await this.toggleMenu(e.target);
+ // If the menu was activated by keyboard, focus the first item.
+ if (wasKeyboardEvent && this.tooltip) {
+ this.tooltip.focus();
+ }
+
+ // MenuButton creates the children dynamically when clicking the button,
+ // so execute the goggle menu after updating the children panel.
+ if (typeof this.props.children === "function") {
+ this.forceUpdate();
+ }
+ }
+ // If we clicked one of the menu items, then, by default, we should
+ // auto-collapse the menu.
+ //
+ // We check for the defaultPrevented state, however, so that menu items can
+ // turn this behavior off (e.g. a menu item with an embedded button).
+ } else if (
+ this.state.expanded &&
+ !e.defaultPrevented &&
+ e.target.matches(focusableSelector)
+ ) {
+ this.hideMenu();
+ }
+ }
+
+ onKeyDown(e) {
+ if (!this.state.expanded) {
+ return;
+ }
+
+ const isButtonFocussed =
+ this.props.toolboxDoc &&
+ this.props.toolboxDoc.activeElement === this.buttonRef.current;
+
+ switch (e.key) {
+ case "Escape":
+ this.hideMenu();
+ e.preventDefault();
+ break;
+
+ case "Tab":
+ case "ArrowDown":
+ if (isButtonFocussed && this.tooltip) {
+ if (this.tooltip.focus()) {
+ e.preventDefault();
+ }
+ }
+ break;
+
+ case "ArrowUp":
+ if (isButtonFocussed && this.tooltip) {
+ if (this.tooltip.focusEnd()) {
+ e.preventDefault();
+ }
+ }
+ break;
+ case "t":
+ if ((isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey)) {
+ // Close the menu if the user opens a new tab while it is still open.
+ //
+ // Bug 1499271: Once toolbox has been converted to XUL we should watch
+ // for the 'visibilitychange' event instead of explicitly looking for
+ // Ctrl+T.
+ this.hideMenu();
+ }
+ break;
+ }
+ }
+
+ render() {
+ const buttonProps = {
+ // Pass through any props set on the button, except the ones we handle
+ // here.
+ ...omit(this.props, Object.keys(MenuButton.propTypes)),
+ onClick: this.onClick,
+ "aria-expanded": this.state.expanded,
+ "aria-haspopup": "menu",
+ ref: this.buttonRef,
+ };
+
+ if (this.state.expanded) {
+ buttonProps.onKeyDown = this.onKeyDown;
+ }
+
+ if (this.props.menuId) {
+ buttonProps["aria-controls"] = this.props.menuId;
+ }
+
+ if (this.props.icon) {
+ const iconClass = "menu-button--iconic";
+ buttonProps.className = buttonProps.className
+ ? `${buttonProps.className} ${iconClass}`
+ : iconClass;
+ buttonProps.style = {
+ "--menuitem-icon-image": "url(" + this.props.icon + ")",
+ };
+ }
+
+ if (this.state.isMenuInitialized) {
+ const menu = createPortal(
+ typeof this.props.children === "function"
+ ? this.props.children()
+ : this.props.children,
+ this.tooltip.panel
+ );
+
+ return button(buttonProps, this.props.label, menu);
+ }
+
+ return button(buttonProps, this.props.label);
+ }
+}
+
+module.exports = MenuButton;
diff --git a/devtools/client/shared/components/menu/MenuItem.js b/devtools/client/shared/components/menu/MenuItem.js
new file mode 100644
index 0000000000..c3efa6db6c
--- /dev/null
+++ b/devtools/client/shared/components/menu/MenuItem.js
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+"use strict";
+
+// A command in a menu.
+
+const {
+ createFactory,
+ createRef,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { button, li, span } = dom;
+loader.lazyGetter(this, "Localized", () =>
+ createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js")
+ .Localized
+ )
+);
+
+class MenuItem extends PureComponent {
+ static get propTypes() {
+ return {
+ // An optional keyboard shortcut to display next to the item.
+ // (This does not actually register the event listener for the key.)
+ accelerator: PropTypes.string,
+
+ // A tri-state value that may be true/false if item should be checkable,
+ // and undefined otherwise.
+ checked: PropTypes.bool,
+
+ // Any additional classes to assign to the button specified as
+ // a space-separated string.
+ className: PropTypes.string,
+
+ // A disabled state of the menu item.
+ disabled: PropTypes.bool,
+
+ // URL of the icon to associate with the MenuItem. (Optional)
+ //
+ // e.g. chrome://devtools/skim/image/foo.svg
+ //
+ // This may also be set in CSS using the --menuitem-icon-image variable.
+ // Note that in this case, the variable should specify the CSS <image> to
+ // use, not simply the URL (e.g.
+ // "url(chrome://devtools/skim/image/foo.svg)").
+ icon: PropTypes.string,
+
+ // An optional ID to be assigned to the item.
+ id: PropTypes.string,
+
+ // The item label for use with legacy localization systems.
+ label: PropTypes.string,
+
+ // The Fluent ID for localizing the label.
+ l10nID: PropTypes.string,
+
+ // An optional callback to be invoked when the item is selected.
+ onClick: PropTypes.func,
+
+ // Optional menu item role override. Use this property with a value
+ // "menuitemradio" if the menu item is a radio.
+ role: PropTypes.string,
+
+ // An optional text for the item tooltip.
+ tooltip: PropTypes.string,
+ };
+ }
+
+ /**
+ * Use this as a fallback `icon` prop if your MenuList contains MenuItems
+ * with or without icon in order to keep all MenuItems aligned.
+ */
+ static get DUMMY_ICON() {
+ return `data:image/svg+xml,${encodeURIComponent(
+ '<svg height="16" width="16"></svg>'
+ )}`;
+ }
+
+ constructor(props) {
+ super(props);
+ this.labelRef = createRef();
+ }
+
+ componentDidMount() {
+ if (!this.labelRef.current) {
+ return;
+ }
+
+ // Pre-fetch any backgrounds specified for the item.
+ const win = this.labelRef.current.ownerDocument.defaultView;
+ this.preloadCallback = win.requestIdleCallback(() => {
+ this.preloadCallback = null;
+ if (!this.labelRef.current) {
+ return;
+ }
+
+ const backgrounds = win
+ .getComputedStyle(this.labelRef.current, ":before")
+ .getCSSImageURLs("background-image");
+ for (const background of backgrounds) {
+ const image = new Image();
+ image.src = background;
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ if (!this.labelRef.current || !this.preloadCallback) {
+ return;
+ }
+
+ const win = this.labelRef.current.ownerDocument.defaultView;
+ if (win) {
+ win.cancelIdleCallback(this.preloadCallback);
+ }
+ this.preloadCallback = null;
+ }
+
+ render() {
+ const attr = {
+ className: "command",
+ };
+
+ if (this.props.id) {
+ attr.id = this.props.id;
+ }
+
+ if (this.props.className) {
+ attr.className += " " + this.props.className;
+ }
+
+ if (this.props.icon) {
+ attr.className += " iconic";
+ attr.style = { "--menuitem-icon-image": "url(" + this.props.icon + ")" };
+ }
+
+ if (this.props.onClick) {
+ attr.onClick = this.props.onClick;
+ }
+
+ if (this.props.tooltip) {
+ attr.title = this.props.tooltip;
+ }
+
+ if (this.props.disabled) {
+ attr.disabled = this.props.disabled;
+ }
+
+ if (this.props.role) {
+ attr.role = this.props.role;
+ } else if (typeof this.props.checked !== "undefined") {
+ attr.role = "menuitemcheckbox";
+ } else {
+ attr.role = "menuitem";
+ }
+
+ if (this.props.checked) {
+ attr["aria-checked"] = true;
+ }
+
+ const children = [];
+ const className = "label";
+
+ // Add the text label.
+ if (this.props.l10nID) {
+ // Fluent localized label.
+ children.push(
+ Localized(
+ { id: this.props.l10nID, key: "label" },
+ span({ className, ref: this.labelRef })
+ )
+ );
+ } else {
+ children.push(
+ span({ key: "label", className, ref: this.labelRef }, this.props.label)
+ );
+ }
+
+ if (this.props.l10nID && this.props.label) {
+ console.warn(
+ "<MenuItem> should only take either an l10nID or a label, not both"
+ );
+ }
+ if (!this.props.l10nID && !this.props.label) {
+ console.warn("<MenuItem> requires either an l10nID, or a label prop.");
+ }
+
+ if (typeof this.props.accelerator !== "undefined") {
+ const acceleratorLabel = span(
+ { key: "accelerator", className: "accelerator" },
+ this.props.accelerator
+ );
+ children.push(acceleratorLabel);
+ }
+
+ return li(
+ {
+ className: "menuitem",
+ role: "presentation",
+ },
+ button(attr, children)
+ );
+ }
+}
+
+module.exports = MenuItem;
diff --git a/devtools/client/shared/components/menu/MenuList.js b/devtools/client/shared/components/menu/MenuList.js
new file mode 100644
index 0000000000..4c355cca10
--- /dev/null
+++ b/devtools/client/shared/components/menu/MenuList.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+"use strict";
+
+// A list of menu items.
+//
+// This component provides keyboard navigation amongst any focusable
+// children.
+
+const {
+ Children,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { div } = dom;
+const {
+ focusableSelector,
+} = require("resource://devtools/client/shared/focus.js");
+
+class MenuList extends PureComponent {
+ static get propTypes() {
+ return {
+ // ID to assign to the list container.
+ id: PropTypes.string,
+
+ // Children of the list.
+ children: PropTypes.any,
+
+ // Called whenever there is a change to the hovered or selected child.
+ // The callback is passed the ID of the highlighted child or null if no
+ // child is highlighted.
+ onHighlightedChildChange: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onMouseOverOrFocus = this.onMouseOverOrFocus.bind(this);
+ this.onMouseOutOrBlur = this.onMouseOutOrBlur.bind(this);
+ this.notifyHighlightedChildChange =
+ this.notifyHighlightedChildChange.bind(this);
+
+ this.setWrapperRef = element => {
+ this.wrapperRef = element;
+ };
+ }
+
+ onMouseOverOrFocus(e) {
+ this.notifyHighlightedChildChange(e.target.id);
+ }
+
+ onMouseOutOrBlur(e) {
+ const hoveredElem = this.wrapperRef.querySelector(":hover");
+ if (!hoveredElem) {
+ this.notifyHighlightedChildChange(null);
+ }
+ }
+
+ notifyHighlightedChildChange(id) {
+ if (this.props.onHighlightedChildChange) {
+ this.props.onHighlightedChildChange(id);
+ }
+ }
+
+ onKeyDown(e) {
+ // Check if the focus is in the list.
+ if (
+ !this.wrapperRef ||
+ !this.wrapperRef.contains(e.target.ownerDocument.activeElement)
+ ) {
+ return;
+ }
+
+ const getTabList = () =>
+ Array.from(this.wrapperRef.querySelectorAll(focusableSelector));
+
+ switch (e.key) {
+ case "Tab":
+ case "ArrowUp":
+ case "ArrowDown":
+ {
+ const tabList = getTabList();
+ const currentElement = e.target.ownerDocument.activeElement;
+ const currentIndex = tabList.indexOf(currentElement);
+ if (currentIndex !== -1) {
+ let nextIndex;
+ if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
+ nextIndex =
+ currentIndex === tabList.length - 1 ? 0 : currentIndex + 1;
+ } else {
+ nextIndex =
+ currentIndex === 0 ? tabList.length - 1 : currentIndex - 1;
+ }
+ tabList[nextIndex].focus();
+ e.preventDefault();
+ }
+ }
+ break;
+
+ case "Home":
+ {
+ const firstItem = this.wrapperRef.querySelector(focusableSelector);
+ if (firstItem) {
+ firstItem.focus();
+ e.preventDefault();
+ }
+ }
+ break;
+
+ case "End":
+ {
+ const tabList = getTabList();
+ if (tabList.length) {
+ tabList[tabList.length - 1].focus();
+ e.preventDefault();
+ }
+ }
+ break;
+ }
+ }
+
+ render() {
+ const attr = {
+ role: "menu",
+ ref: this.setWrapperRef,
+ onKeyDown: this.onKeyDown,
+ onMouseOver: this.onMouseOverOrFocus,
+ onMouseOut: this.onMouseOutOrBlur,
+ onFocus: this.onMouseOverOrFocus,
+ onBlur: this.onMouseOutOrBlur,
+ className: "menu-standard-padding",
+ };
+
+ if (this.props.id) {
+ attr.id = this.props.id;
+ }
+
+ // Add padding for checkbox image if necessary.
+ let hasCheckbox = false;
+ Children.forEach(this.props.children, (child, i) => {
+ if (child == null || typeof child == "undefined") {
+ console.warn("MenuList children at index", i, "is", child);
+ return;
+ }
+
+ if (typeof child?.props?.checked !== "undefined") {
+ hasCheckbox = true;
+ }
+ });
+ if (hasCheckbox) {
+ attr.className = "checkbox-container menu-standard-padding";
+ }
+
+ return div(attr, this.props.children);
+ }
+}
+
+module.exports = MenuList;
diff --git a/devtools/client/shared/components/menu/moz.build b/devtools/client/shared/components/menu/moz.build
new file mode 100644
index 0000000000..08046199e5
--- /dev/null
+++ b/devtools/client/shared/components/menu/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "MenuButton.js",
+ "MenuItem.js",
+ "MenuList.js",
+ "utils.js",
+)
diff --git a/devtools/client/shared/components/menu/utils.js b/devtools/client/shared/components/menu/utils.js
new file mode 100644
index 0000000000..e6fca96822
--- /dev/null
+++ b/devtools/client/shared/components/menu/utils.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Menu = require("resource://devtools/client/framework/menu.js");
+const MenuItem = require("resource://devtools/client/framework/menu-item.js");
+
+/**
+ * Helper function for opening context menu.
+ *
+ * @param {Array} items
+ * List of menu items.
+ * @param {Object} options:
+ * @property {Element} button
+ * Button element used to open the menu.
+ * @property {Number} screenX
+ * Screen x coordinate of the menu on the screen.
+ * @property {Number} screenY
+ * Screen y coordinate of the menu on the screen.
+ */
+function showMenu(items, options) {
+ if (items.length === 0) {
+ return;
+ }
+
+ // Build the menu object from provided menu items.
+ const menu = new Menu();
+ items.forEach(item => {
+ if (item == "-") {
+ item = { type: "separator" };
+ }
+
+ const menuItem = new MenuItem(item);
+ const subItems = item.submenu;
+
+ if (subItems) {
+ const subMenu = new Menu();
+ subItems.forEach(subItem => {
+ subMenu.append(new MenuItem(subItem));
+ });
+ menuItem.submenu = subMenu;
+ }
+
+ menu.append(menuItem);
+ });
+
+ // Calculate position on the screen according to
+ // the parent button if available.
+ if (options.button) {
+ menu.popupAtTarget(options.button);
+ } else {
+ const screenX = options.screenX;
+ const screenY = options.screenY;
+ menu.popup(screenX, screenY, window.document);
+ }
+}
+
+module.exports = {
+ showMenu,
+};
diff --git a/devtools/client/shared/components/moz.build b/devtools/client/shared/components/moz.build
new file mode 100644
index 0000000000..f905adc4e6
--- /dev/null
+++ b/devtools/client/shared/components/moz.build
@@ -0,0 +1,41 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "object-inspector",
+ "menu",
+ "reps",
+ "splitter",
+ "tabs",
+ "throttling",
+ "tree",
+]
+
+DevToolsModules(
+ "Accordion.js",
+ "AppErrorBoundary.js",
+ "Frame.js",
+ "HSplitBox.js",
+ "List.js",
+ "MdnLink.js",
+ "NotificationBox.js",
+ "SearchBox.js",
+ "SearchBoxAutocompletePopup.js",
+ "SearchModifiers.js",
+ "Sidebar.js",
+ "SidebarToggle.js",
+ "SmartTrace.js",
+ "StackTrace.js",
+ "Tree.js",
+ "VirtualizedTree.js",
+ "VisibilityHandler.js",
+)
+
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.ini"]
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.ini",
+ "test/node/stubs/reps/stubs.ini",
+]
diff --git a/devtools/client/shared/components/object-inspector/actions.js b/devtools/client/shared/components/object-inspector/actions.js
new file mode 100644
index 0000000000..370f1b161a
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/actions.js
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const { loadItemProperties } = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js");
+const {
+ getPathExpression,
+ getParentFront,
+ getParentGripValue,
+ getValue,
+ nodeIsBucket,
+ getFront,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const { getLoadedProperties, getWatchpoints } = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+
+/**
+ * This action is responsible for expanding a given node, which also means that
+ * it will call the action responsible to fetch properties.
+ */
+function nodeExpand(node, actor) {
+ return async ({ dispatch }) => {
+ dispatch({ type: "NODE_EXPAND", data: { node } });
+ dispatch(nodeLoadProperties(node, actor));
+ };
+}
+
+function nodeCollapse(node) {
+ return {
+ type: "NODE_COLLAPSE",
+ data: { node },
+ };
+}
+
+/*
+ * This action checks if we need to fetch properties, entries, prototype and
+ * symbols for a given node. If we do, it will call the appropriate ObjectFront
+ * functions.
+ */
+function nodeLoadProperties(node, actor) {
+ return async ({ dispatch, client, getState }) => {
+ const state = getState();
+ const loadedProperties = getLoadedProperties(state);
+ if (loadedProperties.has(node.path)) {
+ return;
+ }
+
+ try {
+ const properties = await loadItemProperties(
+ node,
+ client,
+ loadedProperties
+ );
+
+ // If the client does not have a releaseActor function, it means the actors are
+ // handled directly by the consumer, so we don't need to track them.
+ if (!client || !client.releaseActor) {
+ actor = null;
+ }
+
+ dispatch(nodePropertiesLoaded(node, actor, properties));
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function nodePropertiesLoaded(node, actor, properties) {
+ return {
+ type: "NODE_PROPERTIES_LOADED",
+ data: { node, actor, properties },
+ };
+}
+
+/*
+ * This action adds a property watchpoint to an object
+ */
+function addWatchpoint(item, watchpoint) {
+ return async function({ dispatch, client }) {
+ const { parent, name } = item;
+ let object = getValue(parent);
+
+ if (nodeIsBucket(parent)) {
+ object = getValue(parent.parent);
+ }
+
+ if (!object) {
+ return;
+ }
+
+ const path = parent.path;
+ const property = name;
+ const label = getPathExpression(item);
+ const actor = object.actor;
+
+ await client.addWatchpoint(object, property, label, watchpoint);
+
+ dispatch({
+ type: "SET_WATCHPOINT",
+ data: { path, watchpoint, property, actor },
+ });
+ };
+}
+
+/*
+ * This action removes a property watchpoint from an object
+ */
+function removeWatchpoint(item) {
+ return async function({ dispatch, client }) {
+ const { parent, name } = item;
+ let object = getValue(parent);
+
+ if (nodeIsBucket(parent)) {
+ object = getValue(parent.parent);
+ }
+
+ const property = name;
+ const path = parent.path;
+ const actor = object.actor;
+
+ await client.removeWatchpoint(object, property);
+
+ dispatch({
+ type: "REMOVE_WATCHPOINT",
+ data: { path, property, actor },
+ });
+ };
+}
+
+function getActorIDs(roots) {
+ if (!roots) {
+ return []
+ }
+
+ const actorIds = [];
+ for (const root of roots) {
+ const front = getFront(root);
+ if (front?.actorID) {
+ actorIds.push(front.actorID);
+ }
+ }
+
+ return actorIds;
+}
+
+function closeObjectInspector(roots) {
+ return ({ client }) => {
+ releaseActors(client, roots);
+ };
+}
+
+/*
+ * This action is dispatched when the `roots` prop, provided by a consumer of
+ * the ObjectInspector (inspector, console, …), is modified. It will clean the
+ * internal state properties (expandedPaths, loadedProperties, …) and release
+ * the actors consumed with the previous roots.
+ * It takes a props argument which reflects what is passed by the upper-level
+ * consumer.
+ */
+function rootsChanged(roots, oldRoots) {
+ return ({ dispatch, client }) => {
+ releaseActors(client, oldRoots, roots);
+ dispatch({
+ type: "ROOTS_CHANGED",
+ data: roots,
+ });
+ };
+}
+
+/**
+ * Release any actors we don't need anymore
+ *
+ * @param {Object} client: Object with a `releaseActor` method
+ * @param {Array} oldRoots: The roots in which we want to cleanup now-unused actors
+ * @param {Array} newRoots: The current roots (might have item that are also in oldRoots)
+ */
+async function releaseActors(client, oldRoots, newRoots = []) {
+ if (!client?.releaseActor ) {
+ return;
+ }
+
+ let actorIdsToRelease = getActorIDs(oldRoots);
+ if (newRoots.length) {
+ const newActorIds = getActorIDs(newRoots);
+ actorIdsToRelease = actorIdsToRelease.filter(id => !newActorIds.includes(id));
+ }
+
+ if (!actorIdsToRelease.length) {
+ return;
+ }
+ await Promise.all(actorIdsToRelease.map(client.releaseActor));
+}
+
+function invokeGetter(node, receiverId) {
+ return async ({ dispatch, client, getState }) => {
+ try {
+ const objectFront =
+ getParentFront(node) ||
+ client.createObjectFront(getParentGripValue(node));
+ const getterName = node.propertyName || node.name;
+
+ const result = await objectFront.getPropertyValue(getterName, receiverId);
+ dispatch({
+ type: "GETTER_INVOKED",
+ data: {
+ node,
+ result,
+ },
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+module.exports = {
+ closeObjectInspector,
+ invokeGetter,
+ nodeExpand,
+ nodeCollapse,
+ nodeLoadProperties,
+ nodePropertiesLoaded,
+ rootsChanged,
+ addWatchpoint,
+ removeWatchpoint,
+};
diff --git a/devtools/client/shared/components/object-inspector/components/ObjectInspector.css b/devtools/client/shared/components/object-inspector/components/ObjectInspector.css
new file mode 100644
index 0000000000..d7b9b3ffa7
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/ObjectInspector.css
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.tree.object-inspector .node.object-node {
+ display: inline-block;
+}
+
+.tree.object-inspector .object-label,
+.tree.object-inspector .object-label * {
+ color: var(--theme-highlight-blue);
+}
+
+.tree.object-inspector .node .unavailable {
+ color: var(--theme-comment);
+}
+
+.tree.object-inspector .lessen,
+.tree.object-inspector .lessen *,
+.tree.object-inspector .lessen .object-label,
+.tree.object-inspector .lessen .object-label * {
+ color: var(--theme-comment);
+}
+
+.tree.object-inspector .block .object-label,
+.tree.object-inspector .block .object-label * {
+ color: var(--theme-body-color);
+}
+
+.tree.object-inspector .block .object-label::before {
+ content: "☲";
+ font-size: 1.1em;
+ display: inline;
+ padding-inline-end: 2px;
+ line-height: 14px;
+}
+
+.object-inspector .object-delimiter {
+ color: var(--theme-comment);
+ white-space: pre-wrap;
+}
+
+.object-inspector .tree-node .arrow {
+ display: inline-block;
+ vertical-align: middle;
+ margin-inline-start: -1px;
+}
+
+/* Focused styles */
+.tree.object-inspector .tree-node.focused * {
+ color: inherit;
+}
+
+.tree-node.focused button.open-inspector {
+ fill: currentColor;
+}
+
+.tree-node.focused button.invoke-getter {
+ background-color: currentColor;
+}
+
+button[class*="remove-watchpoint-"] {
+ background: url("chrome://devtools/content/debugger/images/webconsole-logpoint.svg")
+ no-repeat;
+ display: inline-block;
+ vertical-align: top;
+ height: 13px;
+ width: 15px;
+ margin: 1px 4px 0px 20px;
+ padding: 0;
+ border: none;
+ -moz-context-properties: fill, stroke;
+ cursor: pointer;
+}
+
+button.remove-watchpoint-set {
+ fill: var(--breakpoint-fill);
+ stroke: var(--breakpoint-fill);
+}
+
+button.remove-watchpoint-get {
+ fill: var(--purple-60);
+ stroke: var(--purple-60);
+}
+
+button.remove-watchpoint-getorset {
+ fill: var(--yellow-60);
+ stroke: var(--yellow-60);
+}
+
+.tree-node.focused button[class*="remove-watchpoint-"] {
+ stroke: white;
+}
+
+/* Don't display the light grey background we have on button hover */
+.theme-dark button[class*="remove-watchpoint-"]:hover,
+.theme-light button[class*="remove-watchpoint-"]:hover {
+ background-color: transparent;
+}
diff --git a/devtools/client/shared/components/object-inspector/components/ObjectInspector.js b/devtools/client/shared/components/object-inspector/components/ObjectInspector.js
new file mode 100644
index 0000000000..a8af00d32a
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/ObjectInspector.js
@@ -0,0 +1,371 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+ Provider,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+loader.lazyRequireGetter(
+ this,
+ "createStore",
+ "resource://devtools/client/shared/redux/create-store.js"
+);
+
+const actions = require("resource://devtools/client/shared/components/object-inspector/actions.js");
+const {
+ getExpandedPaths,
+ getLoadedProperties,
+ getEvaluations,
+ default: reducer,
+} = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+
+const Tree = createFactory(require("resource://devtools/client/shared/components/Tree.js"));
+
+const ObjectInspectorItem = createFactory(
+ require("resource://devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js")
+);
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const { renderRep, shouldRenderRootsInReps } = Utils;
+const {
+ getChildrenWithEvaluations,
+ getActor,
+ getEvaluatedItem,
+ getParent,
+ getValue,
+ nodeIsPrimitive,
+ nodeHasGetter,
+ nodeHasSetter,
+} = Utils.node;
+
+// This implements a component that renders an interactive inspector
+// for looking at JavaScript objects. It expects descriptions of
+// objects from the protocol, and will dynamically fetch children
+// properties as objects are expanded.
+//
+// If you want to inspect a single object, pass the name and the
+// protocol descriptor of it:
+//
+// ObjectInspector({
+// name: "foo",
+// desc: { writable: true, ..., { value: { actor: "1", ... }}},
+// ...
+// })
+//
+// If you want multiple top-level objects (like scopes), you can pass
+// an array of manually constructed nodes as `roots`:
+//
+// ObjectInspector({
+// roots: [{ name: ... }, ...],
+// ...
+// });
+
+// There are 3 types of nodes: a simple node with a children array, an
+// object that has properties that should be children when they are
+// fetched, and a primitive value that should be displayed with no
+// children.
+
+class ObjectInspector extends Component {
+ static defaultProps = {
+ autoReleaseObjectActors: true
+ };
+ constructor(props) {
+ super();
+ this.cachedNodes = new Map();
+
+ const self = this;
+
+ self.getItemChildren = this.getItemChildren.bind(this);
+ self.isNodeExpandable = this.isNodeExpandable.bind(this);
+ self.setExpanded = this.setExpanded.bind(this);
+ self.focusItem = this.focusItem.bind(this);
+ self.activateItem = this.activateItem.bind(this);
+ self.getRoots = this.getRoots.bind(this);
+ self.getNodeKey = this.getNodeKey.bind(this);
+ self.shouldItemUpdate = this.shouldItemUpdate.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.roots = this.props.roots;
+ this.focusedItem = this.props.focusedItem;
+ this.activeItem = this.props.activeItem;
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate(nextProps) {
+ this.removeOutdatedNodesFromCache(nextProps);
+
+ if (this.roots !== nextProps.roots) {
+ // Since the roots changed, we assume the properties did as well,
+ // so we need to cleanup the component internal state.
+ const oldRoots = this.roots;
+ this.roots = nextProps.roots;
+ this.focusedItem = nextProps.focusedItem;
+ this.activeItem = nextProps.activeItem;
+ if (this.props.rootsChanged) {
+ this.props.rootsChanged(this.roots, oldRoots);
+ }
+ }
+ }
+
+ removeOutdatedNodesFromCache(nextProps) {
+ // When the roots changes, we can wipe out everything.
+ if (this.roots !== nextProps.roots) {
+ this.cachedNodes.clear();
+ return;
+ }
+
+ for (const [path, properties] of nextProps.loadedProperties) {
+ if (properties !== this.props.loadedProperties.get(path)) {
+ this.cachedNodes.delete(path);
+ }
+ }
+
+ // If there are new evaluations, we want to remove the existing cached
+ // nodes from the cache.
+ if (nextProps.evaluations > this.props.evaluations) {
+ for (const key of nextProps.evaluations.keys()) {
+ if (!this.props.evaluations.has(key)) {
+ this.cachedNodes.delete(key);
+ }
+ }
+ }
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { expandedPaths, loadedProperties, evaluations } = this.props;
+
+ // We should update if:
+ // - there are new loaded properties
+ // - OR there are new evaluations
+ // - OR the expanded paths number changed, and all of them have properties
+ // loaded
+ // - OR the expanded paths number did not changed, but old and new sets
+ // differ
+ // - OR the focused node changed.
+ // - OR the active node changed.
+ return (
+ loadedProperties !== nextProps.loadedProperties ||
+ loadedProperties.size !== nextProps.loadedProperties.size ||
+ evaluations.size !== nextProps.evaluations.size ||
+ (expandedPaths.size !== nextProps.expandedPaths.size &&
+ [...nextProps.expandedPaths].every(path =>
+ nextProps.loadedProperties.has(path)
+ )) ||
+ (expandedPaths.size === nextProps.expandedPaths.size &&
+ [...nextProps.expandedPaths].some(key => !expandedPaths.has(key))) ||
+ this.focusedItem !== nextProps.focusedItem ||
+ this.activeItem !== nextProps.activeItem ||
+ this.roots !== nextProps.roots
+ );
+ }
+
+ componentWillUnmount() {
+ if (this.props.autoReleaseObjectActors){
+ this.props.closeObjectInspector(this.props.roots);
+ }
+ }
+
+ getItemChildren(item) {
+ const { loadedProperties, evaluations } = this.props;
+ const { cachedNodes } = this;
+
+ return getChildrenWithEvaluations({
+ evaluations,
+ loadedProperties,
+ cachedNodes,
+ item,
+ });
+ }
+
+ getRoots() {
+ const { evaluations, roots } = this.props;
+ const length = roots.length;
+
+ for (let i = 0; i < length; i++) {
+ let rootItem = roots[i];
+
+ if (evaluations.has(rootItem.path)) {
+ roots[i] = getEvaluatedItem(rootItem, evaluations);
+ }
+ }
+
+ return roots;
+ }
+
+ getNodeKey(item) {
+ return item.path && typeof item.path.toString === "function"
+ ? item.path.toString()
+ : JSON.stringify(item);
+ }
+
+ isNodeExpandable(item) {
+ if (
+ nodeIsPrimitive(item) ||
+ item.contents?.value?.useCustomFormatter
+ ) {
+ return false;
+ }
+
+ if (nodeHasSetter(item) || nodeHasGetter(item)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ setExpanded(item, expand) {
+ if (!this.isNodeExpandable(item)) {
+ return;
+ }
+
+ const {
+ nodeExpand,
+ nodeCollapse,
+ recordTelemetryEvent,
+ setExpanded,
+ roots,
+ } = this.props;
+
+ if (expand === true) {
+ const actor = getActor(item, roots);
+ nodeExpand(item, actor);
+ if (recordTelemetryEvent) {
+ recordTelemetryEvent("object_expanded");
+ }
+ } else {
+ nodeCollapse(item);
+ }
+
+ if (setExpanded) {
+ setExpanded(item, expand);
+ }
+ }
+
+ focusItem(item) {
+ const { focusable = true, onFocus } = this.props;
+
+ if (focusable && this.focusedItem !== item) {
+ this.focusedItem = item;
+ this.forceUpdate();
+
+ if (onFocus) {
+ onFocus(item);
+ }
+ }
+ }
+
+ activateItem(item) {
+ const { focusable = true, onActivate } = this.props;
+
+ if (focusable && this.activeItem !== item) {
+ this.activeItem = item;
+ this.forceUpdate();
+
+ if (onActivate) {
+ onActivate(item);
+ }
+ }
+ }
+
+ shouldItemUpdate(prevItem, nextItem) {
+ const value = getValue(nextItem);
+ // Long string should always update because fullText loading will not
+ // trigger item re-render.
+ return value && value.type === "longString";
+ }
+
+ render() {
+ const {
+ autoExpandAll = true,
+ autoExpandDepth = 1,
+ initiallyExpanded,
+ focusable = true,
+ disableWrap = false,
+ expandedPaths,
+ inline,
+ } = this.props;
+
+ const classNames = ["object-inspector"];
+ if (inline) {
+ classNames.push("inline");
+ }
+ if (disableWrap) {
+ classNames.push("nowrap");
+ }
+
+ return Tree({
+ className: classNames.join(" "),
+
+ autoExpandAll,
+ autoExpandDepth,
+ initiallyExpanded,
+ isExpanded: item => expandedPaths && expandedPaths.has(item.path),
+ isExpandable: this.isNodeExpandable,
+ focused: this.focusedItem,
+ active: this.activeItem,
+
+ getRoots: this.getRoots,
+ getParent,
+ getChildren: this.getItemChildren,
+ getKey: this.getNodeKey,
+
+ onExpand: item => this.setExpanded(item, true),
+ onCollapse: item => this.setExpanded(item, false),
+ onFocus: focusable ? this.focusItem : null,
+ onActivate: focusable ? this.activateItem : null,
+
+ shouldItemUpdate: this.shouldItemUpdate,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ ObjectInspectorItem({
+ ...this.props,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ setExpanded: this.setExpanded,
+ }),
+ });
+ }
+}
+
+function mapStateToProps(state, props) {
+ return {
+ expandedPaths: getExpandedPaths(state),
+ loadedProperties: getLoadedProperties(state),
+ evaluations: getEvaluations(state),
+ };
+}
+
+const OI = connect(mapStateToProps, actions)(ObjectInspector);
+
+module.exports = props => {
+ const { roots, standalone = false } = props;
+
+ if (roots.length == 0) {
+ return null;
+ }
+
+ if (shouldRenderRootsInReps(roots, props)) {
+ return renderRep(roots[0], props);
+ }
+
+ const oiElement = createElement(OI, props);
+
+ if (!standalone) {
+ return oiElement;
+ }
+
+ const store = createStore(reducer);
+ return createElement(Provider, { store }, oiElement);
+};
diff --git a/devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js b/devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js
new file mode 100644
index 0000000000..534ac0e13b
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const { Component } = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+
+const {
+ getValue,
+ nodeHasAccessors,
+ nodeHasProperties,
+ nodeIsBlock,
+ nodeIsDefaultProperties,
+ nodeIsFunction,
+ nodeIsGetter,
+ nodeIsMapEntry,
+ nodeIsMissingArguments,
+ nodeIsOptimizedOut,
+ nodeIsPrimitive,
+ nodeIsPrototype,
+ nodeIsSetter,
+ nodeIsUninitializedBinding,
+ nodeIsUnmappedBinding,
+ nodeIsUnscopedBinding,
+ nodeIsWindow,
+ nodeIsLongString,
+ nodeHasFullText,
+ nodeHasGetter,
+ getNonPrototypeParentGripValue,
+} = Utils.node;
+
+class ObjectInspectorItem extends Component {
+ static get defaultProps() {
+ return {
+ onContextMenu: () => {},
+ renderItemActions: () => null,
+ };
+ }
+
+ // eslint-disable-next-line complexity
+ getLabelAndValue() {
+ const { item, depth, expanded, mode } = this.props;
+
+ const label = item.name;
+ const isPrimitive = nodeIsPrimitive(item);
+
+ if (nodeIsOptimizedOut(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(optimized away)"),
+ };
+ }
+
+ if (nodeIsUninitializedBinding(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(uninitialized)"),
+ };
+ }
+
+ if (nodeIsUnmappedBinding(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(unmapped)"),
+ };
+ }
+
+ if (nodeIsUnscopedBinding(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(unscoped)"),
+ };
+ }
+
+ const itemValue = getValue(item);
+ const unavailable =
+ isPrimitive &&
+ itemValue &&
+ itemValue.hasOwnProperty &&
+ itemValue.hasOwnProperty("unavailable");
+
+ if (nodeIsMissingArguments(item) || unavailable) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(unavailable)"),
+ };
+ }
+
+ if (
+ nodeIsFunction(item) &&
+ !nodeIsGetter(item) &&
+ !nodeIsSetter(item) &&
+ (mode === MODE.TINY || !mode)
+ ) {
+ return {
+ label: Utils.renderRep(item, {
+ ...this.props,
+ functionName: label,
+ }),
+ };
+ }
+
+ if (
+ nodeHasProperties(item) ||
+ nodeHasAccessors(item) ||
+ nodeIsMapEntry(item) ||
+ nodeIsLongString(item) ||
+ isPrimitive
+ ) {
+ const repProps = { ...this.props };
+ if (depth > 0) {
+ repProps.mode = mode === MODE.LONG ? MODE.SHORT : MODE.TINY;
+ }
+
+
+ if (nodeIsLongString(item)) {
+ repProps.member = {
+ open: nodeHasFullText(item) && expanded,
+ };
+ }
+
+ if (nodeHasGetter(item)) {
+ const receiverGrip = getNonPrototypeParentGripValue(item);
+ if (receiverGrip) {
+ Object.assign(repProps, {
+ onInvokeGetterButtonClick: () =>
+ this.props.invokeGetter(item, receiverGrip.actor),
+ });
+ }
+ }
+
+ return {
+ label,
+ value: Utils.renderRep(item, repProps),
+ };
+ }
+
+ return {
+ label,
+ };
+ }
+
+ getTreeItemProps() {
+ const {
+ item,
+ depth,
+ focused,
+ expanded,
+ onCmdCtrlClick,
+ onDoubleClick,
+ dimTopLevelWindow,
+ onContextMenu,
+ } = this.props;
+
+ const classNames = ["node", "object-node"];
+ if (focused) {
+ classNames.push("focused");
+ }
+
+ if (nodeIsBlock(item)) {
+ classNames.push("block");
+ }
+
+ if (
+ !expanded &&
+ (nodeIsDefaultProperties(item) ||
+ nodeIsPrototype(item) ||
+ nodeIsGetter(item) ||
+ nodeIsSetter(item) ||
+ (dimTopLevelWindow === true && nodeIsWindow(item) && depth === 0))
+ ) {
+ classNames.push("lessen");
+ }
+
+ const parentElementProps = {
+ className: classNames.join(" "),
+ onClick: e => {
+ if (
+ onCmdCtrlClick &&
+ ((isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey))
+ ) {
+ onCmdCtrlClick(item, {
+ depth,
+ event: e,
+ focused,
+ expanded,
+ });
+ e.stopPropagation();
+ return;
+ }
+
+ // If this click happened because the user selected some text, bail out.
+ // Note that if the user selected some text before and then clicks here,
+ // the previously selected text will be first unselected, unless the
+ // user clicked on the arrow itself. Indeed because the arrow is an
+ // image, clicking on it does not remove any existing text selection.
+ // So we need to also check if the arrow was clicked.
+ if (
+ e.target &&
+ Utils.selection.documentHasSelection(e.target.ownerDocument) &&
+ !(e.target.matches && e.target.matches(".arrow"))
+ ) {
+ e.stopPropagation();
+ }
+ },
+ onContextMenu: e => onContextMenu(e, item),
+ };
+
+ if (onDoubleClick) {
+ parentElementProps.onDoubleClick = e => {
+ e.stopPropagation();
+ onDoubleClick(item, {
+ depth,
+ focused,
+ expanded,
+ });
+ };
+ }
+
+ return parentElementProps;
+ }
+
+ renderLabel(label) {
+ if (label === null || typeof label === "undefined") {
+ return null;
+ }
+
+ const { item, depth, focused, expanded, onLabelClick } = this.props;
+ return dom.span(
+ {
+ className: "object-label",
+ onClick: onLabelClick
+ ? event => {
+ event.stopPropagation();
+
+ // If the user selected text, bail out.
+ if (
+ Utils.selection.documentHasSelection(event.target.ownerDocument)
+ ) {
+ return;
+ }
+
+ onLabelClick(item, {
+ depth,
+ focused,
+ expanded,
+ setExpanded: this.props.setExpanded,
+ });
+ }
+ : undefined,
+ },
+ label
+ );
+ }
+
+ render() {
+ const { arrow, renderItemActions, item } = this.props;
+
+ const { label, value } = this.getLabelAndValue();
+ const labelElement = this.renderLabel(label);
+ const delimiter =
+ value && labelElement
+ ? dom.span({ className: "object-delimiter" }, ": ")
+ : null;
+
+ return dom.div(
+ this.getTreeItemProps(),
+ arrow,
+ labelElement,
+ delimiter,
+ value,
+ renderItemActions(item)
+ );
+ }
+}
+
+module.exports = ObjectInspectorItem;
diff --git a/devtools/client/shared/components/object-inspector/components/moz.build b/devtools/client/shared/components/object-inspector/components/moz.build
new file mode 100644
index 0000000000..a1744891f2
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "ObjectInspector.js",
+ "ObjectInspectorItem.js",
+)
diff --git a/devtools/client/shared/components/object-inspector/index.js b/devtools/client/shared/components/object-inspector/index.js
new file mode 100644
index 0000000000..34e4d30086
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/index.js
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const ObjectInspector = require("resource://devtools/client/shared/components/object-inspector/components/ObjectInspector.js");
+const utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const reducer = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+const actions = require("resource://devtools/client/shared/components/object-inspector/actions.js");
+
+module.exports = { ObjectInspector, utils, actions, reducer };
diff --git a/devtools/client/shared/components/object-inspector/moz.build b/devtools/client/shared/components/object-inspector/moz.build
new file mode 100644
index 0000000000..14f9c285ba
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "components",
+ "utils",
+]
+
+DevToolsModules(
+ "actions.js",
+ "index.js",
+ "reducer.js",
+)
diff --git a/devtools/client/shared/components/object-inspector/reducer.js b/devtools/client/shared/components/object-inspector/reducer.js
new file mode 100644
index 0000000000..aa8af2b529
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/reducer.js
@@ -0,0 +1,147 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+function initialOIState(overrides) {
+ return {
+ expandedPaths: new Set(),
+ loadedProperties: new Map(),
+ evaluations: new Map(),
+ watchpoints: new Map(),
+ ...overrides,
+ };
+}
+
+function reducer(state = initialOIState(), action = {}) {
+ const { type, data } = action;
+
+ const cloneState = overrides => ({ ...state, ...overrides });
+
+ if (type === "NODE_EXPAND") {
+ return cloneState({
+ expandedPaths: new Set(state.expandedPaths).add(data.node.path),
+ });
+ }
+
+ if (type === "NODE_COLLAPSE") {
+ const expandedPaths = new Set(state.expandedPaths);
+ expandedPaths.delete(data.node.path);
+ return cloneState({ expandedPaths });
+ }
+
+ if (type == "SET_WATCHPOINT") {
+ const { watchpoint, property, path } = data;
+ const obj = state.loadedProperties.get(path);
+
+ return cloneState({
+ loadedProperties: new Map(state.loadedProperties).set(
+ path,
+ updateObject(obj, property, watchpoint)
+ ),
+ watchpoints: new Map(state.watchpoints).set(data.actor, data.watchpoint),
+ });
+ }
+
+ if (type === "REMOVE_WATCHPOINT") {
+ const { path, property, actor } = data;
+ const obj = state.loadedProperties.get(path);
+ const watchpoints = new Map(state.watchpoints);
+ watchpoints.delete(actor);
+
+ return cloneState({
+ loadedProperties: new Map(state.loadedProperties).set(
+ path,
+ updateObject(obj, property, null)
+ ),
+ watchpoints: watchpoints,
+ });
+ }
+
+ if (type === "NODE_PROPERTIES_LOADED") {
+ return cloneState({
+ loadedProperties: new Map(state.loadedProperties).set(
+ data.node.path,
+ action.data.properties
+ ),
+ });
+ }
+
+ if (type === "ROOTS_CHANGED") {
+ return cloneState();
+ }
+
+ if (type === "GETTER_INVOKED") {
+ return cloneState({
+ evaluations: new Map(state.evaluations).set(data.node.path, {
+ getterValue:
+ data.result &&
+ data.result.value &&
+ (data.result.value.throw || data.result.value.return),
+ }),
+ });
+ }
+
+ // NOTE: we clear the state on resume because otherwise the scopes pane
+ // would be out of date. Bug 1514760
+ if (type === "RESUME" || type == "NAVIGATE") {
+ return initialOIState({ watchpoints: state.watchpoints });
+ }
+
+ return state;
+}
+
+function updateObject(obj, property, watchpoint) {
+ return {
+ ...obj,
+ ownProperties: {
+ ...obj.ownProperties,
+ [property]: {
+ ...obj.ownProperties[property],
+ watchpoint,
+ },
+ },
+ };
+}
+
+function getObjectInspectorState(state) {
+ return state.objectInspector || state;
+}
+
+function getExpandedPaths(state) {
+ return getObjectInspectorState(state).expandedPaths;
+}
+
+function getExpandedPathKeys(state) {
+ return [...getExpandedPaths(state).keys()];
+}
+
+function getWatchpoints(state) {
+ return getObjectInspectorState(state).watchpoints;
+}
+
+function getLoadedProperties(state) {
+ return getObjectInspectorState(state).loadedProperties;
+}
+
+function getLoadedPropertyKeys(state) {
+ return [...getLoadedProperties(state).keys()];
+}
+
+function getEvaluations(state) {
+ return getObjectInspectorState(state).evaluations;
+}
+
+const selectors = {
+ getWatchpoints,
+ getEvaluations,
+ getExpandedPathKeys,
+ getExpandedPaths,
+ getLoadedProperties,
+ getLoadedPropertyKeys,
+};
+
+Object.defineProperty(module.exports, "__esModule", {
+ value: true,
+});
+module.exports = { ...selectors, initialOIState };
+module.exports.default = reducer;
diff --git a/devtools/client/shared/components/object-inspector/utils/client.js b/devtools/client/shared/components/object-inspector/utils/client.js
new file mode 100644
index 0000000000..eaa42be05a
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/client.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ getValue,
+ nodeHasFullText,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+async function enumIndexedProperties(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumProperties({
+ ignoreNonIndexedProperties: true,
+ });
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumIndexedProperties", e);
+ return {};
+ }
+}
+
+async function enumNonIndexedProperties(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumProperties({
+ ignoreIndexedProperties: true,
+ });
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumNonIndexedProperties", e);
+ return {};
+ }
+}
+
+async function enumEntries(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumEntries();
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumEntries", e);
+ return {};
+ }
+}
+
+async function enumSymbols(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumSymbols();
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumSymbols", e);
+ return {};
+ }
+}
+
+async function enumPrivateProperties(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumPrivateProperties();
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumPrivateProperties", e);
+ return {};
+ }
+}
+
+async function getPrototype(objectFront) {
+ if (typeof objectFront.getPrototype !== "function") {
+ console.error("objectFront.getPrototype is not a function");
+ return Promise.resolve({});
+ }
+ return objectFront.getPrototype();
+}
+
+async function getFullText(longStringFront, item) {
+ const { initial, fullText, length } = getValue(item);
+ // Return fullText property if it exists so that it can be added to the
+ // loadedProperties map.
+ if (nodeHasFullText(item)) {
+ return { fullText };
+ }
+
+ try {
+ const substring = await longStringFront.substring(initial.length, length);
+ return {
+ fullText: initial + substring,
+ };
+ } catch (e) {
+ console.error("LongStringFront.substring", e);
+ throw e;
+ }
+}
+
+async function getPromiseState(objectFront) {
+ return objectFront.getPromiseState();
+}
+
+async function getProxySlots(objectFront) {
+ return objectFront.getProxySlots();
+}
+
+function iteratorSlice(iterator, start, end) {
+ start = start || 0;
+ const count = end ? end - start + 1 : iterator.count;
+
+ if (count === 0) {
+ return Promise.resolve({});
+ }
+ return iterator.slice(start, count);
+}
+
+module.exports = {
+ enumEntries,
+ enumIndexedProperties,
+ enumNonIndexedProperties,
+ enumPrivateProperties,
+ enumSymbols,
+ getPrototype,
+ getFullText,
+ getPromiseState,
+ getProxySlots,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/index.js b/devtools/client/shared/components/object-inspector/utils/index.js
new file mode 100644
index 0000000000..13b3fd0049
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/index.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const client = require("resource://devtools/client/shared/components/object-inspector/utils/client.js");
+const loadProperties = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js");
+const node = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const { nodeIsError, nodeIsPrimitive } = node;
+const selection = require("resource://devtools/client/shared/components/object-inspector/utils/selection.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ REPS: { Rep, Grip },
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+function shouldRenderRootsInReps(roots, props = {}) {
+ if (roots.length !== 1) {
+ return false;
+ }
+
+ const root = roots[0];
+ const name = root && root.name;
+
+ return (
+ (name === null || typeof name === "undefined") &&
+ (nodeIsPrimitive(root) ||
+ (root?.contents?.value?.useCustomFormatter === true &&
+ Array.isArray(root?.contents?.value?.header)) ||
+ (nodeIsError(root) && props?.customFormat === true))
+ );
+}
+
+function renderRep(item, props) {
+ return Rep({
+ ...props,
+ front: item.contents.front,
+ object: node.getValue(item),
+ mode: props.mode || MODE.TINY,
+ defaultRep: Grip,
+ });
+}
+
+module.exports = {
+ client,
+ loadProperties,
+ node,
+ renderRep,
+ selection,
+ shouldRenderRootsInReps,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/load-properties.js b/devtools/client/shared/components/object-inspector/utils/load-properties.js
new file mode 100644
index 0000000000..42525e54f1
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/load-properties.js
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ enumEntries,
+ enumIndexedProperties,
+ enumNonIndexedProperties,
+ enumPrivateProperties,
+ enumSymbols,
+ getPrototype,
+ getFullText,
+ getPromiseState,
+ getProxySlots,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/client.js");
+
+const {
+ getClosestGripNode,
+ getClosestNonBucketNode,
+ getFront,
+ getValue,
+ nodeHasAccessors,
+ nodeHasProperties,
+ nodeIsBucket,
+ nodeIsDefaultProperties,
+ nodeIsEntries,
+ nodeIsMapEntry,
+ nodeIsPrimitive,
+ nodeIsPromise,
+ nodeIsProxy,
+ nodeNeedsNumericalBuckets,
+ nodeIsLongString,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+function loadItemProperties(item, client, loadedProperties, threadActorID) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+ let front = getFront(gripItem);
+
+ if (!front && value && client && client.getFrontByID) {
+ front = client.getFrontByID(value.actor);
+ }
+
+ const getObjectFront = function() {
+ if (!front) {
+ front = client.createObjectFront(
+ value,
+ client.getFrontByID(threadActorID)
+ );
+ }
+
+ return front;
+ };
+
+ const [start, end] = item.meta
+ ? [item.meta.startIndex, item.meta.endIndex]
+ : [];
+
+ const promises = [];
+
+ if (shouldLoadItemIndexedProperties(item, loadedProperties)) {
+ promises.push(enumIndexedProperties(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemNonIndexedProperties(item, loadedProperties)) {
+ promises.push(enumNonIndexedProperties(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemEntries(item, loadedProperties)) {
+ promises.push(enumEntries(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemPrototype(item, loadedProperties)) {
+ promises.push(getPrototype(getObjectFront()));
+ }
+
+ if (shouldLoadItemPrivateProperties(item, loadedProperties)) {
+ promises.push(enumPrivateProperties(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemSymbols(item, loadedProperties)) {
+ promises.push(enumSymbols(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemFullText(item, loadedProperties)) {
+ const longStringFront = front || client.createLongStringFront(value);
+ promises.push(getFullText(longStringFront, item));
+ }
+
+ if (shouldLoadItemPromiseState(item, loadedProperties)) {
+ promises.push(getPromiseState(getObjectFront()));
+ }
+
+ if (shouldLoadItemProxySlots(item, loadedProperties)) {
+ promises.push(getProxySlots(getObjectFront()));
+ }
+
+ return Promise.all(promises).then(mergeResponses);
+}
+
+function mergeResponses(responses) {
+ const data = {};
+
+ for (const response of responses) {
+ if (response.hasOwnProperty("ownProperties")) {
+ data.ownProperties = { ...data.ownProperties, ...response.ownProperties };
+ }
+
+ if (response.privateProperties && response.privateProperties.length > 0) {
+ data.privateProperties = response.privateProperties;
+ }
+
+ if (response.ownSymbols && response.ownSymbols.length > 0) {
+ data.ownSymbols = response.ownSymbols;
+ }
+
+ if (response.prototype) {
+ data.prototype = response.prototype;
+ }
+
+ if (response.fullText) {
+ data.fullText = response.fullText;
+ }
+
+ if (response.promiseState) {
+ data.promiseState = response.promiseState;
+ }
+
+ if (response.proxyTarget && response.proxyHandler) {
+ data.proxyTarget = response.proxyTarget;
+ data.proxyHandler = response.proxyHandler;
+ }
+ }
+
+ return data;
+}
+
+function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ nodeHasProperties(gripItem) &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsProxy(item) &&
+ !nodeNeedsNumericalBuckets(item) &&
+ !nodeIsEntries(getClosestNonBucketNode(item)) &&
+ // The data is loaded when expanding the window node.
+ !nodeIsDefaultProperties(item)
+ );
+}
+
+function shouldLoadItemNonIndexedProperties(
+ item,
+ loadedProperties = new Map()
+) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ nodeHasProperties(gripItem) &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsProxy(item) &&
+ !nodeIsEntries(getClosestNonBucketNode(item)) &&
+ !nodeIsBucket(item) &&
+ // The data is loaded when expanding the window node.
+ !nodeIsDefaultProperties(item)
+ );
+}
+
+function shouldLoadItemEntries(item, loadedProperties = new Map()) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ nodeIsEntries(getClosestNonBucketNode(item)) &&
+ !loadedProperties.has(item.path) &&
+ !nodeNeedsNumericalBuckets(item)
+ );
+}
+
+function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
+ const value = getValue(item);
+
+ return (
+ value &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsBucket(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsDefaultProperties(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsPrimitive(item) &&
+ !nodeIsLongString(item) &&
+ !nodeIsProxy(item)
+ );
+}
+
+function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
+ const value = getValue(item);
+
+ return (
+ value &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsBucket(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsDefaultProperties(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsPrimitive(item) &&
+ !nodeIsLongString(item) &&
+ !nodeIsProxy(item)
+ );
+}
+
+function shouldLoadItemPrivateProperties(item, loadedProperties = new Map()) {
+ const value = getValue(item);
+
+ return (
+ value &&
+ value?.preview?.privatePropertiesLength &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsBucket(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsDefaultProperties(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsPrimitive(item) &&
+ !nodeIsLongString(item) &&
+ !nodeIsProxy(item)
+ );
+}
+
+function shouldLoadItemFullText(item, loadedProperties = new Map()) {
+ return !loadedProperties.has(item.path) && nodeIsLongString(item);
+}
+
+function shouldLoadItemPromiseState(item, loadedProperties = new Map()) {
+ return !loadedProperties.has(item.path) && nodeIsPromise(item);
+}
+
+function shouldLoadItemProxySlots(item, loadedProperties = new Map()) {
+ return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
+module.exports = {
+ loadItemProperties,
+ mergeResponses,
+ shouldLoadItemEntries,
+ shouldLoadItemIndexedProperties,
+ shouldLoadItemNonIndexedProperties,
+ shouldLoadItemPrototype,
+ shouldLoadItemSymbols,
+ shouldLoadItemFullText,
+ shouldLoadItemPromiseState,
+ shouldLoadItemProxySlots,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/moz.build b/devtools/client/shared/components/object-inspector/utils/moz.build
new file mode 100644
index 0000000000..1301b2aca6
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "client.js",
+ "index.js",
+ "load-properties.js",
+ "node.js",
+ "selection.js",
+)
diff --git a/devtools/client/shared/components/object-inspector/utils/node.js b/devtools/client/shared/components/object-inspector/utils/node.js
new file mode 100644
index 0000000000..7b4d1fb0ce
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/node.js
@@ -0,0 +1,1039 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ maybeEscapePropertyName,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const ArrayRep = require("resource://devtools/client/shared/components/reps/reps/array.js");
+const GripArrayRep = require("resource://devtools/client/shared/components/reps/reps/grip-array.js");
+const GripMap = require("resource://devtools/client/shared/components/reps/reps/grip-map.js");
+const GripEntryRep = require("resource://devtools/client/shared/components/reps/reps/grip-entry.js");
+const ErrorRep = require("resource://devtools/client/shared/components/reps/reps/error.js");
+const BigIntRep = require("resource://devtools/client/shared/components/reps/reps/big-int.js");
+const {
+ isLongString,
+} = require("resource://devtools/client/shared/components/reps/reps/string.js");
+
+const MAX_NUMERICAL_PROPERTIES = 100;
+
+const NODE_TYPES = {
+ BUCKET: Symbol("[n…m]"),
+ DEFAULT_PROPERTIES: Symbol("<default properties>"),
+ ENTRIES: Symbol("<entries>"),
+ GET: Symbol("<get>"),
+ GRIP: Symbol("GRIP"),
+ MAP_ENTRY_KEY: Symbol("<key>"),
+ MAP_ENTRY_VALUE: Symbol("<value>"),
+ PROMISE_REASON: Symbol("<reason>"),
+ PROMISE_STATE: Symbol("<state>"),
+ PROMISE_VALUE: Symbol("<value>"),
+ PROXY_HANDLER: Symbol("<handler>"),
+ PROXY_TARGET: Symbol("<target>"),
+ SET: Symbol("<set>"),
+ PROTOTYPE: Symbol("<prototype>"),
+ BLOCK: Symbol("☲"),
+};
+
+let WINDOW_PROPERTIES = {};
+
+if (typeof window === "object") {
+ WINDOW_PROPERTIES = Object.getOwnPropertyNames(window);
+}
+
+function getType(item) {
+ return item.type;
+}
+
+function getValue(item) {
+ if (nodeHasValue(item)) {
+ return item.contents.value;
+ }
+
+ if (nodeHasGetterValue(item)) {
+ return item.contents.getterValue;
+ }
+
+ if (nodeHasAccessors(item)) {
+ return item.contents;
+ }
+
+ return undefined;
+}
+
+function getFront(item) {
+ return item && item.contents && item.contents.front;
+}
+
+function getActor(item, roots) {
+ const isRoot = isNodeRoot(item, roots);
+ const value = getValue(item);
+ return isRoot || !value ? null : value.actor;
+}
+
+function isNodeRoot(item, roots) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ roots.some(root => {
+ const rootValue = getValue(root);
+ return rootValue && rootValue.actor === value.actor;
+ })
+ );
+}
+
+function nodeIsBucket(item) {
+ return getType(item) === NODE_TYPES.BUCKET;
+}
+
+function nodeIsEntries(item) {
+ return getType(item) === NODE_TYPES.ENTRIES;
+}
+
+function nodeIsMapEntry(item) {
+ return GripEntryRep.supportsObject(getValue(item));
+}
+
+function nodeHasChildren(item) {
+ return Array.isArray(item.contents);
+}
+
+function nodeHasValue(item) {
+ return item && item.contents && item.contents.hasOwnProperty("value");
+}
+
+function nodeHasGetterValue(item) {
+ return item && item.contents && item.contents.hasOwnProperty("getterValue");
+}
+
+function nodeIsObject(item) {
+ const value = getValue(item);
+ return value && value.type === "object";
+}
+
+function nodeIsArrayLike(item) {
+ const value = getValue(item);
+ return GripArrayRep.supportsObject(value) || ArrayRep.supportsObject(value);
+}
+
+function nodeIsFunction(item) {
+ const value = getValue(item);
+ return value && value.class === "Function";
+}
+
+function nodeIsOptimizedOut(item) {
+ const value = getValue(item);
+ return !nodeHasChildren(item) && value && value.optimizedOut;
+}
+
+function nodeIsUninitializedBinding(item) {
+ const value = getValue(item);
+ return value && value.uninitialized;
+}
+
+// Used to check if an item represents a binding that exists in a sourcemap's
+// original file content, but does not match up with a binding found in the
+// generated code.
+function nodeIsUnmappedBinding(item) {
+ const value = getValue(item);
+ return value && value.unmapped;
+}
+
+// Used to check if an item represents a binding that exists in the debugger's
+// parser result, but does not match up with a binding returned by the
+// devtools server.
+function nodeIsUnscopedBinding(item) {
+ const value = getValue(item);
+ return value && value.unscoped;
+}
+
+function nodeIsMissingArguments(item) {
+ const value = getValue(item);
+ return !nodeHasChildren(item) && value && value.missingArguments;
+}
+
+function nodeHasProperties(item) {
+ return !nodeHasChildren(item) && nodeIsObject(item);
+}
+
+function nodeIsPrimitive(item) {
+ return (
+ nodeIsBigInt(item) ||
+ (!nodeHasChildren(item) &&
+ !nodeHasProperties(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsBucket(item) &&
+ !nodeIsLongString(item))
+ );
+}
+
+function nodeIsDefaultProperties(item) {
+ return getType(item) === NODE_TYPES.DEFAULT_PROPERTIES;
+}
+
+function isDefaultWindowProperty(name) {
+ return WINDOW_PROPERTIES.includes(name);
+}
+
+function nodeIsPromise(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ return value.class == "Promise";
+}
+
+function nodeIsProxy(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ return value.class == "Proxy";
+}
+
+function nodeIsPrototype(item) {
+ return getType(item) === NODE_TYPES.PROTOTYPE;
+}
+
+function nodeIsWindow(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ return value.class == "Window";
+}
+
+function nodeIsGetter(item) {
+ return getType(item) === NODE_TYPES.GET;
+}
+
+function nodeIsSetter(item) {
+ return getType(item) === NODE_TYPES.SET;
+}
+
+function nodeIsBlock(item) {
+ return getType(item) === NODE_TYPES.BLOCK;
+}
+
+function nodeIsError(item) {
+ return ErrorRep.supportsObject(getValue(item));
+}
+
+function nodeIsLongString(item) {
+ return isLongString(getValue(item));
+}
+
+function nodeIsBigInt(item) {
+ return BigIntRep.supportsObject(getValue(item));
+}
+
+function nodeHasFullText(item) {
+ const value = getValue(item);
+ return nodeIsLongString(item) && value.hasOwnProperty("fullText");
+}
+
+function nodeHasGetter(item) {
+ const getter = getNodeGetter(item);
+ return getter && getter.type !== "undefined";
+}
+
+function nodeHasSetter(item) {
+ const setter = getNodeSetter(item);
+ return setter && setter.type !== "undefined";
+}
+
+function nodeHasAccessors(item) {
+ return nodeHasGetter(item) || nodeHasSetter(item);
+}
+
+function nodeSupportsNumericalBucketing(item) {
+ // We exclude elements with entries since it's the <entries> node
+ // itself that can have buckets.
+ return (
+ (nodeIsArrayLike(item) && !nodeHasEntries(item)) ||
+ nodeIsEntries(item) ||
+ nodeIsBucket(item)
+ );
+}
+
+function nodeHasEntries(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ const className = value.class;
+ return (
+ className === "Map" ||
+ className === "Set" ||
+ className === "WeakMap" ||
+ className === "WeakSet" ||
+ className === "Storage" ||
+ className === "URLSearchParams" ||
+ className === "Headers" ||
+ className === "FormData" ||
+ className === "MIDIInputMap" ||
+ className === "MIDIOutputMap"
+ );
+}
+
+function nodeNeedsNumericalBuckets(item) {
+ return (
+ nodeSupportsNumericalBucketing(item) &&
+ getNumericalPropertiesCount(item) > MAX_NUMERICAL_PROPERTIES
+ );
+}
+
+function makeNodesForPromiseProperties(loadedProps, item) {
+ const { reason, value, state } = loadedProps.promiseState;
+ const properties = [];
+
+ if (state) {
+ properties.push(
+ createNode({
+ parent: item,
+ name: "<state>",
+ contents: { value: state },
+ type: NODE_TYPES.PROMISE_STATE,
+ })
+ );
+ }
+
+ if (reason) {
+ properties.push(
+ createNode({
+ parent: item,
+ name: "<reason>",
+ contents: {
+ value: reason.getGrip ? reason.getGrip() : reason,
+ front: reason.getGrip ? reason : null,
+ },
+ type: NODE_TYPES.PROMISE_REASON,
+ })
+ );
+ }
+
+ if (value) {
+ properties.push(
+ createNode({
+ parent: item,
+ name: "<value>",
+ contents: {
+ value: value.getGrip ? value.getGrip() : value,
+ front: value.getGrip ? value : null,
+ },
+ type: NODE_TYPES.PROMISE_VALUE,
+ })
+ );
+ }
+
+ return properties;
+}
+
+function makeNodesForProxyProperties(loadedProps, item) {
+ const { proxyHandler, proxyTarget } = loadedProps;
+
+ const isProxyHandlerFront = proxyHandler && proxyHandler.getGrip;
+ const proxyHandlerGrip = isProxyHandlerFront
+ ? proxyHandler.getGrip()
+ : proxyHandler;
+ const proxyHandlerFront = isProxyHandlerFront ? proxyHandler : null;
+
+ const isProxyTargetFront = proxyTarget && proxyTarget.getGrip;
+ const proxyTargetGrip = isProxyTargetFront
+ ? proxyTarget.getGrip()
+ : proxyTarget;
+ const proxyTargetFront = isProxyTargetFront ? proxyTarget : null;
+
+ return [
+ createNode({
+ parent: item,
+ name: "<target>",
+ contents: { value: proxyTargetGrip, front: proxyTargetFront },
+ type: NODE_TYPES.PROXY_TARGET,
+ }),
+ createNode({
+ parent: item,
+ name: "<handler>",
+ contents: { value: proxyHandlerGrip, front: proxyHandlerFront },
+ type: NODE_TYPES.PROXY_HANDLER,
+ }),
+ ];
+}
+
+function makeNodesForEntries(item) {
+ const nodeName = "<entries>";
+
+ return createNode({
+ parent: item,
+ name: nodeName,
+ contents: null,
+ type: NODE_TYPES.ENTRIES,
+ });
+}
+
+function makeNodesForMapEntry(item) {
+ const nodeValue = getValue(item);
+ if (!nodeValue || !nodeValue.preview) {
+ return [];
+ }
+
+ const { key, value } = nodeValue.preview;
+ const isKeyFront = key && key.getGrip;
+ const keyGrip = isKeyFront ? key.getGrip() : key;
+ const keyFront = isKeyFront ? key : null;
+
+ const isValueFront = value && value.getGrip;
+ const valueGrip = isValueFront ? value.getGrip() : value;
+ const valueFront = isValueFront ? value : null;
+
+ return [
+ createNode({
+ parent: item,
+ name: "<key>",
+ contents: { value: keyGrip, front: keyFront },
+ type: NODE_TYPES.MAP_ENTRY_KEY,
+ }),
+ createNode({
+ parent: item,
+ name: "<value>",
+ contents: { value: valueGrip, front: valueFront },
+ type: NODE_TYPES.MAP_ENTRY_VALUE,
+ }),
+ ];
+}
+
+function getNodeGetter(item) {
+ return item && item.contents ? item.contents.get : undefined;
+}
+
+function getNodeSetter(item) {
+ return item && item.contents ? item.contents.set : undefined;
+}
+
+function sortProperties(properties) {
+ return properties.sort((a, b) => {
+ // Sort numbers in ascending order and sort strings lexicographically
+ const aInt = parseInt(a, 10);
+ const bInt = parseInt(b, 10);
+
+ if (isNaN(aInt) || isNaN(bInt)) {
+ return a > b ? 1 : -1;
+ }
+
+ return aInt - bInt;
+ });
+}
+
+function makeNumericalBuckets(parent) {
+ const numProperties = getNumericalPropertiesCount(parent);
+
+ // We want to have at most a hundred slices.
+ const bucketSize =
+ 10 ** Math.max(2, Math.ceil(Math.log10(numProperties)) - 2);
+ const numBuckets = Math.ceil(numProperties / bucketSize);
+
+ const buckets = [];
+ for (let i = 1; i <= numBuckets; i++) {
+ const minKey = (i - 1) * bucketSize;
+ const maxKey = Math.min(i * bucketSize - 1, numProperties - 1);
+ const startIndex = nodeIsBucket(parent) ? parent.meta.startIndex : 0;
+ const minIndex = startIndex + minKey;
+ const maxIndex = startIndex + maxKey;
+ const bucketName = `[${minIndex}…${maxIndex}]`;
+
+ buckets.push(
+ createNode({
+ parent,
+ name: bucketName,
+ contents: null,
+ type: NODE_TYPES.BUCKET,
+ meta: {
+ startIndex: minIndex,
+ endIndex: maxIndex,
+ },
+ })
+ );
+ }
+ return buckets;
+}
+
+function makeDefaultPropsBucket(propertiesNames, parent, ownProperties) {
+ const userPropertiesNames = [];
+ const defaultProperties = [];
+
+ propertiesNames.forEach(name => {
+ if (isDefaultWindowProperty(name)) {
+ defaultProperties.push(name);
+ } else {
+ userPropertiesNames.push(name);
+ }
+ });
+
+ const nodes = makeNodesForOwnProps(
+ userPropertiesNames,
+ parent,
+ ownProperties
+ );
+
+ if (defaultProperties.length > 0) {
+ const defaultPropertiesNode = createNode({
+ parent,
+ name: "<default properties>",
+ contents: null,
+ type: NODE_TYPES.DEFAULT_PROPERTIES,
+ });
+
+ const defaultNodes = makeNodesForOwnProps(
+ defaultProperties,
+ defaultPropertiesNode,
+ ownProperties
+ );
+ nodes.push(setNodeChildren(defaultPropertiesNode, defaultNodes));
+ }
+ return nodes;
+}
+
+function makeNodesForOwnProps(propertiesNames, parent, ownProperties) {
+ return propertiesNames.map(name => {
+ const property = ownProperties[name];
+
+ let propertyValue = property;
+ if (property && property.hasOwnProperty("getterValue")) {
+ propertyValue = property.getterValue;
+ } else if (property && property.hasOwnProperty("value")) {
+ propertyValue = property.value;
+ }
+
+ // propertyValue can be a front (LongString or Object) or a primitive grip.
+ const isFront = propertyValue && propertyValue.getGrip;
+ const front = isFront ? propertyValue : null;
+ const grip = isFront ? front.getGrip() : propertyValue;
+
+ return createNode({
+ parent,
+ name: maybeEscapePropertyName(name),
+ propertyName: name,
+ contents: {
+ ...(property || {}),
+ value: grip,
+ front,
+ },
+ });
+ });
+}
+
+function makeNodesForProperties(objProps, parent) {
+ const {
+ ownProperties = {},
+ ownSymbols,
+ privateProperties,
+ prototype,
+ safeGetterValues,
+ } = objProps;
+
+ const parentValue = getValue(parent);
+ const allProperties = { ...ownProperties, ...safeGetterValues };
+
+ // Ignore properties that are neither non-concrete nor getters/setters.
+ const propertiesNames = sortProperties(Object.keys(allProperties)).filter(
+ name => {
+ if (!allProperties[name]) {
+ return false;
+ }
+
+ const properties = Object.getOwnPropertyNames(allProperties[name]);
+ return properties.some(property =>
+ ["value", "getterValue", "get", "set"].includes(property)
+ );
+ }
+ );
+
+ const isParentNodeWindow = parentValue && parentValue.class == "Window";
+ const nodes = isParentNodeWindow
+ ? makeDefaultPropsBucket(propertiesNames, parent, allProperties)
+ : makeNodesForOwnProps(propertiesNames, parent, allProperties);
+
+ if (Array.isArray(ownSymbols)) {
+ ownSymbols.forEach((ownSymbol, index) => {
+ const descriptorValue = ownSymbol?.descriptor?.value;
+ const hasGrip = descriptorValue?.getGrip;
+ const symbolGrip = hasGrip ? descriptorValue.getGrip() : descriptorValue;
+ const symbolFront = hasGrip ? ownSymbol.descriptor.value : null;
+
+ nodes.push(
+ createNode({
+ parent,
+ name: ownSymbol.name,
+ path: `symbol-${index}`,
+ contents: {
+ value: symbolGrip,
+ front: symbolFront,
+ },
+ })
+ );
+ }, this);
+ }
+
+ if (Array.isArray(privateProperties)) {
+ privateProperties.forEach((privateProperty, index) => {
+ const descriptorValue = privateProperty?.descriptor?.value;
+ const hasGrip = descriptorValue?.getGrip;
+ const privatePropertyGrip = hasGrip
+ ? descriptorValue.getGrip()
+ : descriptorValue;
+ const privatePropertyFront = hasGrip
+ ? privateProperty.descriptor.value
+ : null;
+
+ nodes.push(
+ createNode({
+ parent,
+ name: privateProperty.name,
+ path: `private-${index}`,
+ contents: {
+ value: privatePropertyGrip,
+ front: privatePropertyFront,
+ },
+ })
+ );
+ }, this);
+ }
+
+ if (nodeIsPromise(parent)) {
+ nodes.push(...makeNodesForPromiseProperties(objProps, parent));
+ }
+
+ if (nodeHasEntries(parent)) {
+ nodes.push(makeNodesForEntries(parent));
+ }
+
+ // Add accessor nodes if needed
+ const defaultPropertiesNode = isParentNodeWindow
+ ? nodes.find(node => nodeIsDefaultProperties(node))
+ : null;
+
+ for (const name of propertiesNames) {
+ const property = allProperties[name];
+ const isDefaultProperty =
+ isParentNodeWindow &&
+ defaultPropertiesNode &&
+ isDefaultWindowProperty(name);
+ const parentNode = isDefaultProperty ? defaultPropertiesNode : parent;
+ const parentContentsArray =
+ isDefaultProperty && defaultPropertiesNode
+ ? defaultPropertiesNode.contents
+ : nodes;
+
+ if (property.get && property.get.type !== "undefined") {
+ parentContentsArray.push(
+ createGetterNode({
+ parent: parentNode,
+ property,
+ name,
+ })
+ );
+ }
+
+ if (property.set && property.set.type !== "undefined") {
+ parentContentsArray.push(
+ createSetterNode({
+ parent: parentNode,
+ property,
+ name,
+ })
+ );
+ }
+ }
+
+ // Add the prototype if it exists and is not null
+ if (prototype && prototype.type !== "null") {
+ nodes.push(makeNodeForPrototype(objProps, parent));
+ }
+
+ return nodes;
+}
+
+function setNodeFullText(loadedProps, node) {
+ if (nodeHasFullText(node) || !nodeIsLongString(node)) {
+ return node;
+ }
+
+ const { fullText } = loadedProps;
+ if (nodeHasValue(node)) {
+ node.contents.value.fullText = fullText;
+ } else if (nodeHasGetterValue(node)) {
+ node.contents.getterValue.fullText = fullText;
+ }
+
+ return node;
+}
+
+function makeNodeForPrototype(objProps, parent) {
+ const { prototype } = objProps || {};
+
+ // Add the prototype if it exists and is not null
+ if (prototype && prototype.type !== "null") {
+ return createNode({
+ parent,
+ name: "<prototype>",
+ contents: {
+ value: prototype.getGrip ? prototype.getGrip() : prototype,
+ front: prototype.getGrip ? prototype : null,
+ },
+ type: NODE_TYPES.PROTOTYPE,
+ });
+ }
+
+ return null;
+}
+
+function createNode(options) {
+ const {
+ parent,
+ name,
+ propertyName,
+ path,
+ contents,
+ type = NODE_TYPES.GRIP,
+ meta,
+ } = options;
+
+ if (contents === undefined) {
+ return null;
+ }
+
+ // The path is important to uniquely identify the item in the entire
+ // tree. This helps debugging & optimizes React's rendering of large
+ // lists. The path will be separated by property name.
+
+ return {
+ parent,
+ name,
+ // `name` can be escaped; propertyName contains the original property name.
+ propertyName,
+ path: createPath(parent && parent.path, path || name),
+ contents,
+ type,
+ meta,
+ };
+}
+
+function createGetterNode({ parent, property, name }) {
+ const isFront = property.get && property.get.getGrip;
+ const grip = isFront ? property.get.getGrip() : property.get;
+ const front = isFront ? property.get : null;
+
+ return createNode({
+ parent,
+ name: `<get ${name}()>`,
+ contents: { value: grip, front },
+ type: NODE_TYPES.GET,
+ });
+}
+
+function createSetterNode({ parent, property, name }) {
+ const isFront = property.set && property.set.getGrip;
+ const grip = isFront ? property.set.getGrip() : property.set;
+ const front = isFront ? property.set : null;
+
+ return createNode({
+ parent,
+ name: `<set ${name}()>`,
+ contents: { value: grip, front },
+ type: NODE_TYPES.SET,
+ });
+}
+
+function setNodeChildren(node, children) {
+ node.contents = children;
+ return node;
+}
+
+function getEvaluatedItem(item, evaluations) {
+ if (!evaluations.has(item.path)) {
+ return item;
+ }
+
+ const evaluation = evaluations.get(item.path);
+ const isFront =
+ evaluation && evaluation.getterValue && evaluation.getterValue.getGrip;
+
+ const contents = isFront
+ ? {
+ getterValue: evaluation.getterValue.getGrip(),
+ front: evaluation.getterValue,
+ }
+ : evaluations.get(item.path);
+
+ return {
+ ...item,
+ contents,
+ };
+}
+
+function getChildrenWithEvaluations(options) {
+ const { item, loadedProperties, cachedNodes, evaluations } = options;
+
+ const children = getChildren({
+ loadedProperties,
+ cachedNodes,
+ item,
+ });
+
+ if (Array.isArray(children)) {
+ return children.map(i => getEvaluatedItem(i, evaluations));
+ }
+
+ if (children) {
+ return getEvaluatedItem(children, evaluations);
+ }
+
+ return [];
+}
+
+function getChildren(options) {
+ const { cachedNodes, item, loadedProperties = new Map() } = options;
+
+ const key = item.path;
+ if (cachedNodes && cachedNodes.has(key)) {
+ return cachedNodes.get(key);
+ }
+
+ const loadedProps = loadedProperties.get(key);
+ const hasLoadedProps = loadedProperties.has(key);
+
+ // Because we are dynamically creating the tree as the user
+ // expands it (not precalculated tree structure), we cache child
+ // arrays. This not only helps performance, but is necessary
+ // because the expanded state depends on instances of nodes
+ // being the same across renders. If we didn't do this, each
+ // node would be a new instance every render.
+ // If the node needs properties, we only add children to
+ // the cache if the properties are loaded.
+ const addToCache = children => {
+ if (cachedNodes) {
+ cachedNodes.set(item.path, children);
+ }
+ return children;
+ };
+
+ // Nodes can either have children already, or be an object with
+ // properties that we need to go and fetch.
+ if (nodeHasChildren(item)) {
+ return addToCache(item.contents);
+ }
+
+ if (nodeIsMapEntry(item)) {
+ return addToCache(makeNodesForMapEntry(item));
+ }
+
+ if (nodeIsProxy(item) && hasLoadedProps) {
+ return addToCache(makeNodesForProxyProperties(loadedProps, item));
+ }
+
+ if (nodeIsLongString(item) && hasLoadedProps) {
+ // Set longString object's fullText to fetched one.
+ return addToCache(setNodeFullText(loadedProps, item));
+ }
+
+ if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
+ // Even if we have numerical buckets, we should have loaded non indexed
+ // properties.
+ const bucketNodes = makeNumericalBuckets(item);
+ return addToCache(
+ bucketNodes.concat(makeNodesForProperties(loadedProps, item))
+ );
+ }
+
+ if (!nodeIsEntries(item) && !nodeIsBucket(item) && !nodeHasProperties(item)) {
+ return [];
+ }
+
+ if (!hasLoadedProps) {
+ return [];
+ }
+
+ return addToCache(makeNodesForProperties(loadedProps, item));
+}
+
+// Builds an expression that resolves to the value of the item in question
+// e.g. `b` in { a: { b: 2 } } resolves to `a.b`
+function getPathExpression(item) {
+ if (item && item.parent) {
+ const parent = nodeIsBucket(item.parent) ? item.parent.parent : item.parent;
+ return `${getPathExpression(parent)}.${item.name}`;
+ }
+
+ return item.name;
+}
+
+function getParent(item) {
+ return item.parent;
+}
+
+function getNumericalPropertiesCount(item) {
+ if (nodeIsBucket(item)) {
+ return item.meta.endIndex - item.meta.startIndex + 1;
+ }
+
+ const value = getValue(getClosestGripNode(item));
+ if (!value) {
+ return 0;
+ }
+
+ if (GripArrayRep.supportsObject(value)) {
+ return GripArrayRep.getLength(value);
+ }
+
+ if (GripMap.supportsObject(value)) {
+ return GripMap.getLength(value);
+ }
+
+ // TODO: We can also have numerical properties on Objects, but at the
+ // moment we don't have a way to distinguish them from non-indexed properties,
+ // as they are all computed in a ownPropertiesLength property.
+
+ return 0;
+}
+
+function getClosestGripNode(item) {
+ const type = getType(item);
+ if (
+ type !== NODE_TYPES.BUCKET &&
+ type !== NODE_TYPES.DEFAULT_PROPERTIES &&
+ type !== NODE_TYPES.ENTRIES
+ ) {
+ return item;
+ }
+
+ const parent = getParent(item);
+ if (!parent) {
+ return null;
+ }
+
+ return getClosestGripNode(parent);
+}
+
+function getClosestNonBucketNode(item) {
+ const type = getType(item);
+
+ if (type !== NODE_TYPES.BUCKET) {
+ return item;
+ }
+
+ const parent = getParent(item);
+ if (!parent) {
+ return null;
+ }
+
+ return getClosestNonBucketNode(parent);
+}
+
+function getParentGripNode(item) {
+ const parentNode = getParent(item);
+ if (!parentNode) {
+ return null;
+ }
+
+ return getClosestGripNode(parentNode);
+}
+
+function getParentGripValue(item) {
+ const parentGripNode = getParentGripNode(item);
+ if (!parentGripNode) {
+ return null;
+ }
+
+ return getValue(parentGripNode);
+}
+
+function getParentFront(item) {
+ const parentGripNode = getParentGripNode(item);
+ if (!parentGripNode) {
+ return null;
+ }
+
+ return getFront(parentGripNode);
+}
+
+function getNonPrototypeParentGripValue(item) {
+ const parentGripNode = getParentGripNode(item);
+ if (!parentGripNode) {
+ return null;
+ }
+
+ if (getType(parentGripNode) === NODE_TYPES.PROTOTYPE) {
+ return getNonPrototypeParentGripValue(parentGripNode);
+ }
+
+ return getValue(parentGripNode);
+}
+
+function createPath(parentPath, path) {
+ return parentPath ? `${parentPath}â—¦${path}` : path;
+}
+
+module.exports = {
+ createNode,
+ createGetterNode,
+ createSetterNode,
+ getActor,
+ getChildren,
+ getChildrenWithEvaluations,
+ getClosestGripNode,
+ getClosestNonBucketNode,
+ getEvaluatedItem,
+ getFront,
+ getPathExpression,
+ getParent,
+ getParentFront,
+ getParentGripValue,
+ getNonPrototypeParentGripValue,
+ getNumericalPropertiesCount,
+ getValue,
+ makeNodesForEntries,
+ makeNodesForPromiseProperties,
+ makeNodesForProperties,
+ makeNumericalBuckets,
+ nodeHasAccessors,
+ nodeHasChildren,
+ nodeHasEntries,
+ nodeHasProperties,
+ nodeHasGetter,
+ nodeHasSetter,
+ nodeIsBlock,
+ nodeIsBucket,
+ nodeIsDefaultProperties,
+ nodeIsEntries,
+ nodeIsError,
+ nodeIsLongString,
+ nodeHasFullText,
+ nodeIsFunction,
+ nodeIsGetter,
+ nodeIsMapEntry,
+ nodeIsMissingArguments,
+ nodeIsObject,
+ nodeIsOptimizedOut,
+ nodeIsPrimitive,
+ nodeIsPromise,
+ nodeIsPrototype,
+ nodeIsProxy,
+ nodeIsSetter,
+ nodeIsUninitializedBinding,
+ nodeIsUnmappedBinding,
+ nodeIsUnscopedBinding,
+ nodeIsWindow,
+ nodeNeedsNumericalBuckets,
+ nodeSupportsNumericalBucketing,
+ setNodeChildren,
+ sortProperties,
+ NODE_TYPES,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/selection.js b/devtools/client/shared/components/object-inspector/utils/selection.js
new file mode 100644
index 0000000000..fdcca7ff6b
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/selection.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+function documentHasSelection(doc = document) {
+ const selection = doc.defaultView.getSelection();
+ if (!selection) {
+ return false;
+ }
+
+ return selection.type === "Range";
+}
+
+module.exports = {
+ documentHasSelection,
+};
diff --git a/devtools/client/shared/components/reps/images/input.svg b/devtools/client/shared/components/reps/images/input.svg
new file mode 100644
index 0000000000..830b651e9e
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/input.svg
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
+ <path d="M11.04 5.46L7.29 1.71a.75.75 0 0 0-1.06 1.06L9.45 6 6.23 9.21a.75.75 0 1 0 1.06 1.06l3.75-3.75c.3-.3.3-.77 0-1.06z"/>
+ <path d="M6.04 5.46L2.29 1.71a.75.75 0 0 0-1.06 1.06L4.45 6 1.23 9.21a.75.75 0 1 0 1.06 1.06l3.75-3.75c.3-.3.3-.77 0-1.06z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/shared/components/reps/images/jump-definition.svg b/devtools/client/shared/components/reps/images/jump-definition.svg
new file mode 100644
index 0000000000..9ac071523d
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/jump-definition.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" stroke="context-stroke" fill="none" stroke-linecap="round">
+ <path d="M5.5 3.5l2 2M5.5 7.5l2-2"/>
+ <path d="M7 5.5H4.006c-1.012 0-1.995 1.017-2.011 2.024-.005.023-.005 1.347 0 3.971" stroke-linejoin="round"/>
+ <path d="M10.5 5.5h4M9.5 3.5h5M9.5 7.5h5"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/shared/components/reps/images/open-a11y.svg b/devtools/client/shared/components/reps/images/open-a11y.svg
new file mode 100644
index 0000000000..cba2ab93c9
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/open-a11y.svg
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="context-fill #0C0C0D">
+ <path d="M9.5 2.5C9.5 3.60457 8.60457 4.5 7.5 4.5C6.39543 4.5 5.5 3.60457 5.5 2.5C5.5 1.39543 6.39543 0.5 7.5 0.5C8.60457 0.5 9.5 1.39543 9.5 2.5Z"/>
+ <path d="M1.5 6C1.5 5.44772 1.94772 5 2.5 5H12.5C13.0523 5 13.5 5.44772 13.5 6C13.5 6.55228 13.0523 7 12.5 7H2.5C1.94772 7 1.5 6.55228 1.5 6Z"/>
+ <path d="M6 5C6.55228 5 7 5.44772 7 6L7 13C7 13.5523 6.55228 14 6 14C5.44771 14 5 13.5523 5 13L5 6C5 5.44772 5.44772 5 6 5Z"/>
+ <path d="M9 5C9.55228 5 10 5.44772 10 6V13C10 13.5523 9.55228 14 9 14C8.44771 14 8 13.5523 8 13L8 6C8 5.44772 8.44772 5 9 5Z"/>
+ <path d="M5 7H10V10.03H5V7Z"/>
+</svg>
diff --git a/devtools/client/shared/components/reps/images/open-inspector.svg b/devtools/client/shared/components/reps/images/open-inspector.svg
new file mode 100644
index 0000000000..9e8a277e7c
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/open-inspector.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="context-fill">
+ <path d="M7 3H5a2 2 0 0 0-2 2v2H1.5a.5.5 0 0 0 0 1H3v2c0 1.1.9 2 2 2h2v1.5a.5.5 0 0 0 1 0V12h2a2 2 0 0 0 2-2V8h1.5a.5.5 0 0 0 0-1H12V5a2 2 0 0 0-2-2H8V1.5a.5.5 0 0 0-1 0V3zM5 5h5v5H5V5z"/>
+</svg>
diff --git a/devtools/client/shared/components/reps/index.js b/devtools/client/shared/components/reps/index.js
new file mode 100644
index 0000000000..e99e642c38
--- /dev/null
+++ b/devtools/client/shared/components/reps/index.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ MODE,
+} = require("devtools/client/shared/components/reps/reps/constants");
+const {
+ REPS,
+ getRep,
+} = require("devtools/client/shared/components/reps/reps/rep");
+const objectInspector = require("devtools/client/shared/components/object-inspector/index");
+
+const {
+ parseURLEncodedText,
+ parseURLParams,
+ maybeEscapePropertyName,
+ getGripPreviewItems,
+} = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+module.exports = {
+ REPS,
+ getRep,
+ MODE,
+ maybeEscapePropertyName,
+ parseURLEncodedText,
+ parseURLParams,
+ getGripPreviewItems,
+ objectInspector,
+};
diff --git a/devtools/client/shared/components/reps/moz.build b/devtools/client/shared/components/reps/moz.build
new file mode 100644
index 0000000000..058e8046a7
--- /dev/null
+++ b/devtools/client/shared/components/reps/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "reps",
+ "shared",
+]
+
+DevToolsModules(
+ "index.js",
+)
diff --git a/devtools/client/shared/components/reps/reps.css b/devtools/client/shared/components/reps/reps.css
new file mode 100644
index 0000000000..da45f6d078
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps.css
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.theme-dark,
+.theme-light {
+ --number-color: var(--theme-highlight-green);
+ --string-color: var(--theme-highlight-red);
+ --null-color: var(--theme-comment);
+ --object-color: var(--theme-highlight-blue);
+ --caption-color: var(--theme-highlight-blue);
+ --location-color: var(--theme-comment);
+ --source-link-color: var(--theme-highlight-blue);
+ --node-color: var(--theme-highlight-purple);
+ --reference-color: var(--theme-highlight-blue);
+ --comment-node-color: var(--theme-comment);
+}
+
+/******************************************************************************/
+
+.inline {
+ display: inline;
+ white-space: normal;
+}
+
+.objectBox-object {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-symbol,
+.objectBox-text,
+.objectBox-textNode,
+.objectBox-table {
+ white-space: pre-wrap;
+}
+
+.objectBox * {
+ unicode-bidi: isolate;
+}
+
+.objectBox-number,
+.objectBox-styleRule,
+.objectBox-element,
+.objectBox-textNode,
+.objectBox-array > .length {
+ color: var(--number-color);
+}
+
+.objectBox-textNode,
+.objectBox-string,
+.objectBox-symbol {
+ color: var(--string-color);
+}
+
+.objectBox-empty-string {
+ font-style: italic;
+}
+
+.objectBox-string a {
+ word-break: break-all;
+}
+
+.objectBox-string a,
+.objectBox-string a:visited {
+ color: currentColor;
+ text-decoration: underline;
+ text-decoration-skip-ink: none;
+ font-style: italic;
+ cursor: pointer;
+}
+
+/* Visually hide the middle of "cropped" url */
+.objectBox-string a .cropped-url-middle {
+ max-width: 0;
+ max-height: 0;
+ display: inline-block;
+ overflow: hidden;
+ vertical-align: bottom;
+}
+
+.objectBox-string a .cropped-url-end::before {
+ content: "…";
+}
+
+
+.objectBox-function,
+.objectBox-profile {
+ color: var(--object-color);
+}
+
+.objectBox-stackTrace.reps-custom-format,
+.objectBox-stackTrace.reps-custom-format > .objectBox-string {
+ color: var(--error-color);
+}
+
+.objectBox-stackTrace-grid {
+ display: inline-grid;
+ grid-template-columns: auto auto;
+ margin-top: 3px;
+}
+
+.objectBox-stackTrace-fn {
+ color: var(--console-output-color);
+ padding-inline-start: 17px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-inline-end: 5px;
+}
+
+.objectBox-stackTrace-location {
+ color: var(--frame-link-source, currentColor);
+ direction: rtl;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: end;
+}
+
+.objectBox-stackTrace-location:hover {
+ text-decoration: underline;
+}
+
+.objectBox-stackTrace-location {
+ cursor: pointer;
+}
+
+.objectBox-Location,
+.location {
+ color: var(--location-color);
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.objectBox-nan,
+.logRowHint {
+ color: var(--null-color);
+}
+
+.objectBox-sourceLink {
+ position: absolute;
+ right: 4px;
+ top: 2px;
+ padding-left: 8px;
+ font-weight: bold;
+ color: var(--source-link-color);
+}
+
+.objectBox-failure {
+ color: var(--string-color);
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 2px;
+ font-size: 0.8em;
+ padding: 0 2px;
+}
+
+.objectBox-accessible.clickable,
+.objectBox-node.clickable {
+ cursor: pointer;
+}
+
+/* JsonML reps can be nested, though only the top-level rep needs layout
+ * adjustments to align it with the toggle arrow and fit its width to its
+ * contents. */
+.objectBox-jsonml-wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ width: fit-content;
+ word-break: break-word;
+ line-height: normal;
+}
+
+.objectBox-jsonml-wrapper[data-expandable="true"] {
+ cursor: default;
+}
+
+.objectBox-jsonml-wrapper .jsonml-header-collapse-button {
+ margin: 0 4px 2px 0;
+ padding: 0;
+ vertical-align: middle;
+}
+
+.objectBox-jsonml-wrapper .jsonml-header-collapse-button::before {
+ content: "";
+ display: block;
+ width: 10px;
+ height: 10px;
+ background: url("chrome://devtools/skin/images/arrow.svg") no-repeat center;
+ background-size: 10px;
+ transform: rotate(-90deg);
+ transition: transform 125ms ease;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+}
+
+.objectBox-jsonml-wrapper .jsonml-header-collapse-button[aria-expanded="true"]::before {
+ transform: rotate(0deg);
+}
+
+/******************************************************************************/
+
+.objectBox-event,
+.objectBox-eventLog,
+.objectBox-regexp,
+.objectBox-object {
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox .Date {
+ color: var(--string-color);
+ white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectBox.theme-comment {
+ color: var(--comment-node-color);
+}
+
+.accessible-role,
+.tag-name {
+ color: var(--object-color);
+}
+
+.attrName {
+ color: var(--string-color);
+}
+
+.attrEqual,
+.objectEqual {
+ color: var(--comment-node-color);
+}
+
+.attrValue,
+.attrValue.objectBox-string {
+ color: var(--node-color);
+}
+
+.angleBracket {
+ color: var(--theme-body-color);
+}
+
+/******************************************************************************/
+/* Length bubble for arraylikes and maplikes */
+
+.objectLengthBubble {
+ color: var(--null-color);
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+ color: var(--object-color);
+}
+
+/******************************************************************************/
+/* Cycle reference */
+
+.objectBox-Reference {
+ font-weight: bold;
+ color: var(--reference-color);
+}
+
+[class*="objectBox"] > .objectTitle {
+ color: var(--object-color);
+}
+
+.caption {
+ color: var(--caption-color);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+ font-style: normal;
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ font-weight: normal;
+ white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+ font-weight: normal;
+}
+
+/******************************************************************************/
+/* Open DOMNode in inspector or Accessible in accessibility inspector button */
+
+button.open-accessibility-inspector {
+ background: url("chrome://devtools/content/shared/components/reps/images/open-a11y.svg")
+ no-repeat;
+}
+
+button.open-inspector {
+ background: url("chrome://devtools/content/shared/components/reps/images/open-inspector.svg")
+ no-repeat;
+}
+
+button.highlight-node {
+ background: url("chrome://devtools/skin/images/highlight-selector.svg")
+ no-repeat;
+}
+
+
+button:is(.open-accessibility-inspector, .open-inspector, .highlight-node) {
+ display: inline-block;
+ vertical-align: top;
+ height: 15px;
+ width: 15px;
+ margin: 0 4px;
+ padding: 0;
+ border: none;
+ fill: var(--theme-icon-color);
+ cursor: pointer;
+ -moz-context-properties: fill;
+}
+
+.objectBox-accessible:hover .open-accessibility-inspector,
+.objectBox-node:hover .open-inspector,
+.objectBox-textNode:hover .open-inspector,
+.open-accessibility-inspector:hover,
+.highlight-node:hover,
+.open-inspector:hover {
+ fill: var(--theme-icon-checked-color);
+}
+
+/******************************************************************************/
+/* Jump to definition button */
+
+button.jump-definition {
+ display: inline-block;
+ height: 16px;
+ margin-left: 0.25em;
+ vertical-align: middle;
+ background: 0% 50%
+ url("chrome://devtools/content/shared/components/reps/images/jump-definition.svg")
+ no-repeat;
+ border-color: transparent;
+ stroke: var(--theme-icon-color);
+ -moz-context-properties: stroke;
+ cursor: pointer;
+}
+
+.jump-definition:hover {
+ stroke: var(--theme-icon-checked-color);
+}
+
+.tree-node.focused .jump-definition {
+ stroke: currentColor;
+}
+
+/******************************************************************************/
+/* Invoke getter button */
+
+button.invoke-getter {
+ mask: url(chrome://devtools/content/shared/components/reps/images/input.svg)
+ no-repeat;
+ display: inline-block;
+ background-color: var(--theme-icon-color);
+ height: 10px;
+ vertical-align: middle;
+ border: none;
+}
+
+.invoke-getter:hover {
+ background-color: var(--theme-icon-checked-color);
+}
+
+/******************************************************************************/
+/* "more…" ellipsis */
+.more-ellipsis {
+ color: var(--comment-node-color);
+}
+
+/* function parameters */
+.objectBox-function .param {
+ color: var(--theme-highlight-red);
+}
diff --git a/devtools/client/shared/components/reps/reps/accessible.js b/devtools/client/shared/components/reps/reps/accessible.js
new file mode 100644
index 0000000000..796a850161
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/accessible.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ rep: StringRep,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ /**
+ * Renders Accessible object.
+ */
+
+ Accessible.propTypes = {
+ object: PropTypes.object.isRequired,
+ inspectIconTitle: PropTypes.string,
+ nameMaxLength: PropTypes.number,
+ onAccessibleClick: PropTypes.func,
+ onAccessibleMouseOver: PropTypes.func,
+ onAccessibleMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ roleFirst: PropTypes.bool,
+ separatorText: PropTypes.string,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Accessible(props) {
+ const {
+ object,
+ inspectIconTitle,
+ nameMaxLength,
+ onAccessibleClick,
+ onInspectIconClick,
+ roleFirst,
+ separatorText,
+ } = props;
+
+ const isInTree = object.preview && object.preview.isConnected === true;
+
+ const config = getElementConfig({ ...props, isInTree });
+ const elements = getElements(
+ object,
+ nameMaxLength,
+ roleFirst,
+ separatorText
+ );
+ const inspectIcon = getInspectIcon({
+ object,
+ onInspectIconClick,
+ inspectIconTitle,
+ onAccessibleClick,
+ isInTree,
+ });
+
+ return span(config, ...elements, inspectIcon);
+ }
+
+ // Get React Config Obj
+ function getElementConfig(opts) {
+ const {
+ object,
+ isInTree,
+ onAccessibleClick,
+ onAccessibleMouseOver,
+ onAccessibleMouseOut,
+ shouldRenderTooltip,
+ roleFirst,
+ } = opts;
+ const { name, role } = object.preview;
+
+ // Initiate config
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-accessible",
+ };
+
+ if (isInTree) {
+ if (onAccessibleClick) {
+ Object.assign(config, {
+ onClick: _ => onAccessibleClick(object),
+ className: `${config.className} clickable`,
+ });
+ }
+
+ if (onAccessibleMouseOver) {
+ Object.assign(config, {
+ onMouseOver: _ => onAccessibleMouseOver(object),
+ });
+ }
+
+ if (onAccessibleMouseOut) {
+ Object.assign(config, {
+ onMouseOut: onAccessibleMouseOut,
+ });
+ }
+ }
+
+ // If tooltip, build tooltip
+ if (shouldRenderTooltip) {
+ let tooltip;
+ if (!name) {
+ tooltip = role;
+ } else {
+ const quotedName = `"${name}"`;
+ tooltip = `${roleFirst ? role : quotedName}: ${
+ roleFirst ? quotedName : role
+ }`;
+ }
+
+ config.title = tooltip;
+ }
+
+ // Return config obj
+ return config;
+ }
+
+ // Get Content Elements
+ function getElements(
+ grip,
+ nameMaxLength,
+ roleFirst = false,
+ separatorText = ": "
+ ) {
+ const { name, role } = grip.preview;
+ const elements = [];
+
+ // If there's a `name` value in `grip.preview`, render it with the
+ // StringRep and push element into Elements array
+
+ if (name) {
+ elements.push(
+ StringRep({
+ className: "accessible-name",
+ object: name,
+ cropLimit: nameMaxLength,
+ }),
+ span({ className: "separator" }, separatorText)
+ );
+ }
+
+ elements.push(span({ className: "accessible-role" }, role));
+ return roleFirst ? elements.reverse() : elements;
+ }
+
+ // Get Icon
+ function getInspectIcon(opts) {
+ const {
+ object,
+ onInspectIconClick,
+ inspectIconTitle,
+ onAccessibleClick,
+ isInTree,
+ } = opts;
+
+ if (!isInTree || !onInspectIconClick) {
+ return null;
+ }
+
+ return button({
+ className: "open-accessibility-inspector",
+ title: inspectIconTitle,
+ onClick: e => {
+ if (onAccessibleClick) {
+ e.stopPropagation();
+ }
+
+ onInspectIconClick(object, e);
+ },
+ });
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return (
+ object?.preview && object.typeName && object.typeName === "accessible"
+ );
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Accessible),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/accessor.js b/devtools/client/shared/components/reps/reps/accessor.js
new file mode 100644
index 0000000000..b234d814b3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/accessor.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+
+ Accessor.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Accessor(props) {
+ const {
+ object,
+ evaluation,
+ onInvokeGetterButtonClick,
+ shouldRenderTooltip,
+ } = props;
+
+ if (evaluation) {
+ const {
+ Rep,
+ Grip,
+ } = require("devtools/client/shared/components/reps/reps/rep");
+ return span(
+ {
+ className: "objectBox objectBox-accessor objectTitle",
+ },
+ Rep({
+ ...props,
+ object: evaluation.getterValue,
+ mode: props.mode || MODE.TINY,
+ defaultRep: Grip,
+ })
+ );
+ }
+
+ if (hasGetter(object) && onInvokeGetterButtonClick) {
+ return button({
+ className: "invoke-getter",
+ title: "Invoke getter",
+ onClick: event => {
+ onInvokeGetterButtonClick();
+ event.stopPropagation();
+ },
+ });
+ }
+
+ const accessors = [];
+ if (hasGetter(object)) {
+ accessors.push("Getter");
+ }
+
+ if (hasSetter(object)) {
+ accessors.push("Setter");
+ }
+
+ const accessorsString = accessors.join(" & ");
+
+ return span(
+ {
+ className: "objectBox objectBox-accessor objectTitle",
+ title: shouldRenderTooltip ? accessorsString : null,
+ },
+ accessorsString
+ );
+ }
+
+ function hasGetter(object) {
+ return object && object.get && object.get.type !== "undefined";
+ }
+
+ function hasSetter(object) {
+ return object && object.set && object.set.type !== "undefined";
+ }
+
+ function supportsObject(object) {
+ return hasGetter(object) || hasSetter(object);
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Accessor),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/array.js b/devtools/client/shared/components/reps/reps/array.js
new file mode 100644
index 0000000000..f8797fa4d3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/array.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const ModePropType = PropTypes.oneOf(Object.values(MODE));
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+
+ ArrayRep.propTypes = {
+ mode: ModePropType,
+ object: PropTypes.array.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ArrayRep(props) {
+ const { object, mode = MODE.SHORT, shouldRenderTooltip = true } = props;
+
+ let brackets;
+ let items;
+ const needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+ };
+
+ if (mode === MODE.TINY) {
+ const isEmpty = object.length === 0;
+ if (isEmpty) {
+ items = [];
+ } else {
+ items = [
+ span(
+ {
+ className: "more-ellipsis",
+ },
+ "…"
+ ),
+ ];
+ }
+ brackets = needSpace(false);
+ } else {
+ items = arrayIterator(props, object, maxLengthMap.get(mode));
+ brackets = needSpace(!!items.length);
+ }
+
+ return span(
+ {
+ className: "objectBox objectBox-array",
+ title: shouldRenderTooltip ? "Array" : null,
+ },
+ span(
+ {
+ className: "arrayLeftBracket",
+ },
+ brackets.left
+ ),
+ ...items,
+ span(
+ {
+ className: "arrayRightBracket",
+ },
+ brackets.right
+ )
+ );
+ }
+
+ function arrayIterator(props, array, max) {
+ const items = [];
+
+ for (let i = 0; i < array.length && i < max; i++) {
+ const config = {
+ mode: MODE.TINY,
+ delim: i == array.length - 1 ? "" : ", ",
+ };
+ let item;
+
+ try {
+ item = ItemRep({
+ ...props,
+ ...config,
+ object: array[i],
+ });
+ } catch (exc) {
+ item = ItemRep({
+ ...props,
+ ...config,
+ object: exc,
+ });
+ }
+ items.push(item);
+ }
+
+ if (array.length > max) {
+ items.push(
+ span(
+ {
+ className: "more-ellipsis",
+ },
+ "…"
+ )
+ );
+ }
+
+ return items;
+ }
+
+ /**
+ * Renders array item. Individual values are separated by a comma.
+ */
+
+ ItemRep.propTypes = {
+ object: PropTypes.any.isRequired,
+ delim: PropTypes.string.isRequired,
+ mode: ModePropType,
+ };
+
+ function ItemRep(props) {
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ const { object, delim, mode } = props;
+ return span(
+ {},
+ Rep({
+ ...props,
+ object,
+ mode,
+ }),
+ delim
+ );
+ }
+
+ function getLength(object) {
+ return object.length;
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return (
+ noGrip &&
+ (Array.isArray(object) ||
+ Object.prototype.toString.call(object) === "[object Arguments]")
+ );
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ArrayRep),
+ supportsObject,
+ maxLengthMap,
+ getLength,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/attribute.js b/devtools/client/shared/components/reps/reps/attribute.js
new file mode 100644
index 0000000000..71975fe978
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/attribute.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ rep: StringRep,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ /**
+ * Renders DOM attribute
+ */
+
+ Attribute.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Attribute(props) {
+ const { object, shouldRenderTooltip } = props;
+ const value = object.preview.value;
+ const attrName = getTitle(object);
+
+ const config = getElementConfig({
+ attrName,
+ shouldRenderTooltip,
+ value,
+ object,
+ });
+
+ return span(
+ config,
+ span({ className: "attrName" }, attrName),
+ span({ className: "attrEqual" }, "="),
+ StringRep({ className: "attrValue", object: value })
+ );
+ }
+
+ function getTitle(grip) {
+ return grip.preview.nodeName;
+ }
+
+ function getElementConfig(opts) {
+ const { attrName, shouldRenderTooltip, value, object } = opts;
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox-Attr",
+ title: shouldRenderTooltip ? `${attrName}="${value}"` : null,
+ };
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return getGripType(grip, noGrip) == "Attr" && grip?.preview;
+ }
+
+ module.exports = {
+ rep: wrapRender(Attribute),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/big-int.js b/devtools/client/shared/components/reps/reps/big-int.js
new file mode 100644
index 0000000000..4bb7db507f
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/big-int.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a BigInt Number
+ */
+
+ BigInt.propTypes = {
+ object: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.number,
+ PropTypes.bool,
+ ]).isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function BigInt(props) {
+ const { object, shouldRenderTooltip } = props;
+ const text = object.text;
+ const config = getElementConfig({ text, shouldRenderTooltip });
+
+ return span(config, `${text}n`);
+ }
+
+ function getElementConfig(opts) {
+ const { text, shouldRenderTooltip } = opts;
+
+ return {
+ className: "objectBox objectBox-number",
+ title: shouldRenderTooltip ? `${text}n` : null,
+ };
+ }
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) === "BigInt";
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(BigInt),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/comment-node.js b/devtools/client/shared/components/reps/reps/comment-node.js
new file mode 100644
index 0000000000..c84119a03a
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/comment-node.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ const {
+ cropString,
+ cropMultipleLines,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const nodeConstants = require("devtools/client/shared/components/reps/shared/dom-node-constants");
+
+ /**
+ * Renders DOM comment node.
+ */
+
+ CommentNode.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function CommentNode(props) {
+ const { object, mode = MODE.SHORT, shouldRenderTooltip } = props;
+
+ let { textContent } = object.preview;
+ if (mode === MODE.TINY) {
+ textContent = cropMultipleLines(textContent, 30);
+ } else if (mode === MODE.SHORT) {
+ textContent = cropString(textContent, 50);
+ }
+
+ const config = getElementConfig({
+ object,
+ textContent,
+ shouldRenderTooltip,
+ });
+
+ return span(config, `<!-- ${textContent} -->`);
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip } = opts;
+
+ // Run textContent through cropString to sanitize
+ const uncroppedText = shouldRenderTooltip
+ ? cropString(object.preview.textContent)
+ : null;
+
+ return {
+ className: "objectBox theme-comment",
+ "data-link-actor-id": object.actor,
+ title: shouldRenderTooltip ? `<!-- ${uncroppedText} -->` : null,
+ };
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return object?.preview?.nodeType === nodeConstants.COMMENT_NODE;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(CommentNode),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/constants.js b/devtools/client/shared/components/reps/reps/constants.js
new file mode 100644
index 0000000000..24965c3f5c
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/constants.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ module.exports = {
+ MODE: {
+ TINY: Symbol("TINY"),
+ SHORT: Symbol("SHORT"),
+ LONG: Symbol("LONG"),
+ },
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/custom-formatter.js b/devtools/client/shared/components/reps/reps/custom-formatter.js
new file mode 100644
index 0000000000..469866f790
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/custom-formatter.js
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ // Dependencies
+ const {
+ Component,
+ createElement,
+ createFactory,
+ } = require("devtools/client/shared/vendor/react");
+ const {
+ cleanupStyle,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const flags = require("resource://devtools/shared/flags.js");
+
+ const ALLOWED_TAGS = new Set([
+ "span",
+ "div",
+ "ol",
+ "ul",
+ "li",
+ "table",
+ "tr",
+ "td",
+ ]);
+
+ class CustomFormatter extends Component {
+ static get propTypes() {
+ return {
+ autoExpandDepth: PropTypes.number,
+ client: PropTypes.object,
+ createElement: PropTypes.func,
+ frame: PropTypes.object,
+ front: PropTypes.object,
+ object: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = { open: false };
+ this.toggleBody = this.toggleBody.bind(this);
+ }
+
+ componentDidThrow(e) {
+ console.error("Error in CustomFormatter", e);
+ this.setState(state => ({ ...state, hasError: true }));
+ }
+
+ async toggleBody(evt) {
+ evt.stopPropagation();
+
+ const open = !this.state.open;
+ if (open && !this.state.bodyJsonMl) {
+ let front = this.props.front;
+ if (!front && this.props.client?.createObjectFront) {
+ if (flags.testing && !this.props.frame) {
+ throw new Error("props.frame is mandatory");
+ }
+ front = this.props.client.createObjectFront(
+ this.props.object,
+ this.props.frame
+ );
+ }
+ if (!front) {
+ return;
+ }
+
+ const response = await front.customFormatterBody();
+
+ const bodyJsonMl = renderJsonMl(response.customFormatterBody, {
+ ...this.props,
+ autoExpandDepth: this.props.autoExpandDepth
+ ? this.props.autoExpandDepth - 1
+ : 0,
+ object: null,
+ });
+
+ this.setState(state => ({
+ ...state,
+ bodyJsonMl,
+ open,
+ }));
+ } else {
+ this.setState(state => ({
+ ...state,
+ bodyJsonMl: null,
+ open,
+ }));
+ }
+ }
+
+ render() {
+ if (this.state && this.state.hasError) {
+ return createElement(
+ "span",
+ {
+ className: "objectBox objectBox-failure",
+ title:
+ "This object could not be rendered, " +
+ "please file a bug on bugzilla.mozilla.org",
+ },
+ "Invalid custom formatter object"
+ );
+ }
+
+ const headerJsonMl = renderJsonMl(this.props.object.header, {
+ ...this.props,
+ open: this.state?.open,
+ });
+
+ return createElement(
+ "span",
+ {
+ className: "objectBox-jsonml-wrapper",
+ "data-expandable": this.props.object.hasBody,
+ "aria-expanded": this.state.open,
+ onClick: this.props.object.hasBody ? this.toggleBody : null,
+ },
+ headerJsonMl,
+ this.state.bodyJsonMl
+ ? createElement(
+ "div",
+ { className: "objectBox-jsonml-body-wrapper" },
+ this.state.bodyJsonMl
+ )
+ : null
+ );
+ }
+ }
+
+ function renderJsonMl(jsonMl, props, index = 0) {
+ // The second item of the array can either be an object holding the attributes
+ // for the element or the first child element. Therefore, all array items after the
+ // first one are fetched together and split afterwards if needed.
+ let [tagName, ...attributesAndChildren] = jsonMl ?? [];
+
+ if (!ALLOWED_TAGS.has(tagName)) {
+ tagName = "div";
+ }
+
+ const attributes = attributesAndChildren[0];
+ const hasAttributes =
+ Object(attributes) === attributes && !Array.isArray(attributes);
+ const style =
+ hasAttributes && attributes?.style && props.createElement
+ ? cleanupStyle(attributes.style, props.createElement)
+ : null;
+ const children = attributesAndChildren;
+ if (hasAttributes) {
+ children.shift();
+ }
+
+ const childElements = [];
+
+ if (props.object?.hasBody) {
+ childElements.push(
+ createElement("button", {
+ "aria-expanded": props.open,
+ className: `collapse-button jsonml-header-collapse-button${
+ props.open ? " expanded" : ""
+ }`,
+ })
+ );
+ }
+
+ if (Array.isArray(children)) {
+ children.forEach((child, childIndex) => {
+ let childElement;
+ // If the child is an array, it should be a JsonML item, so use this function to
+ // render them.
+ if (Array.isArray(child)) {
+ childElement = renderJsonMl(
+ child,
+ { ...props, object: null },
+ childIndex
+ );
+ } else if (typeof child === "object" && child !== null) {
+ // If we don't have an array, this means that we're probably dealing with
+ // a front or a grip. If the object has a `getGrip` function, call it to get the
+ // actual grip.
+ const gripOrPrimitive =
+ (child.typeName == "obj" || child.typeName == "string") &&
+ typeof child?.getGrip == "function"
+ ? child.getGrip()
+ : child;
+
+ // If the grip represents an object that was custom formatted, we should render
+ // it using this component.
+ if (supportsObject(gripOrPrimitive)) {
+ childElement = createElement(CustomFormatter, {
+ ...props,
+ object: gripOrPrimitive,
+ front: child && !!child.typeName ? child : null,
+ });
+ } else {
+ // Here we have a non custom-formatted grip, so we let the ObjectInspector
+ // handles it.
+ const {
+ objectInspector,
+ MODE,
+ } = require("devtools/client/shared/components/reps/index");
+ childElement = createElement(objectInspector.ObjectInspector, {
+ ...props,
+ mode: props.mode == MODE.LONG ? MODE.SHORT : MODE.TINY,
+ roots: [
+ {
+ path: `${
+ gripOrPrimitive?.actorID ?? gripOrPrimitive?.actor ?? null
+ }`,
+ contents: {
+ value: gripOrPrimitive,
+ front: child && !!child.typeName ? child : null,
+ },
+ },
+ ],
+ });
+ }
+ } else {
+ // Here we have a primitive. We don't want to use Rep to render them as reps come
+ // with their own styling which might clash with the style defined in the JsonMl.
+ childElement = child;
+ }
+ childElements.push(childElement);
+ });
+ } else {
+ childElements.push(children);
+ }
+
+ return createElement(
+ tagName,
+ {
+ className: `objectBox objectBox-jsonml`,
+ key: `jsonml-${tagName}-${index}`,
+ style,
+ },
+ childElements
+ );
+ }
+
+ function supportsObject(grip) {
+ return grip?.useCustomFormatter === true && Array.isArray(grip?.header);
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: createFactory(CustomFormatter),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/date-time.js b/devtools/client/shared/components/reps/reps/date-time.js
new file mode 100644
index 0000000000..36d15fcbaa
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/date-time.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Used to render JS built-in Date() object.
+ */
+
+ DateTime.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function DateTime(props) {
+ const { object: grip, shouldRenderTooltip } = props;
+ let date;
+ try {
+ const dateObject = new Date(grip.preview.timestamp);
+ // Calling `toISOString` will throw if the date is invalid,
+ // so we can render an `Invalid Date` element.
+ dateObject.toISOString();
+
+ const dateObjectString = dateObject.toString();
+
+ const config = getElementConfig({
+ grip,
+ dateObjectString,
+ shouldRenderTooltip,
+ });
+
+ date = span(
+ config,
+ getTitle(grip),
+ span({ className: "Date" }, dateObjectString)
+ );
+ } catch (e) {
+ date = span(
+ {
+ className: "objectBox",
+ title: shouldRenderTooltip ? "Invalid Date" : null,
+ },
+ "Invalid Date"
+ );
+ }
+
+ return date;
+ }
+
+ function getElementConfig(opts) {
+ const { grip, dateObjectString, shouldRenderTooltip } = opts;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox",
+ title: shouldRenderTooltip ? `${grip.class} ${dateObjectString}` : null,
+ };
+ }
+
+ // getTitle() is used to render the `Date ` before the stringified date object,
+ // not to render the actual span "title".
+
+ function getTitle(grip) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ `${grip.class} `
+ );
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return getGripType(grip, noGrip) == "Date" && grip?.preview;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(DateTime),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/document-type.js b/devtools/client/shared/components/reps/reps/document-type.js
new file mode 100644
index 0000000000..36442858fc
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/document-type.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders DOM documentType object.
+ */
+
+ DocumentType.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function DocumentType(props) {
+ const { object, shouldRenderTooltip } = props;
+ const name =
+ object && object.preview && object.preview.nodeName
+ ? ` ${object.preview.nodeName}`
+ : "";
+
+ const config = getElementConfig({ object, shouldRenderTooltip, name });
+
+ return span(config, `<!DOCTYPE${name}>`);
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip, name } = opts;
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-document",
+ title: shouldRenderTooltip ? `<!DOCTYPE${name}>` : null,
+ };
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return object?.preview && getGripType(object, noGrip) === "DocumentType";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(DocumentType),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/document.js b/devtools/client/shared/components/reps/reps/document.js
new file mode 100644
index 0000000000..1ee4eeb9a7
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/document.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders DOM document object.
+ */
+
+ Document.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Document(props) {
+ const grip = props.object;
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+ const location = getLocation(grip);
+ const config = getElementConfig({ grip, location, shouldRenderTooltip });
+ return span(
+ config,
+ getTitle(grip),
+ location ? span({ className: "location" }, ` ${location}`) : null
+ );
+ }
+
+ function getElementConfig(opts) {
+ const { grip, location, shouldRenderTooltip } = opts;
+ const config = {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox objectBox-document",
+ };
+
+ if (!shouldRenderTooltip || !location) {
+ return config;
+ }
+ config.title = `${grip.class} ${location}`;
+ return config;
+ }
+
+ function getLocation(grip) {
+ const location = grip.preview.location;
+ return location ? getURLDisplayString(location) : null;
+ }
+
+ function getTitle(grip) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ grip.class
+ );
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return object?.preview && getGripType(object, noGrip) === "HTMLDocument";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Document),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/element-node.js b/devtools/client/shared/components/reps/reps/element-node.js
new file mode 100644
index 0000000000..cb7b0e6a32
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/element-node.js
@@ -0,0 +1,310 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ rep: StringRep,
+ isLongString,
+ } = require("devtools/client/shared/components/reps/reps/string");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const nodeConstants = require("devtools/client/shared/components/reps/shared/dom-node-constants");
+
+ const MAX_ATTRIBUTE_LENGTH = 50;
+
+ /**
+ * Renders DOM element node.
+ */
+
+ ElementNode.propTypes = {
+ object: PropTypes.object.isRequired,
+ // The class should be in reps.css
+ inspectIconTitle: PropTypes.oneOf(["open-inspector", "highlight-node"]),
+ inspectIconClassName: PropTypes.string,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeClick: PropTypes.func,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ElementNode(props) {
+ const { object, mode, shouldRenderTooltip } = props;
+
+ const {
+ isAfterPseudoElement,
+ isBeforePseudoElement,
+ isMarkerPseudoElement,
+ } = object.preview;
+
+ let renderElements = [];
+ const isInTree = object.preview && object.preview.isConnected === true;
+ let config = getElementConfig({ ...props, isInTree });
+ const inspectIcon = getInspectIcon({ ...props, isInTree });
+
+ // Elements Case 1: Pseudo Element
+ if (
+ isAfterPseudoElement ||
+ isBeforePseudoElement ||
+ isMarkerPseudoElement
+ ) {
+ const pseudoNodeElement = getPseudoNodeElement(object);
+
+ // Regenerate config if shouldRenderTooltip
+ if (shouldRenderTooltip) {
+ const tooltipString = pseudoNodeElement.content;
+ config = getElementConfig({ ...props, tooltipString, isInTree });
+ }
+
+ // Return ONLY pseudo node element as array[0]
+ renderElements = [
+ span(pseudoNodeElement.config, pseudoNodeElement.content),
+ ];
+ } else if (mode === MODE.TINY) {
+ // Elements Case 2: MODE.TINY
+ const tinyElements = getTinyElements(object);
+
+ // Regenerate config to include tooltip title
+ if (shouldRenderTooltip) {
+ // Reduce for plaintext
+ const tooltipString = tinyElements.reduce(function (acc, cur) {
+ return acc.concat(cur.content);
+ }, "");
+
+ config = getElementConfig({ ...props, tooltipString, isInTree });
+ }
+
+ // Reduce for React elements
+ const tinyElementsRender = tinyElements.reduce(function (acc, cur) {
+ acc.push(span(cur.config, cur.content));
+ return acc;
+ }, []);
+
+ // Render array of React spans
+ renderElements = tinyElementsRender;
+ } else {
+ // Elements Default case
+ renderElements = getElements(props);
+ }
+
+ return span(config, ...renderElements, inspectIcon ? inspectIcon : null);
+ }
+
+ function getElementConfig(opts) {
+ const {
+ object,
+ isInTree,
+ onDOMNodeClick,
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ shouldRenderTooltip,
+ tooltipString,
+ } = opts;
+
+ // Initiate config
+ const config = {
+ "data-link-actor-id": object.actor,
+ "data-link-content-dom-reference": JSON.stringify(
+ object.contentDomReference
+ ),
+ className: "objectBox objectBox-node",
+ };
+
+ // Triage event handlers
+ if (isInTree) {
+ if (onDOMNodeClick) {
+ Object.assign(config, {
+ onClick: _ => onDOMNodeClick(object),
+ className: `${config.className} clickable`,
+ });
+ }
+
+ if (onDOMNodeMouseOver) {
+ Object.assign(config, {
+ onMouseOver: _ => onDOMNodeMouseOver(object),
+ });
+ }
+
+ if (onDOMNodeMouseOut) {
+ Object.assign(config, {
+ onMouseOut: _ => onDOMNodeMouseOut(object),
+ });
+ }
+ }
+
+ // If tooltip, build tooltip
+ if (tooltipString && shouldRenderTooltip) {
+ config.title = tooltipString;
+ }
+
+ // Return config obj
+ return config;
+ }
+
+ function getElements(opts) {
+ const { object: grip } = opts;
+
+ const { attributes, nodeName } = grip.preview;
+
+ const nodeNameElement = span(
+ {
+ className: "tag-name",
+ },
+ nodeName
+ );
+
+ const attributeKeys = Object.keys(attributes);
+ if (attributeKeys.includes("class")) {
+ attributeKeys.splice(attributeKeys.indexOf("class"), 1);
+ attributeKeys.unshift("class");
+ }
+ if (attributeKeys.includes("id")) {
+ attributeKeys.splice(attributeKeys.indexOf("id"), 1);
+ attributeKeys.unshift("id");
+ }
+ const attributeElements = attributeKeys.reduce((arr, name, i, keys) => {
+ const value = attributes[name];
+
+ let title = isLongString(value) ? value.initial : value;
+ if (title.length < MAX_ATTRIBUTE_LENGTH) {
+ title = null;
+ }
+
+ const attribute = span(
+ {},
+ span({ className: "attrName" }, name),
+ span({ className: "attrEqual" }, "="),
+ StringRep({
+ className: "attrValue",
+ object: value,
+ cropLimit: MAX_ATTRIBUTE_LENGTH,
+ title,
+ })
+ );
+
+ return arr.concat([" ", attribute]);
+ }, []);
+
+ return [
+ span({ className: "angleBracket" }, "<"),
+ nodeNameElement,
+ ...attributeElements,
+ span({ className: "angleBracket" }, ">"),
+ ];
+ }
+
+ function getTinyElements(grip) {
+ const { attributes, nodeName } = grip.preview;
+
+ // Initialize elements array
+ const elements = [
+ {
+ config: { className: "tag-name" },
+ content: nodeName,
+ },
+ ];
+
+ // Push ID element
+ if (attributes.id) {
+ elements.push({
+ config: { className: "attrName" },
+ content: `#${attributes.id}`,
+ });
+ }
+
+ // Push Classes
+ if (attributes.class) {
+ const elementClasses = attributes.class
+ .trim()
+ .split(/\s+/)
+ .map(cls => `.${cls}`)
+ .join("");
+ elements.push({
+ config: { className: "attrName" },
+ content: elementClasses,
+ });
+ }
+
+ return elements;
+ }
+
+ function getPseudoNodeElement(grip) {
+ const {
+ isAfterPseudoElement,
+ isBeforePseudoElement,
+ isMarkerPseudoElement,
+ } = grip.preview;
+
+ let pseudoNodeName;
+
+ if (isAfterPseudoElement) {
+ pseudoNodeName = "after";
+ } else if (isBeforePseudoElement) {
+ pseudoNodeName = "before";
+ } else if (isMarkerPseudoElement) {
+ pseudoNodeName = "marker";
+ }
+
+ return {
+ config: { className: "attrName" },
+ content: `::${pseudoNodeName}`,
+ };
+ }
+
+ function getInspectIcon(opts) {
+ const {
+ object,
+ isInTree,
+ onInspectIconClick,
+ inspectIconTitle,
+ inspectIconClassName,
+ onDOMNodeClick,
+ } = opts;
+
+ if (!isInTree || !onInspectIconClick) {
+ return null;
+ }
+
+ return button({
+ className: inspectIconClassName || "open-inspector",
+ // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
+ title: inspectIconTitle || "Click to select the node in the inspector",
+ onClick: e => {
+ if (onDOMNodeClick) {
+ e.stopPropagation();
+ }
+
+ onInspectIconClick(object, e);
+ },
+ });
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return object?.preview?.nodeType === nodeConstants.ELEMENT_NODE;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ElementNode),
+ supportsObject,
+ MAX_ATTRIBUTE_LENGTH,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/error.js b/devtools/client/shared/components/reps/reps/error.js
new file mode 100644
index 0000000000..9f6786adf6
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/error.js
@@ -0,0 +1,330 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ div,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ cleanFunctionName,
+ } = require("devtools/client/shared/components/reps/reps/function");
+ const {
+ isLongString,
+ } = require("devtools/client/shared/components/reps/reps/string");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+ /**
+ * Renders Error objects.
+ */
+ ErrorRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ // An optional function that will be used to render the Error stacktrace.
+ renderStacktrace: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ /**
+ * Render an Error object.
+ * The customFormat prop allows to print a simplified view of the object, with only the
+ * message and the stacktrace, e.g.:
+ * Error: "blah"
+ * <anonymous> debugger eval code:1
+ *
+ * The customFormat prop will only be taken into account if the mode isn't tiny and the
+ * depth is 0. This is because we don't want error in previews or in object to be
+ * displayed unlike other objects:
+ * - Object { err: Error }
+ * - â–¼ {
+ * err: Error: "blah"
+ * }
+ */
+ function ErrorRep(props) {
+ const { object, mode, shouldRenderTooltip, depth } = props;
+ const preview = object.preview;
+ const customFormat = props.customFormat && mode !== MODE.TINY && !depth;
+
+ let name;
+ if (
+ preview &&
+ preview.name &&
+ typeof preview.name === "string" &&
+ preview.kind
+ ) {
+ switch (preview.kind) {
+ case "Error":
+ name = preview.name;
+ break;
+ case "DOMException":
+ name = preview.kind;
+ break;
+ default:
+ throw new Error("Unknown preview kind for the Error rep.");
+ }
+ } else {
+ name = "Error";
+ }
+
+ const errorTitle = mode === MODE.TINY ? name : `${name}: `;
+ const content = [];
+
+ if (customFormat) {
+ content.push(errorTitle);
+ } else {
+ content.push(
+ span({ className: "objectTitle", key: "title" }, errorTitle)
+ );
+ }
+
+ if (mode !== MODE.TINY) {
+ const {
+ Rep,
+ } = require("devtools/client/shared/components/reps/reps/rep");
+ content.push(
+ Rep({
+ ...props,
+ key: "message",
+ object: preview.message,
+ mode: props.mode || MODE.TINY,
+ useQuotes: false,
+ })
+ );
+ }
+ const renderStack = preview.stack && customFormat;
+ if (renderStack) {
+ const stacktrace = props.renderStacktrace
+ ? props.renderStacktrace(parseStackString(preview.stack))
+ : getStacktraceElements(props, preview);
+ content.push(stacktrace);
+ }
+
+ const renderCause = customFormat && preview.hasOwnProperty("cause");
+ if (renderCause) {
+ content.push(getCauseElement(props, preview));
+ }
+
+ return span(
+ {
+ "data-link-actor-id": object.actor,
+ className: `objectBox-stackTrace ${
+ customFormat ? "reps-custom-format" : ""
+ }`,
+ title: shouldRenderTooltip ? `${name}: "${preview.message}"` : null,
+ },
+ ...content
+ );
+ }
+
+ /**
+ * Returns a React element reprensenting the Error stacktrace, i.e.
+ * transform error.stack from:
+ *
+ * semicolon@debugger eval code:1:109
+ * jkl@debugger eval code:1:63
+ * asdf@debugger eval code:1:28
+ * @debugger eval code:1:227
+ *
+ * Into a column layout:
+ *
+ * semicolon (<anonymous>:8:10)
+ * jkl (<anonymous>:5:10)
+ * asdf (<anonymous>:2:10)
+ * (<anonymous>:11:1)
+ */
+ function getStacktraceElements(props, preview) {
+ const stack = [];
+ if (!preview.stack) {
+ return stack;
+ }
+
+ parseStackString(preview.stack).forEach((frame, index, frames) => {
+ let onLocationClick;
+ const { filename, lineNumber, columnNumber, functionName, location } =
+ frame;
+
+ if (
+ props.onViewSourceInDebugger &&
+ !IGNORED_SOURCE_URLS.includes(filename)
+ ) {
+ onLocationClick = e => {
+ // Don't trigger ObjectInspector expand/collapse.
+ e.stopPropagation();
+ props.onViewSourceInDebugger({
+ url: filename,
+ line: lineNumber,
+ column: columnNumber,
+ });
+ };
+ }
+
+ stack.push(
+ "\t",
+ span(
+ {
+ key: `fn${index}`,
+ className: "objectBox-stackTrace-fn",
+ },
+ cleanFunctionName(functionName)
+ ),
+ " ",
+ span(
+ {
+ key: `location${index}`,
+ className: "objectBox-stackTrace-location",
+ onClick: onLocationClick,
+ title: onLocationClick
+ ? `View source in debugger → ${location}`
+ : undefined,
+ },
+ location
+ ),
+ "\n"
+ );
+ });
+
+ return span(
+ {
+ key: "stack",
+ className: "objectBox-stackTrace-grid",
+ },
+ stack
+ );
+ }
+
+ /**
+ * Returns a React element representing the cause of the Error i.e. the `cause`
+ * property in the second parameter of the Error constructor (`new Error("message", { cause })`)
+ *
+ * Example:
+ * Caused by: Error: original error
+ */
+ function getCauseElement(props, preview) {
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+ return div(
+ {
+ key: "cause-container",
+ className: "error-rep-cause",
+ },
+ "Caused by: ",
+ Rep({
+ ...props,
+ key: "cause",
+ object: preview.cause,
+ mode: props.mode || MODE.TINY,
+ })
+ );
+ }
+
+ /**
+ * Parse a string that should represent a stack trace and returns an array of
+ * the frames. The shape of the frames are extremely important as they can then
+ * be processed here or in the toolbox by other components.
+ * @param {String} stack
+ * @returns {Array} Array of frames, which are object with the following shape:
+ * - {String} filename
+ * - {String} functionName
+ * - {String} location
+ * - {Number} columnNumber
+ * - {Number} lineNumber
+ */
+ function parseStackString(stack) {
+ if (!stack) {
+ return [];
+ }
+
+ const isStacktraceALongString = isLongString(stack);
+ const stackString = isStacktraceALongString ? stack.initial : stack;
+
+ if (typeof stackString !== "string") {
+ return [];
+ }
+
+ const res = [];
+ stackString.split("\n").forEach((frame, index, frames) => {
+ if (!frame) {
+ // Skip any blank lines
+ return;
+ }
+
+ // If the stacktrace is a longString, don't include the last frame in the
+ // array, since it is certainly incomplete.
+ // Can be removed when https://bugzilla.mozilla.org/show_bug.cgi?id=1448833
+ // is fixed.
+ if (isStacktraceALongString && index === frames.length - 1) {
+ return;
+ }
+
+ let functionName;
+ let location;
+
+ // Retrieve the index of the first @ to split the frame string.
+ const atCharIndex = frame.indexOf("@");
+ if (atCharIndex > -1) {
+ functionName = frame.slice(0, atCharIndex);
+ location = frame.slice(atCharIndex + 1);
+ }
+
+ if (location && location.includes(" -> ")) {
+ // If the resource was loaded by base-loader.sys.mjs, the location looks like:
+ // resource://devtools/shared/base-loader.sys.mjs -> resource://path/to/file.js .
+ // What's needed is only the last part after " -> ".
+ location = location.split(" -> ").pop();
+ }
+
+ if (!functionName) {
+ functionName = "<anonymous>";
+ }
+
+ // Given the input: "scriptLocation:2:100"
+ // Result:
+ // ["scriptLocation:2:100", "scriptLocation", "2", "100"]
+ const locationParts = location
+ ? location.match(/^(.*):(\d+):(\d+)$/)
+ : null;
+
+ if (location && locationParts) {
+ const [, filename, line, column] = locationParts;
+ res.push({
+ filename,
+ functionName,
+ location,
+ columnNumber: Number(column),
+ lineNumber: Number(line),
+ });
+ }
+ });
+
+ return res;
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return (
+ object?.isError ||
+ object?.class === "DOMException" ||
+ object?.class === "Exception"
+ );
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ErrorRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/event.js b/devtools/client/shared/components/reps/reps/event.js
new file mode 100644
index 0000000000..460f7e8c31
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/event.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ // Reps
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const { rep } = require("devtools/client/shared/components/reps/reps/grip");
+
+ /**
+ * Renders DOM event objects.
+ */
+ Event.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ };
+
+ function Event(props) {
+ const gripProps = {
+ ...props,
+ title: getTitle(props),
+ object: {
+ ...props.object,
+ preview: {
+ ...props.object.preview,
+ ownProperties: {},
+ },
+ },
+ };
+
+ if (gripProps.object.preview.target) {
+ Object.assign(gripProps.object.preview.ownProperties, {
+ target: gripProps.object.preview.target,
+ });
+ }
+ Object.assign(
+ gripProps.object.preview.ownProperties,
+ gripProps.object.preview.properties
+ );
+
+ delete gripProps.object.preview.properties;
+ gripProps.object.ownPropertyLength = Object.keys(
+ gripProps.object.preview.ownProperties
+ ).length;
+
+ switch (gripProps.object.class) {
+ case "MouseEvent":
+ gripProps.isInterestingProp = (type, value, name) => {
+ return ["target", "clientX", "clientY", "layerX", "layerY"].includes(
+ name
+ );
+ };
+ break;
+ case "KeyboardEvent":
+ gripProps.isInterestingProp = (type, value, name) => {
+ return ["target", "key", "charCode", "keyCode"].includes(name);
+ };
+ break;
+ case "MessageEvent":
+ gripProps.isInterestingProp = (type, value, name) => {
+ return ["target", "isTrusted", "data"].includes(name);
+ };
+ break;
+ default:
+ gripProps.isInterestingProp = (type, value, name) => {
+ // We want to show the properties in the order they are declared.
+ return Object.keys(gripProps.object.preview.ownProperties).includes(
+ name
+ );
+ };
+ }
+
+ return rep(gripProps);
+ }
+
+ function getTitle(props) {
+ const preview = props.object.preview;
+ let title = preview.type;
+
+ if (
+ preview.eventKind == "key" &&
+ preview.modifiers &&
+ preview.modifiers.length
+ ) {
+ title = `${title} ${preview.modifiers.join("-")}`;
+ }
+ return title;
+ }
+
+ // Registration
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "DOMEvent";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Event),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/function.js b/devtools/client/shared/components/reps/reps/function.js
new file mode 100644
index 0000000000..54d8905c20
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/function.js
@@ -0,0 +1,264 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ cropString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+ /**
+ * This component represents a template for Function objects.
+ */
+
+ FunctionRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ onViewSourceInDebugger: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function FunctionRep(props) {
+ const {
+ object: grip,
+ onViewSourceInDebugger,
+ recordTelemetryEvent,
+ shouldRenderTooltip,
+ } = props;
+
+ let jumpToDefinitionButton;
+
+ // Test to see if we should display the link back to the original function definition
+ if (
+ onViewSourceInDebugger &&
+ grip.location &&
+ grip.location.url &&
+ !IGNORED_SOURCE_URLS.includes(grip.location.url)
+ ) {
+ jumpToDefinitionButton = button({
+ className: "jump-definition",
+ draggable: false,
+ title: "Jump to definition",
+ onClick: async e => {
+ // Stop the event propagation so we don't trigger ObjectInspector
+ // expand/collapse.
+ e.stopPropagation();
+ if (recordTelemetryEvent) {
+ recordTelemetryEvent("jump_to_definition");
+ }
+
+ onViewSourceInDebugger(grip.location);
+ },
+ });
+ }
+
+ const elProps = {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox objectBox-function",
+ // Set dir="ltr" to prevent parentheses from
+ // appearing in the wrong direction
+ dir: "ltr",
+ };
+
+ const parameterNames = (grip.parameterNames || []).filter(Boolean);
+ const fnTitle = getFunctionTitle(grip, props);
+ const fnName = getFunctionName(grip, props);
+
+ if (grip.isClassConstructor) {
+ const classTitle = getClassTitle(grip, props);
+ const classBodyTooltip = getClassBody(parameterNames, true, props);
+ const classTooltip = `${classTitle ? classTitle.props.children : ""}${
+ fnName ? fnName : ""
+ }${classBodyTooltip.join("")}`;
+
+ elProps.title = shouldRenderTooltip ? classTooltip : null;
+
+ return span(
+ elProps,
+ classTitle,
+ fnName,
+ ...getClassBody(parameterNames, false, props),
+ jumpToDefinitionButton
+ );
+ }
+
+ const fnTooltip = `${fnTitle ? fnTitle.props.children : ""}${
+ fnName ? fnName : ""
+ }(${parameterNames.join(", ")})`;
+
+ elProps.title = shouldRenderTooltip ? fnTooltip : null;
+
+ const returnSpan = span(
+ elProps,
+ fnTitle,
+ fnName,
+ "(",
+ ...getParams(parameterNames),
+ ")",
+ jumpToDefinitionButton
+ );
+
+ return returnSpan;
+ }
+
+ function getClassTitle(grip) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ "class "
+ );
+ }
+
+ function getFunctionTitle(grip, props) {
+ const { mode } = props;
+
+ if (mode === MODE.TINY && !grip.isGenerator && !grip.isAsync) {
+ return null;
+ }
+
+ let title = mode === MODE.TINY ? "" : "function ";
+
+ if (grip.isGenerator) {
+ title = mode === MODE.TINY ? "* " : "function* ";
+ }
+
+ if (grip.isAsync) {
+ title = `${"async" + " "}${title}`;
+ }
+
+ return span(
+ {
+ className: "objectTitle",
+ },
+ title
+ );
+ }
+
+ /**
+ * Returns a ReactElement representing the function name.
+ *
+ * @param {Object} grip : Function grip
+ * @param {Object} props: Function rep props
+ */
+ function getFunctionName(grip, props = {}) {
+ let { functionName } = props;
+ let name;
+
+ if (functionName) {
+ const end = functionName.length - 1;
+ functionName =
+ functionName.startsWith('"') && functionName.endsWith('"')
+ ? functionName.substring(1, end)
+ : functionName;
+ }
+
+ if (
+ grip.displayName != undefined &&
+ functionName != undefined &&
+ grip.displayName != functionName
+ ) {
+ name = `${functionName}:${grip.displayName}`;
+ } else {
+ name = cleanFunctionName(
+ grip.userDisplayName ||
+ grip.displayName ||
+ grip.name ||
+ props.functionName ||
+ ""
+ );
+ }
+
+ return cropString(name, 100);
+ }
+
+ const objectProperty = /([\w\d\$]+)$/;
+ const arrayProperty = /\[(.*?)\]$/;
+ const functionProperty = /([\w\d]+)[\/\.<]*?$/;
+ const annonymousProperty = /([\w\d]+)\(\^\)$/;
+
+ /**
+ * Decodes an anonymous naming scheme that
+ * spider monkey implements based on "Naming Anonymous JavaScript Functions"
+ * http://johnjbarton.github.io/nonymous/index.html
+ *
+ * @param {String} name : Function name to clean up
+ * @returns String
+ */
+ function cleanFunctionName(name) {
+ for (const reg of [
+ objectProperty,
+ arrayProperty,
+ functionProperty,
+ annonymousProperty,
+ ]) {
+ const match = reg.exec(name);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return name;
+ }
+
+ function getClassBody(constructorParams, textOnly = false, props) {
+ const { mode } = props;
+
+ if (mode === MODE.TINY) {
+ return [];
+ }
+
+ return [" {", ...getClassConstructor(textOnly, constructorParams), "}"];
+ }
+
+ function getClassConstructor(textOnly = false, parameterNames) {
+ if (parameterNames.length === 0) {
+ return [];
+ }
+
+ if (textOnly) {
+ return [` constructor(${parameterNames.join(", ")}) `];
+ }
+ return [" constructor(", ...getParams(parameterNames), ") "];
+ }
+
+ function getParams(parameterNames) {
+ return parameterNames.flatMap((param, index, arr) => {
+ return [
+ span({ className: "param" }, param),
+ index === arr.length - 1 ? "" : span({ className: "delimiter" }, ", "),
+ ];
+ });
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return getGripType(grip, noGrip) === "Function";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(FunctionRep),
+ supportsObject,
+ cleanFunctionName,
+ // exported for testing purpose.
+ getFunctionName,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip-array.js b/devtools/client/shared/components/reps/reps/grip-array.js
new file mode 100644
index 0000000000..23589fc90f
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip-array.js
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ lengthBubble,
+ } = require("devtools/client/shared/components/reps/shared/grip-length-bubble");
+ const {
+ interleave,
+ getGripType,
+ wrapRender,
+ ellipsisElement,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const DEFAULT_TITLE = "Array";
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+
+ GripArray.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ provider: PropTypes.object,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function GripArray(props) {
+ const { object, mode = MODE.SHORT, shouldRenderTooltip } = props;
+
+ let brackets;
+ const needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+ };
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-array",
+ title: shouldRenderTooltip ? "Array" : null,
+ };
+
+ const title = getTitle(props, object);
+
+ if (mode === MODE.TINY) {
+ const isEmpty = getLength(object) === 0;
+
+ // Omit bracketed ellipsis for non-empty non-Array arraylikes (f.e: Sets).
+ if (!isEmpty && object.class !== "Array") {
+ return span(config, title);
+ }
+
+ brackets = needSpace(false);
+ return span(
+ config,
+ title,
+ span(
+ {
+ className: "arrayLeftBracket",
+ },
+ brackets.left
+ ),
+ isEmpty ? null : ellipsisElement,
+ span(
+ {
+ className: "arrayRightBracket",
+ },
+ brackets.right
+ )
+ );
+ }
+
+ const max = maxLengthMap.get(mode);
+ const items = arrayIterator(props, object, max);
+ brackets = needSpace(!!items.length);
+
+ return span(
+ config,
+ title,
+ span(
+ {
+ className: "arrayLeftBracket",
+ },
+ brackets.left
+ ),
+ ...interleave(items, ", "),
+ span(
+ {
+ className: "arrayRightBracket",
+ },
+ brackets.right
+ ),
+ span({
+ className: "arrayProperties",
+ role: "group",
+ })
+ );
+ }
+
+ function getLength(grip) {
+ if (!grip.preview) {
+ return 0;
+ }
+
+ return grip.preview.length || grip.preview.childNodesLength || 0;
+ }
+
+ function getTitle(props, object) {
+ const objectLength = getLength(object);
+ const isEmpty = objectLength === 0;
+
+ let title = props.title || object.class || DEFAULT_TITLE;
+
+ const length = lengthBubble({
+ object,
+ mode: props.mode,
+ maxLengthMap,
+ getLength,
+ });
+
+ if (props.mode === MODE.TINY) {
+ if (isEmpty) {
+ if (object.class === DEFAULT_TITLE) {
+ return null;
+ }
+
+ return span({ className: "objectTitle" }, `${title} `);
+ }
+
+ let trailingSpace;
+ if (object.class === DEFAULT_TITLE) {
+ title = null;
+ trailingSpace = " ";
+ }
+
+ return span({ className: "objectTitle" }, title, length, trailingSpace);
+ }
+
+ return span({ className: "objectTitle" }, title, length, " ");
+ }
+
+ function getPreviewItems(grip) {
+ if (!grip.preview) {
+ return null;
+ }
+
+ return grip.preview.items || grip.preview.childNodes || [];
+ }
+
+ function arrayIterator(props, grip, max) {
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ let items = [];
+ const gripLength = getLength(grip);
+
+ if (!gripLength) {
+ return items;
+ }
+
+ const previewItems = getPreviewItems(grip);
+ const provider = props.provider;
+
+ let emptySlots = 0;
+ let foldedEmptySlots = 0;
+ items = previewItems.reduce((res, itemGrip) => {
+ if (res.length >= max) {
+ return res;
+ }
+
+ let object;
+ try {
+ if (!provider && itemGrip === null) {
+ emptySlots++;
+ return res;
+ }
+
+ object = provider ? provider.getValue(itemGrip) : itemGrip;
+ } catch (exc) {
+ object = exc;
+ }
+
+ if (emptySlots > 0) {
+ res.push(getEmptySlotsElement(emptySlots));
+ foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
+ emptySlots = 0;
+ }
+
+ if (res.length < max) {
+ res.push(
+ Rep({
+ ...props,
+ object,
+ mode: MODE.TINY,
+ // Do not propagate title to array items reps
+ title: undefined,
+ })
+ );
+ }
+
+ return res;
+ }, []);
+
+ // Handle trailing empty slots if there are some.
+ if (items.length < max && emptySlots > 0) {
+ items.push(getEmptySlotsElement(emptySlots));
+ foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
+ }
+
+ const itemsShown = items.length + foldedEmptySlots;
+ if (gripLength > itemsShown) {
+ items.push(ellipsisElement);
+ }
+
+ return items;
+ }
+
+ function getEmptySlotsElement(number) {
+ // TODO: Use l10N - See https://github.com/firefox-devtools/reps/issues/141
+ return `<${number} empty slot${number > 1 ? "s" : ""}>`;
+ }
+
+ function supportsObject(grip, noGrip = false) {
+ return (
+ grip?.preview &&
+ (grip.preview.kind == "ArrayLike" ||
+ getGripType(grip, noGrip) === "DocumentFragment")
+ );
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(GripArray),
+ supportsObject,
+ maxLengthMap,
+ getLength,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip-entry.js b/devtools/client/shared/components/reps/reps/grip-entry.js
new file mode 100644
index 0000000000..8bc5585c09
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip-entry.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders an entry of a Map, (Local|Session)Storage, Header or FormData entry.
+ */
+ GripEntry.propTypes = {
+ object: PropTypes.object,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ };
+
+ function GripEntry(props) {
+ const { object } = props;
+
+ let { key, value } = object.preview;
+ if (key && key.getGrip) {
+ key = key.getGrip();
+ }
+ if (value && value.getGrip) {
+ value = value.getGrip();
+ }
+
+ return span(
+ {
+ className: "objectBox objectBox-map-entry",
+ },
+ PropRep({
+ ...props,
+ name: key,
+ object: value,
+ equal: " \u2192 ",
+ title: null,
+ suppressQuotes: false,
+ })
+ );
+ }
+
+ function supportsObject(grip, noGrip = false) {
+ if (noGrip === true) {
+ return false;
+ }
+ return (
+ grip &&
+ (grip.type === "mapEntry" ||
+ grip.type === "storageEntry" ||
+ grip.type === "formDataEntry" ||
+ grip.type === "urlSearchParamsEntry") &&
+ grip.preview
+ );
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(GripEntry),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip-map.js b/devtools/client/shared/components/reps/reps/grip-map.js
new file mode 100644
index 0000000000..d0157137ef
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip-map.js
@@ -0,0 +1,234 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ lengthBubble,
+ } = require("devtools/client/shared/components/reps/shared/grip-length-bubble");
+ const {
+ interleave,
+ wrapRender,
+ ellipsisElement,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+
+ GripMap.propTypes = {
+ object: PropTypes.object,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ isInterestingEntry: PropTypes.func,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ title: PropTypes.string,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function GripMap(props) {
+ const { mode, object, shouldRenderTooltip } = props;
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? getTooltip(object, props) : null,
+ };
+
+ const title = getTitle(props, object);
+ const isEmpty = getLength(object) === 0;
+
+ if (isEmpty || mode === MODE.TINY) {
+ return span(config, title);
+ }
+
+ const propsArray = safeEntriesIterator(
+ props,
+ object,
+ maxLengthMap.get(mode)
+ );
+
+ return span(
+ config,
+ title,
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ " { "
+ ),
+ ...interleave(propsArray, ", "),
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ " }"
+ )
+ );
+ }
+
+ function getTitle(props, object) {
+ const title =
+ props.title || (object && object.class ? object.class : "Map");
+ return span(
+ {
+ className: "objectTitle",
+ },
+ title,
+ lengthBubble({
+ object,
+ mode: props.mode,
+ maxLengthMap,
+ getLength,
+ showZeroLength: true,
+ })
+ );
+ }
+
+ function getTooltip(object, props) {
+ const tooltip =
+ props.title || (object && object.class ? object.class : "Map");
+ return `${tooltip}(${getLength(object)})`;
+ }
+
+ function safeEntriesIterator(props, object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return entriesIterator(props, object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ }
+
+ function entriesIterator(props, object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ const isInterestingEntry =
+ props.isInterestingEntry ||
+ ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && !!value.length)
+ );
+ });
+
+ const mapEntries =
+ object.preview && object.preview.entries ? object.preview.entries : [];
+
+ let indexes = getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(
+ getEntriesIndexes(
+ mapEntries,
+ max - indexes.length,
+ (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ }
+ )
+ );
+ }
+
+ const entries = getEntries(props, mapEntries, indexes);
+ if (entries.length < getLength(object)) {
+ // There are some undisplayed entries. Then display "…".
+ entries.push(ellipsisElement);
+ }
+
+ return entries;
+ }
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Object} props Component props.
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ function getEntries(props, entries, indexes) {
+ const { onDOMNodeMouseOver, onDOMNodeMouseOut, onInspectIconClick } = props;
+
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ const [key, entryValue] = entries[index];
+ const value =
+ entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ name: key && key.getGrip ? key.getGrip() : key,
+ equal: " \u2192 ",
+ object: value && value.getGrip ? value.getGrip() : value,
+ mode: MODE.TINY,
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ onInspectIconClick,
+ });
+ });
+ }
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ function getEntriesIndexes(entries, max, filter) {
+ return entries.reduce((indexes, [key, entry], i) => {
+ if (indexes.length < max) {
+ const value = entry && entry.value !== undefined ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ const type = (
+ value && value.class ? value.class : typeof value
+ ).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ }
+
+ function getLength(grip) {
+ return grip.preview.size || 0;
+ }
+
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "MapLike";
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(GripMap),
+ supportsObject,
+ maxLengthMap,
+ getLength,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip.js b/devtools/client/shared/components/reps/reps/grip.js
new file mode 100644
index 0000000000..604496d376
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip.js
@@ -0,0 +1,396 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Dependencies
+ const {
+ interleave,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders generic grip. Grip is client representation
+ * of remote JS object and is used as an input object
+ * for this rep component.
+ */
+
+ GripRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ isInterestingProp: PropTypes.func,
+ title: PropTypes.string,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ noGrip: PropTypes.bool,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ const DEFAULT_TITLE = "Object";
+
+ function GripRep(props) {
+ const { mode = MODE.SHORT, object, shouldRenderTooltip } = props;
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-object",
+ };
+
+ if (mode === MODE.TINY) {
+ const propertiesLength = getPropertiesLength(object);
+
+ const tinyModeItems = [];
+ if (getTitle(props, object) !== DEFAULT_TITLE) {
+ tinyModeItems.push(getTitleElement(props, object));
+ } else {
+ tinyModeItems.push(
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ "{"
+ ),
+ propertiesLength > 0
+ ? span(
+ {
+ key: "more",
+ className: "more-ellipsis",
+ },
+ "…"
+ )
+ : null,
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ "}"
+ )
+ );
+ }
+
+ config.title = shouldRenderTooltip ? getTitle(props, object) : null;
+
+ return span(config, ...tinyModeItems);
+ }
+
+ const propsArray = safePropIterator(props, object, maxLengthMap.get(mode));
+
+ config.title = shouldRenderTooltip ? getTitle(props, object) : null;
+
+ return span(
+ config,
+ getTitleElement(props, object),
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ " { "
+ ),
+ ...interleave(propsArray, ", "),
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ " }"
+ )
+ );
+ }
+
+ function getTitleElement(props, object) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ getTitle(props, object)
+ );
+ }
+
+ function getTitle(props, object) {
+ return props.title || object.class || DEFAULT_TITLE;
+ }
+
+ function getPropertiesLength(object) {
+ let propertiesLength =
+ object.preview && object.preview.ownPropertiesLength
+ ? object.preview.ownPropertiesLength
+ : object.ownPropertyLength;
+
+ if (object.preview && object.preview.safeGetterValues) {
+ propertiesLength += Object.keys(object.preview.safeGetterValues).length;
+ }
+
+ if (object.preview && object.preview.ownSymbols) {
+ propertiesLength += object.preview.ownSymbolsLength;
+ }
+
+ if (object.preview && object.preview.privateProperties) {
+ propertiesLength += object.preview.privatePropertiesLength;
+ }
+
+ return propertiesLength;
+ }
+
+ function safePropIterator(props, object, max) {
+ max = typeof max === "undefined" ? maxLengthMap.get(MODE.SHORT) : max;
+ try {
+ return propIterator(props, object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ }
+
+ function propIterator(props, object, max) {
+ if (
+ object.preview &&
+ Object.keys(object.preview).includes("wrappedValue")
+ ) {
+ const {
+ Rep,
+ } = require("devtools/client/shared/components/reps/reps/rep");
+
+ return [
+ Rep({
+ object: object.preview.wrappedValue,
+ mode: props.mode || MODE.TINY,
+ defaultRep: Grip,
+ }),
+ ];
+ }
+
+ // Property filter. Show only interesting properties to the user.
+ const isInterestingProp =
+ props.isInterestingProp ||
+ ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && !!value.length)
+ );
+ });
+
+ let properties = object.preview ? object.preview.ownProperties || {} : {};
+
+ const propertiesLength = getPropertiesLength(object);
+
+ if (object.preview && object.preview.safeGetterValues) {
+ properties = { ...properties, ...object.preview.safeGetterValues };
+ }
+
+ let indexes = getPropIndexes(properties, max, isInterestingProp);
+ if (indexes.length < max && indexes.length < propertiesLength) {
+ // There are not enough props yet.
+ // Then add uninteresting props to display them.
+ indexes = indexes.concat(
+ getPropIndexes(properties, max - indexes.length, (t, value, name) => {
+ return !isInterestingProp(t, value, name);
+ })
+ );
+ }
+
+ // The server synthesizes some property names for a Proxy, like
+ // <target> and <handler>; we don't want to quote these because,
+ // as synthetic properties, they appear more natural when
+ // unquoted. Analogous for a Promise.
+ const suppressQuotes = ["Proxy", "Promise"].includes(object.class);
+ const propsArray = getProps(props, properties, indexes, suppressQuotes);
+
+ // Show private properties
+ if (object.preview && object.preview.privateProperties) {
+ const { privateProperties } = object.preview;
+ const length = max - indexes.length;
+
+ const privateProps = privateProperties.slice(0, length).map(item => {
+ const value = item.descriptor.value;
+ const grip = value && value.getGrip ? value.getGrip() : value;
+
+ return PropRep({
+ ...props,
+ keyClassName: "private",
+ mode: MODE.TINY,
+ name: item.name,
+ object: grip,
+ equal: ": ",
+ defaultRep: Grip,
+ title: null,
+ suppressQuotes: true,
+ });
+ });
+
+ propsArray.push(...privateProps);
+ }
+
+ // Show symbols.
+ if (object.preview && object.preview.ownSymbols) {
+ const { ownSymbols } = object.preview;
+ const length = max - indexes.length;
+
+ const symbolsProps = ownSymbols.slice(0, length).map(symbolItem => {
+ const symbolValue = symbolItem.descriptor.value;
+ const symbolGrip =
+ symbolValue && symbolValue.getGrip
+ ? symbolValue.getGrip()
+ : symbolValue;
+
+ return PropRep({
+ ...props,
+ mode: MODE.TINY,
+ name: symbolItem,
+ object: symbolGrip,
+ equal: ": ",
+ defaultRep: Grip,
+ title: null,
+ suppressQuotes,
+ });
+ });
+
+ propsArray.push(...symbolsProps);
+ }
+
+ if (
+ Object.keys(properties).length > max ||
+ propertiesLength > max ||
+ // When the object has non-enumerable properties, we don't have them in the
+ // packet, but we might want to show there's something in the object.
+ propertiesLength > propsArray.length
+ ) {
+ // There are some undisplayed props. Then display "more...".
+ propsArray.push(
+ span(
+ {
+ key: "more",
+ className: "more-ellipsis",
+ },
+ "…"
+ )
+ );
+ }
+
+ return propsArray;
+ }
+
+ /**
+ * Get props ordered by index.
+ *
+ * @param {Object} componentProps Grip Component props.
+ * @param {Object} properties Properties of the object the Grip describes.
+ * @param {Array} indexes Indexes of properties.
+ * @param {Boolean} suppressQuotes true if we should suppress quotes
+ * on property names.
+ * @return {Array} Props.
+ */
+ function getProps(componentProps, properties, indexes, suppressQuotes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ const propertiesKeys = Object.keys(properties);
+ return indexes.map(i => {
+ const name = propertiesKeys[i];
+ const value = getPropValue(properties[name]);
+
+ return PropRep({
+ ...componentProps,
+ mode: MODE.TINY,
+ name,
+ object: value,
+ equal: ": ",
+ defaultRep: Grip,
+ title: null,
+ suppressQuotes,
+ });
+ });
+ }
+
+ /**
+ * Get the indexes of props in the object.
+ *
+ * @param {Object} properties Props object.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the props you want.
+ * @return {Array} Indexes of interesting props in the object.
+ */
+ function getPropIndexes(properties, max, filter) {
+ const indexes = [];
+
+ try {
+ let i = 0;
+ for (const name in properties) {
+ if (indexes.length >= max) {
+ return indexes;
+ }
+
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ const value = getPropValue(properties[name]);
+ let type = value.class || typeof value;
+ type = type.toLowerCase();
+
+ if (filter(type, value, name)) {
+ indexes.push(i);
+ }
+ i++;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ return indexes;
+ }
+
+ /**
+ * Get the actual value of a property.
+ *
+ * @param {Object} property
+ * @return {Object} Value of the property.
+ */
+ function getPropValue(property) {
+ let value = property;
+ if (typeof property === "object") {
+ const keys = Object.keys(property);
+ if (keys.includes("value")) {
+ value = property.value;
+ } else if (keys.includes("getterValue")) {
+ value = property.getterValue;
+ }
+ }
+ return value;
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ if (object?.class === "DeadObject") {
+ return true;
+ }
+
+ return object?.preview
+ ? typeof object.preview.ownProperties !== "undefined"
+ : typeof object?.ownPropertyLength !== "undefined";
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Grip is used in propIterator and has to be defined here.
+ const Grip = {
+ rep: wrapRender(GripRep),
+ supportsObject,
+ maxLengthMap,
+ };
+
+ // Exports from this module
+ module.exports = Grip;
+});
diff --git a/devtools/client/shared/components/reps/reps/infinity.js b/devtools/client/shared/components/reps/reps/infinity.js
new file mode 100644
index 0000000000..2a36698611
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/infinity.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a Infinity object
+ */
+
+ InfinityRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function InfinityRep(props) {
+ const { object, shouldRenderTooltip } = props;
+
+ const config = getElementConfig(shouldRenderTooltip, object);
+
+ return span(config, object.type);
+ }
+
+ function getElementConfig(shouldRenderTooltip, object) {
+ return {
+ className: "objectBox objectBox-number",
+ title: shouldRenderTooltip ? object.type : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ const type = getGripType(object, noGrip);
+ return type == "Infinity" || type == "-Infinity";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(InfinityRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/moz.build b/devtools/client/shared/components/reps/reps/moz.build
new file mode 100644
index 0000000000..30b7e72a73
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/moz.build
@@ -0,0 +1,45 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "accessible.js",
+ "accessor.js",
+ "array.js",
+ "attribute.js",
+ "big-int.js",
+ "comment-node.js",
+ "constants.js",
+ "custom-formatter.js",
+ "date-time.js",
+ "document-type.js",
+ "document.js",
+ "element-node.js",
+ "error.js",
+ "event.js",
+ "function.js",
+ "grip-array.js",
+ "grip-entry.js",
+ "grip-map.js",
+ "grip.js",
+ "infinity.js",
+ "nan.js",
+ "null.js",
+ "number.js",
+ "object-with-text.js",
+ "object-with-url.js",
+ "object.js",
+ "promise.js",
+ "prop-rep.js",
+ "regexp.js",
+ "rep-utils.js",
+ "rep.js",
+ "string.js",
+ "stylesheet.js",
+ "symbol.js",
+ "text-node.js",
+ "undefined.js",
+ "window.js",
+)
diff --git a/devtools/client/shared/components/reps/reps/nan.js b/devtools/client/shared/components/reps/reps/nan.js
new file mode 100644
index 0000000000..d8b022177c
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/nan.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a NaN object
+ */
+
+ NaNRep.PropTypes = {
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function NaNRep(props) {
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+
+ const config = getElementConfig(shouldRenderTooltip);
+
+ return span(config, "NaN");
+ }
+
+ function getElementConfig(shouldRenderTooltip) {
+ return {
+ className: "objectBox objectBox-nan",
+ title: shouldRenderTooltip ? "NaN" : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "NaN";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(NaNRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/null.js b/devtools/client/shared/components/reps/reps/null.js
new file mode 100644
index 0000000000..53dca6b8f3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/null.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders null value
+ */
+
+ Null.PropTypes = {
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Null(props) {
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+
+ const config = getElementConfig(shouldRenderTooltip);
+
+ return span(config, "null");
+ }
+
+ function getElementConfig(shouldRenderTooltip) {
+ return {
+ className: "objectBox objectBox-null",
+ title: shouldRenderTooltip ? "null" : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ if (noGrip === true) {
+ return object === null;
+ }
+
+ if (object && object.type && object.type == "null") {
+ return true;
+ }
+
+ return object == null;
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(Null),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/number.js b/devtools/client/shared/components/reps/reps/number.js
new file mode 100644
index 0000000000..455cb971e7
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/number.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a number
+ */
+
+ Number.propTypes = {
+ object: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.number,
+ PropTypes.bool,
+ ]).isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Number(props) {
+ const value = stringify(props.object);
+ const config = getElementConfig(props.shouldRenderTooltip, value);
+
+ return span(config, value);
+ }
+
+ function stringify(object) {
+ const isNegativeZero =
+ Object.is(object, -0) || (object.type && object.type == "-0");
+
+ return isNegativeZero ? "-0" : String(object);
+ }
+
+ function getElementConfig(shouldRenderTooltip, value) {
+ return {
+ className: "objectBox objectBox-number",
+ title: shouldRenderTooltip ? value : null,
+ };
+ }
+
+ const SUPPORTED_TYPES = new Set(["boolean", "number", "-0"]);
+ function supportsObject(object, noGrip = false) {
+ return SUPPORTED_TYPES.has(getGripType(object, noGrip));
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(Number),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/object-with-text.js b/devtools/client/shared/components/reps/reps/object-with-text.js
new file mode 100644
index 0000000000..5dc3f27ae8
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/object-with-text.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const String =
+ require("devtools/client/shared/components/reps/reps/string").rep;
+
+ /**
+ * Renders a grip object with textual data.
+ */
+
+ ObjectWithText.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ObjectWithText(props) {
+ const grip = props.object;
+ const config = getElementConfig(props);
+
+ return span(config, `${getType(grip)} `, getDescription(grip));
+ }
+
+ function getElementConfig(opts) {
+ const shouldRenderTooltip = opts.shouldRenderTooltip;
+ const grip = opts.object;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: `objectTitle objectBox objectBox-${getType(grip)}`,
+ title: shouldRenderTooltip
+ ? `${getType(grip)} "${grip.preview.text}"`
+ : null,
+ };
+ }
+
+ function getType(grip) {
+ return grip.class;
+ }
+
+ function getDescription(grip) {
+ return String({
+ object: grip.preview.text,
+ });
+ }
+
+ // Registration
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "ObjectWithText";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ObjectWithText),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/object-with-url.js b/devtools/client/shared/components/reps/reps/object-with-url.js
new file mode 100644
index 0000000000..0d0660cacf
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/object-with-url.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a grip object with URL data.
+ */
+
+ ObjectWithURL.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ObjectWithURL(props) {
+ const grip = props.object;
+ const config = getElementConfig(props);
+
+ return span(
+ config,
+ getTitle(grip),
+ span({ className: "objectPropValue" }, getDescription(grip))
+ );
+ }
+
+ function getElementConfig(opts) {
+ const grip = opts.object;
+ const shouldRenderTooltip = opts.shouldRenderTooltip;
+ const tooltip = `${getType(grip)} ${getDescription(grip)}`;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: `objectBox objectBox-${getType(grip)}`,
+ title: shouldRenderTooltip ? tooltip : null,
+ };
+ }
+
+ function getTitle(grip) {
+ return span({ className: "objectTitle" }, `${getType(grip)} `);
+ }
+
+ function getType(grip) {
+ return grip.class;
+ }
+
+ function getDescription(grip) {
+ return getURLDisplayString(grip.preview.url);
+ }
+
+ // Registration
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "ObjectWithURL";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ObjectWithURL),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/object.js b/devtools/client/shared/components/reps/reps/object.js
new file mode 100644
index 0000000000..bdd98cfd04
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/object.js
@@ -0,0 +1,207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ wrapRender,
+ ellipsisElement,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const DEFAULT_TITLE = "Object";
+
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+
+ ObjectRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ title: PropTypes.string,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ObjectRep(props) {
+ const object = props.object;
+ const { shouldRenderTooltip = true } = props;
+
+ if (props.mode === MODE.TINY) {
+ const tinyModeItems = [];
+ if (getTitle(props) !== DEFAULT_TITLE) {
+ tinyModeItems.push(getTitleElement(props));
+ } else {
+ tinyModeItems.push(
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ "{"
+ ),
+ Object.keys(object).length ? ellipsisElement : null,
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ "}"
+ )
+ );
+ }
+
+ return span(
+ {
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? getTitle(props) : null,
+ },
+ ...tinyModeItems
+ );
+ }
+
+ const propsArray = safePropIterator(props, object);
+
+ return span(
+ {
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? getTitle(props) : null,
+ },
+ getTitleElement(props),
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ " { "
+ ),
+ ...propsArray,
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ " }"
+ )
+ );
+ }
+
+ function getTitleElement(props) {
+ return span({ className: "objectTitle" }, getTitle(props));
+ }
+
+ function getTitle(props) {
+ return props.title || DEFAULT_TITLE;
+ }
+
+ function safePropIterator(props, object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return propIterator(props, object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ }
+
+ function propIterator(props, object, max) {
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+ if (Object.prototype.toString.call(object) === "[object Generator]") {
+ object = Object.getPrototypeOf(object);
+ }
+
+ const elements = [];
+ const unimportantProperties = [];
+ let propertiesNumber = 0;
+ const propertiesNames = Object.keys(object);
+
+ const pushPropRep = (name, value) => {
+ elements.push(
+ PropRep({
+ ...props,
+ key: name,
+ mode: MODE.TINY,
+ name,
+ object: value,
+ equal: ": ",
+ })
+ );
+ propertiesNumber++;
+
+ if (propertiesNumber < propertiesNames.length) {
+ elements.push(", ");
+ }
+ };
+
+ try {
+ for (const name of propertiesNames) {
+ if (propertiesNumber >= max) {
+ break;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ // Object members with non-empty values are preferred since it gives the
+ // user a better overview of the object.
+ if (isInterestingProp(value)) {
+ pushPropRep(name, value);
+ } else {
+ // If the property is not important, put its name on an array for later
+ // use.
+ unimportantProperties.push(name);
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ if (propertiesNumber < max) {
+ for (const name of unimportantProperties) {
+ if (propertiesNumber >= max) {
+ break;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ pushPropRep(name, value);
+ }
+ }
+
+ if (propertiesNumber < propertiesNames.length) {
+ elements.push(ellipsisElement);
+ }
+
+ return elements;
+ }
+
+ function isInterestingProp(value) {
+ const type = typeof value;
+ return type == "boolean" || type == "number" || (type == "string" && value);
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return noGrip;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ObjectRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/promise.js b/devtools/client/shared/components/reps/reps/promise.js
new file mode 100644
index 0000000000..db21f29477
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/promise.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Dependencies
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const Grip = require("devtools/client/shared/components/reps/reps/grip");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders a DOM Promise object.
+ */
+
+ PromiseRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function PromiseRep(props) {
+ const object = props.object;
+
+ // @backward-compat { version 85 } On older servers, the preview of a promise was
+ // useless and didn't include the internal promise state, which was directly exposed
+ // in the grip.
+ if (object.promiseState) {
+ const { state, value, reason } = object.promiseState;
+ const ownProperties = Object.create(null);
+ ownProperties["<state>"] = { value: state };
+ let ownPropertiesLength = 1;
+ if (state == "fulfilled") {
+ ownProperties["<value>"] = { value };
+ ++ownPropertiesLength;
+ } else if (state == "rejected") {
+ ownProperties["<reason>"] = { value: reason };
+ ++ownPropertiesLength;
+ }
+ object.preview = {
+ kind: "Object",
+ ownProperties,
+ ownPropertiesLength,
+ };
+ }
+
+ if (props.mode !== MODE.TINY) {
+ return Grip.rep(props);
+ }
+
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? "Promise" : null,
+ };
+
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ return span(
+ config,
+ getTitle(object),
+ span({ className: "objectLeftBrace" }, " { "),
+ Rep({ object: object.preview.ownProperties["<state>"].value }),
+ span({ className: "objectRightBrace" }, " }")
+ );
+ }
+
+ function getTitle(object) {
+ return span({ className: "objectTitle" }, object.class);
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ if (!Grip.supportsObject(object, noGrip)) {
+ return false;
+ }
+ return getGripType(object, noGrip) == "Promise";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(PromiseRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/prop-rep.js b/devtools/client/shared/components/reps/reps/prop-rep.js
new file mode 100644
index 0000000000..a0ab4d084c
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/prop-rep.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ maybeEscapePropertyName,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
+ */
+ PropRep.propTypes = {
+ // Additional class to set on the key element
+ keyClassName: PropTypes.string,
+ // Property name.
+ name: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
+ // Equal character rendered between property name and value.
+ equal: PropTypes.string,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ // Normally a PropRep will quote a property name that isn't valid
+ // when unquoted; but this flag can be used to suppress the
+ // quoting.
+ suppressQuotes: PropTypes.bool,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ /**
+ * Function that given a name, a delimiter and an object returns an array
+ * of React elements representing an object property (e.g. `name: value`)
+ *
+ * @param {Object} props
+ * @return {Array} Array of React elements.
+ */
+
+ function PropRep(props) {
+ const Grip = require("devtools/client/shared/components/reps/reps/grip");
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ let {
+ equal,
+ keyClassName,
+ mode,
+ name,
+ shouldRenderTooltip,
+ suppressQuotes,
+ } = props;
+
+ const className = `nodeName${keyClassName ? " " + keyClassName : ""}`;
+
+ let key;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof name === "string") {
+ if (!suppressQuotes) {
+ name = maybeEscapePropertyName(name);
+ }
+ key = span(
+ {
+ className,
+ title: shouldRenderTooltip ? name : null,
+ },
+ name
+ );
+ } else {
+ key = Rep({
+ ...props,
+ className,
+ object: name,
+ mode: mode || MODE.TINY,
+ defaultRep: Grip,
+ });
+ }
+
+ return [
+ key,
+ span(
+ {
+ className: "objectEqual",
+ },
+ equal
+ ),
+ Rep({ ...props }),
+ ];
+ }
+
+ // Exports from this module
+ module.exports = wrapRender(PropRep);
+});
diff --git a/devtools/client/shared/components/reps/reps/regexp.js b/devtools/client/shared/components/reps/reps/regexp.js
new file mode 100644
index 0000000000..fc753e0fca
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/regexp.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ ELLIPSIS,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a grip object with regular expression.
+ */
+
+ RegExp.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function RegExp(props) {
+ const { object } = props;
+ const config = getElementConfig(props);
+
+ return span(config, getSource(object));
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip } = opts;
+ const text = getSource(object);
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-regexp regexpSource",
+ title: shouldRenderTooltip ? text : null,
+ };
+ }
+
+ function getSource(grip) {
+ const { displayString } = grip;
+ if (displayString?.type === "longString") {
+ return `${displayString.initial}${ELLIPSIS}`;
+ }
+
+ return displayString;
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "RegExp";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(RegExp),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/rep-utils.js b/devtools/client/shared/components/reps/reps/rep-utils.js
new file mode 100644
index 0000000000..390cb0f4f1
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/rep-utils.js
@@ -0,0 +1,574 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const validProtocols = /(http|https|ftp|data|resource|chrome):/i;
+
+ // URL Regex, common idioms:
+ //
+ // Lead-in (URL):
+ // ( Capture because we need to know if there was a lead-in
+ // character so we can include it as part of the text
+ // preceding the match. We lack look-behind matching.
+ // ^| The URL can start at the beginning of the string.
+ // [\s(,;'"`“] Or whitespace or some punctuation that does not imply
+ // a context which would preclude a URL.
+ // )
+ //
+ // We do not need a trailing look-ahead because our regex's will terminate
+ // because they run out of characters they can eat.
+
+ // What we do not attempt to have the regexp do:
+ // - Avoid trailing '.' and ')' characters. We let our greedy match absorb
+ // these, but have a separate regex for extra characters to leave off at the
+ // end.
+ //
+ // The Regex (apart from lead-in/lead-out):
+ // ( Begin capture of the URL
+ // (?: (potential detect beginnings)
+ // https?:\/\/| Start with "http" or "https"
+ // www\d{0,3}[.][a-z0-9.\-]{2,249}|
+ // Start with "www", up to 3 numbers, then "." then
+ // something that looks domain-namey. We differ from the
+ // next case in that we do not constrain the top-level
+ // domain as tightly and do not require a trailing path
+ // indicator of "/". This is IDN root compatible.
+ // [a-z0-9.\-]{2,250}[.][a-z]{2,4}\/
+ // Detect a non-www domain, but requiring a trailing "/"
+ // to indicate a path. This only detects IDN domains
+ // with a non-IDN root. This is reasonable in cases where
+ // there is no explicit http/https start us out, but
+ // unreasonable where there is. Our real fix is the bug
+ // to port the Thunderbird/gecko linkification logic.
+ //
+ // Domain names can be up to 253 characters long, and are
+ // limited to a-zA-Z0-9 and '-'. The roots don't have
+ // hyphens unless they are IDN roots. Root zones can be
+ // found here: http://www.iana.org/domains/root/db
+ // )
+ // [-\w.!~*'();,/?:@&=+$#%]*
+ // path onwards. We allow the set of characters that
+ // encodeURI does not escape plus the result of escaping
+ // (so also '%')
+ // )
+ // eslint-disable-next-line max-len
+ const urlRegex =
+ /(^|[\s(,;'"`“])((?:https?:\/(\/)?|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
+
+ // Set of terminators that are likely to have been part of the context rather
+ // than part of the URL and so should be uneaten. This is '(', ',', ';', plus
+ // quotes and question end-ing punctuation and the potential permutations with
+ // parentheses (english-specific).
+ const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
+
+ const ELLIPSIS = "\u2026";
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { span } = dom;
+
+ function escapeNewLines(value) {
+ return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+ }
+
+ // Map from character code to the corresponding escape sequence. \0
+ // isn't here because it would require special treatment in some
+ // situations. \b, \f, and \v aren't here because they aren't very
+ // common. \' isn't here because there's no need, we only
+ // double-quote strings.
+ const escapeMap = {
+ // Tab.
+ 9: "\\t",
+ // Newline.
+ 0xa: "\\n",
+ // Carriage return.
+ 0xd: "\\r",
+ // Quote.
+ 0x22: '\\"',
+ // Backslash.
+ 0x5c: "\\\\",
+ };
+
+ // All characters we might possibly want to escape, excluding quotes.
+ // Note that we over-match here, because it's difficult to, say, match
+ // an unpaired surrogate with a regexp. The details are worked out by
+ // the replacement function; see |escapeString|.
+ const commonEscapes =
+ // Backslash.
+ "\\\\" +
+ // Controls.
+ "\x00-\x1f" +
+ // More controls.
+ "\x7f-\x9f" +
+ // BOM
+ "\ufeff" +
+ // Specials, except for the replacement character.
+ "\ufff0-\ufffc\ufffe\uffff" +
+ // Surrogates.
+ "\ud800-\udfff" +
+ // Mathematical invisibles.
+ "\u2061-\u2064" +
+ // Line and paragraph separators.
+ "\u2028-\u2029" +
+ // Private use area.
+ "\ue000-\uf8ff";
+ const escapeRegexp = new RegExp(`[${commonEscapes}]`, "g");
+ const escapeRegexpIncludingDoubleQuote = new RegExp(
+ `[${commonEscapes}"]`,
+ "g"
+ );
+
+ /**
+ * Escape a string so that the result is viewable and valid JS.
+ * Control characters, other invisibles, invalid characters, and backslash
+ * are escaped. The resulting string is quoted with either double quotes,
+ * single quotes, or backticks. The preference is for a quote that doesn't
+ * require escaping, falling back to double quotes if that's not possible
+ * (and then escaping them in the string).
+ *
+ * @param {String} str
+ * the input
+ * @param {Boolean} escapeWhitespace
+ * if true, TAB, CR, and NL characters will be escaped
+ * @return {String} the escaped string
+ */
+ function escapeString(str, escapeWhitespace) {
+ let quote = '"';
+ let regexp = escapeRegexp;
+ if (str.includes('"')) {
+ if (!str.includes("'")) {
+ quote = "'";
+ } else if (!str.includes("`") && !str.includes("${")) {
+ quote = "`";
+ } else {
+ regexp = escapeRegexpIncludingDoubleQuote;
+ }
+ }
+ return `${quote}${str.replace(regexp, (match, offset) => {
+ const c = match.charCodeAt(0);
+ if (c in escapeMap) {
+ if (!escapeWhitespace && (c === 9 || c === 0xa || c === 0xd)) {
+ return match[0];
+ }
+ return escapeMap[c];
+ }
+ if (c >= 0xd800 && c <= 0xdfff) {
+ // Find the full code point containing the surrogate, with a
+ // special case for a trailing surrogate at the start of the
+ // string.
+ if (c >= 0xdc00 && offset > 0) {
+ --offset;
+ }
+ const codePoint = str.codePointAt(offset);
+ if (codePoint >= 0xd800 && codePoint <= 0xdfff) {
+ // Unpaired surrogate.
+ return `\\u${codePoint.toString(16)}`;
+ } else if (codePoint >= 0xf0000 && codePoint <= 0x10fffd) {
+ // Private use area. Because we visit each pair of a such a
+ // character, return the empty string for one half and the
+ // real result for the other, to avoid duplication.
+ if (c <= 0xdbff) {
+ return `\\u{${codePoint.toString(16)}}`;
+ }
+ return "";
+ }
+ // Other surrogate characters are passed through.
+ return match;
+ }
+ return `\\u${`0000${c.toString(16)}`.substr(-4)}`;
+ })}${quote}`;
+ }
+
+ /**
+ * Escape a property name, if needed. "Escaping" in this context
+ * means surrounding the property name with quotes.
+ *
+ * @param {String}
+ * name the property name
+ * @return {String} either the input, or the input surrounded by
+ * quotes, properly quoted in JS syntax.
+ */
+ function maybeEscapePropertyName(name) {
+ // Quote the property name if it needs quoting. This particular
+ // test is an approximation; see
+ // https://mathiasbynens.be/notes/javascript-properties. However,
+ // the full solution requires a fair amount of Unicode data, and so
+ // let's defer that until either it's important, or the \p regexp
+ // syntax lands, see
+ // https://github.com/tc39/proposal-regexp-unicode-property-escapes.
+ if (!/^\w+$/.test(name)) {
+ name = escapeString(name);
+ }
+ return name;
+ }
+
+ function cropMultipleLines(text, limit) {
+ return escapeNewLines(cropString(text, limit));
+ }
+
+ function rawCropString(text, limit, alternativeText = ELLIPSIS) {
+ // Crop the string only if a limit is actually specified.
+ if (!limit || limit <= 0) {
+ return text;
+ }
+
+ // Set the limit at least to the length of the alternative text
+ // plus one character of the original text.
+ if (limit <= alternativeText.length) {
+ limit = alternativeText.length + 1;
+ }
+
+ const halfLimit = (limit - alternativeText.length) / 2;
+
+ if (text.length > limit) {
+ return (
+ text.substr(0, Math.ceil(halfLimit)) +
+ alternativeText +
+ text.substr(text.length - Math.floor(halfLimit))
+ );
+ }
+
+ return text;
+ }
+
+ function cropString(text, limit, alternativeText) {
+ return rawCropString(sanitizeString(`${text}`), limit, alternativeText);
+ }
+
+ function sanitizeString(text) {
+ // Replace all non-printable characters, except of
+ // (horizontal) tab (HT: \x09) and newline (LF: \x0A, CR: \x0D),
+ // with unicode replacement character (u+fffd).
+ // eslint-disable-next-line no-control-regex
+ const re = new RegExp("[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]", "g");
+ return text.replace(re, "\ufffd");
+ }
+
+ function parseURLParams(url) {
+ url = new URL(url);
+ return parseURLEncodedText(url.searchParams);
+ }
+
+ function parseURLEncodedText(text) {
+ const params = [];
+
+ // In case the text is empty just return the empty parameters
+ if (text == "") {
+ return params;
+ }
+
+ const searchParams = new URLSearchParams(text);
+ const entries = [...searchParams.entries()];
+ return entries.map(entry => {
+ return {
+ name: entry[0],
+ value: entry[1],
+ };
+ });
+ }
+
+ function getFileName(url) {
+ const split = splitURLBase(url);
+ return split.name;
+ }
+
+ function splitURLBase(url) {
+ if (!isDataURL(url)) {
+ return splitURLTrue(url);
+ }
+ return {};
+ }
+
+ function getURLDisplayString(url) {
+ return cropString(url);
+ }
+
+ function isDataURL(url) {
+ return url && url.substr(0, 5) == "data:";
+ }
+
+ function splitURLTrue(url) {
+ const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+ const m = reSplitFile.exec(url);
+
+ if (!m) {
+ return {
+ name: url,
+ path: url,
+ };
+ } else if (m[4] == "" && m[5] == "") {
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[3],
+ name: m[3] != "/" ? m[3] : m[2],
+ };
+ }
+
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[2] + m[3],
+ name: m[4] + m[5],
+ };
+ }
+
+ /**
+ * Wrap the provided render() method of a rep in a try/catch block that will
+ * render a fallback rep if the render fails.
+ */
+ function wrapRender(renderMethod) {
+ const wrappedFunction = function (props) {
+ try {
+ return renderMethod.call(this, props);
+ } catch (e) {
+ console.error(e);
+ return span(
+ {
+ className: "objectBox objectBox-failure",
+ title:
+ "This object could not be rendered, " +
+ "please file a bug on bugzilla.mozilla.org",
+ },
+ /* Labels have to be hardcoded for reps, see Bug 1317038. */
+ "Invalid object"
+ );
+ }
+ };
+ wrappedFunction.propTypes = renderMethod.propTypes;
+ return wrappedFunction;
+ }
+
+ /**
+ * Get preview items from a Grip.
+ *
+ * @param {Object} Grip from which we want the preview items
+ * @return {Array} Array of the preview items of the grip, or an empty array
+ * if the grip does not have preview items
+ */
+ function getGripPreviewItems(grip) {
+ if (!grip) {
+ return [];
+ }
+
+ // Array Grip
+ if (grip.preview && grip.preview.items) {
+ return grip.preview.items;
+ }
+
+ // Node Grip
+ if (grip.preview && grip.preview.childNodes) {
+ return grip.preview.childNodes;
+ }
+
+ // Set or Map Grip
+ if (grip.preview && grip.preview.entries) {
+ return grip.preview.entries.reduce((res, entry) => res.concat(entry), []);
+ }
+
+ // Event Grip
+ if (grip.preview && grip.preview.target) {
+ const keys = Object.keys(grip.preview.properties);
+ const values = Object.values(grip.preview.properties);
+ return [grip.preview.target, ...keys, ...values];
+ }
+
+ // RegEx Grip
+ if (grip.displayString) {
+ return [grip.displayString];
+ }
+
+ // Generic Grip
+ if (grip.preview && grip.preview.ownProperties) {
+ let propertiesValues = Object.values(grip.preview.ownProperties).map(
+ property => property.value || property
+ );
+
+ const propertyKeys = Object.keys(grip.preview.ownProperties);
+ propertiesValues = propertiesValues.concat(propertyKeys);
+
+ // ArrayBuffer Grip
+ if (grip.preview.safeGetterValues) {
+ propertiesValues = propertiesValues.concat(
+ Object.values(grip.preview.safeGetterValues).map(
+ property => property.getterValue || property
+ )
+ );
+ }
+
+ return propertiesValues;
+ }
+
+ return [];
+ }
+
+ /**
+ * Get the type of an object.
+ *
+ * @param {Object} Grip from which we want the type.
+ * @param {boolean} noGrip true if the object is not a grip.
+ * @return {boolean}
+ */
+ function getGripType(object, noGrip) {
+ if (noGrip || Object(object) !== object) {
+ return typeof object;
+ }
+ if (object.type === "object") {
+ return object.class;
+ }
+ return object.type;
+ }
+
+ /**
+ * Determines whether a grip is a string containing a URL.
+ *
+ * @param string grip
+ * The grip, which may contain a URL.
+ * @return boolean
+ * Whether the grip is a string containing a URL.
+ */
+ function containsURL(grip) {
+ // An URL can't be shorter than 5 char (e.g. "ftp:").
+ if (typeof grip !== "string" || grip.length < 5) {
+ return false;
+ }
+
+ return validProtocols.test(grip);
+ }
+
+ /**
+ * Determines whether a string token is a valid URL.
+ *
+ * @param string token
+ * The token.
+ * @return boolean
+ * Whether the token is a URL.
+ */
+ function isURL(token) {
+ try {
+ if (!validProtocols.test(token)) {
+ return false;
+ }
+ new URL(token);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns new array in which `char` are interleaved between the original items.
+ *
+ * @param {Array} items
+ * @param {String} char
+ * @returns Array
+ */
+ function interleave(items, char) {
+ return items.reduce((res, item, index) => {
+ if (index !== items.length - 1) {
+ return res.concat(item, char);
+ }
+ return res.concat(item);
+ }, []);
+ }
+
+ const ellipsisElement = span(
+ {
+ key: "more",
+ className: "more-ellipsis",
+ title: `more${ELLIPSIS}`,
+ },
+ ELLIPSIS
+ );
+
+ /**
+ * Removes any unallowed CSS properties from a string of CSS declarations
+ *
+ * @param {String} userProvidedStyle CSS declarations
+ * @param {Function} createElement Method to create a dummy element the styles get applied to
+ * @returns {Object} Filtered CSS properties as JavaScript object in camelCase notation
+ */
+ function cleanupStyle(userProvidedStyle, createElement) {
+ // Regular expression that matches the allowed CSS property names.
+ const allowedStylesRegex = new RegExp(
+ "^(?:-moz-)?(?:align|background|border|box|clear|color|cursor|display|" +
+ "float|font|justify|line|margin|padding|position|text|transition" +
+ "|outline|vertical-align|white-space|word|writing|" +
+ "(?:min-|max-)?width|(?:min-|max-)?height)"
+ );
+
+ const mozElementRegex = /\b((?:-moz-)?element)[\s('"]+/gi;
+
+ // Regex to retrieve usages of `url(*)` in property value
+ const cssUrlRegex = /url\([\'\"]?([^\)]*)/g;
+
+ // Use a dummy element to parse the style string.
+ const dummy = createElement("div");
+ dummy.style = userProvidedStyle;
+
+ // Return a style object as expected by React DOM components, e.g.
+ // {color: "red"}
+ // without forbidden properties and values.
+ return Array.from(dummy.style)
+ .filter(name => {
+ if (!allowedStylesRegex.test(name)) {
+ return false;
+ }
+
+ if (mozElementRegex.test(name)) {
+ return false;
+ }
+
+ if (name === "position") {
+ return ["static", "relative"].includes(
+ dummy.style.getPropertyValue(name)
+ );
+ }
+ // There can be multiple call to `url()` (e.g.` background: url("path/to/image"), url("data:image/png,…");`);
+ // filter out the property if the url function is called with anything that is not
+ // a data URL.
+ return Array.from(dummy.style[name].matchAll(cssUrlRegex))
+ .map(match => match[1])
+ .every(potentialUrl => potentialUrl.startsWith("data:"));
+ })
+ .reduce((object, name) => {
+ // React requires CSS properties to be provided in JavaScript form, i.e. camelCased.
+ const jsName = name.replace(/-([a-z])/g, (_, char) =>
+ char.toUpperCase()
+ );
+ return Object.assign(
+ {
+ [jsName]: dummy.style.getPropertyValue(name),
+ },
+ object
+ );
+ }, {});
+ }
+
+ module.exports = {
+ interleave,
+ isURL,
+ cropString,
+ containsURL,
+ rawCropString,
+ sanitizeString,
+ escapeString,
+ wrapRender,
+ cropMultipleLines,
+ parseURLParams,
+ parseURLEncodedText,
+ getFileName,
+ getURLDisplayString,
+ maybeEscapePropertyName,
+ getGripPreviewItems,
+ getGripType,
+ ellipsisElement,
+ ELLIPSIS,
+ uneatLastUrlCharsRegex,
+ urlRegex,
+ cleanupStyle,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/rep.js b/devtools/client/shared/components/reps/reps/rep.js
new file mode 100644
index 0000000000..8086a90b3e
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/rep.js
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Load all existing rep templates
+ const Undefined = require("devtools/client/shared/components/reps/reps/undefined");
+ const Null = require("devtools/client/shared/components/reps/reps/null");
+ const StringRep = require("devtools/client/shared/components/reps/reps/string");
+ const Number = require("devtools/client/shared/components/reps/reps/number");
+ const ArrayRep = require("devtools/client/shared/components/reps/reps/array");
+ const Obj = require("devtools/client/shared/components/reps/reps/object");
+ const SymbolRep = require("devtools/client/shared/components/reps/reps/symbol");
+ const InfinityRep = require("devtools/client/shared/components/reps/reps/infinity");
+ const NaNRep = require("devtools/client/shared/components/reps/reps/nan");
+ const Accessor = require("devtools/client/shared/components/reps/reps/accessor");
+
+ // DOM types (grips)
+ const Accessible = require("devtools/client/shared/components/reps/reps/accessible");
+ const Attribute = require("devtools/client/shared/components/reps/reps/attribute");
+ const BigInt = require("devtools/client/shared/components/reps/reps/big-int");
+ const DateTime = require("devtools/client/shared/components/reps/reps/date-time");
+ const Document = require("devtools/client/shared/components/reps/reps/document");
+ const DocumentType = require("devtools/client/shared/components/reps/reps/document-type");
+ const Event = require("devtools/client/shared/components/reps/reps/event");
+ const Func = require("devtools/client/shared/components/reps/reps/function");
+ const PromiseRep = require("devtools/client/shared/components/reps/reps/promise");
+ const RegExp = require("devtools/client/shared/components/reps/reps/regexp");
+ const StyleSheet = require("devtools/client/shared/components/reps/reps/stylesheet");
+ const CommentNode = require("devtools/client/shared/components/reps/reps/comment-node");
+ const ElementNode = require("devtools/client/shared/components/reps/reps/element-node");
+ const TextNode = require("devtools/client/shared/components/reps/reps/text-node");
+ const ErrorRep = require("devtools/client/shared/components/reps/reps/error");
+ const Window = require("devtools/client/shared/components/reps/reps/window");
+ const ObjectWithText = require("devtools/client/shared/components/reps/reps/object-with-text");
+ const ObjectWithURL = require("devtools/client/shared/components/reps/reps/object-with-url");
+ const GripArray = require("devtools/client/shared/components/reps/reps/grip-array");
+ const GripEntry = require("devtools/client/shared/components/reps/reps/grip-entry");
+ const GripMap = require("devtools/client/shared/components/reps/reps/grip-map");
+ const Grip = require("devtools/client/shared/components/reps/reps/grip");
+
+ // List of all registered template.
+ // XXX there should be a way for extensions to register a new
+ // or modify an existing rep.
+ const reps = [
+ RegExp,
+ StyleSheet,
+ Event,
+ DateTime,
+ CommentNode,
+ Accessible,
+ ElementNode,
+ TextNode,
+ Attribute,
+ Func,
+ PromiseRep,
+ Document,
+ DocumentType,
+ Window,
+ ObjectWithText,
+ ObjectWithURL,
+ ErrorRep,
+ GripArray,
+ GripMap,
+ GripEntry,
+ Grip,
+ Undefined,
+ Null,
+ StringRep,
+ Number,
+ BigInt,
+ SymbolRep,
+ InfinityRep,
+ NaNRep,
+ Accessor,
+ ];
+
+ // Reps for rendering of native object reference (e.g. used from the JSONViewer, Netmonitor, …)
+ const noGripReps = [StringRep, Number, ArrayRep, Undefined, Null, Obj];
+
+ /**
+ * Generic rep that is used for rendering native JS types or an object.
+ * The right template used for rendering is picked automatically according
+ * to the current value type. The value must be passed in as the 'object'
+ * property.
+ */
+ const Rep = function (props) {
+ const { object, defaultRep } = props;
+ const rep = getRep(
+ object,
+ defaultRep,
+ props.noGrip,
+ props.mayUseCustomFormatter
+ );
+ return rep(props);
+ };
+
+ const exportedReps = {
+ Accessible,
+ Accessor,
+ ArrayRep,
+ Attribute,
+ BigInt,
+ CommentNode,
+ DateTime,
+ Document,
+ DocumentType,
+ ElementNode,
+ ErrorRep,
+ Event,
+ Func,
+ Grip,
+ GripArray,
+ GripMap,
+ GripEntry,
+ InfinityRep,
+ NaNRep,
+ Null,
+ Number,
+ Obj,
+ ObjectWithText,
+ ObjectWithURL,
+ PromiseRep,
+ RegExp,
+ Rep,
+ StringRep,
+ StyleSheet,
+ SymbolRep,
+ TextNode,
+ Undefined,
+ Window,
+ };
+
+ // Custom Formatters
+ // ToDo: This preference can be removed once the custom formatters feature is stable enough
+ // Services.prefs isn't available in jsonviewer. It doesn't matter as we don't want to use
+ // custom formatters there
+ if (typeof Services == "object" && Services?.prefs) {
+ const customFormattersExperimentallyEnabled = Services.prefs.getBoolPref(
+ "devtools.custom-formatters",
+ false
+ );
+
+ const useCustomFormatters =
+ customFormattersExperimentallyEnabled &&
+ Services.prefs.getBoolPref("devtools.custom-formatters.enabled", false);
+
+ if (useCustomFormatters) {
+ const CustomFormatter = require("devtools/client/shared/components/reps/reps/custom-formatter");
+ reps.unshift(CustomFormatter);
+ exportedReps.CustomFormatter = CustomFormatter;
+ }
+ }
+
+ // Helpers
+
+ /**
+ * Return a rep object that is responsible for rendering given
+ * object.
+ *
+ * @param object {Object} Object to be rendered in the UI. This
+ * can be generic JS object as well as a grip (handle to a remote
+ * debuggee object).
+ *
+ * @param defaultRep {React.Component} The default template
+ * that should be used to render given object if none is found.
+ *
+ * @param noGrip {Boolean} If true, will only check reps not made for remote
+ * objects.
+ *
+ * @param mayUseCustomFormatter {Boolean} If true, custom formatters are
+ * allowed to be used as rep.
+ */
+ function getRep(
+ object,
+ defaultRep = Grip,
+ noGrip = false,
+ mayUseCustomFormatter = false
+ ) {
+ const repsList = noGrip ? noGripReps : reps;
+ for (const rep of repsList) {
+ if (rep === exportedReps.CustomFormatter && !mayUseCustomFormatter) {
+ continue;
+ }
+
+ try {
+ // supportsObject could return weight (not only true/false
+ // but a number), which would allow to priorities templates and
+ // support better extensibility.
+ if (rep.supportsObject(object, noGrip)) {
+ return rep.rep;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return defaultRep.rep;
+ }
+
+ module.exports = {
+ Rep,
+ REPS: exportedReps,
+ // Exporting for tests
+ getRep,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/string.js b/devtools/client/shared/components/reps/reps/string.js
new file mode 100644
index 0000000000..92539a9140
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/string.js
@@ -0,0 +1,407 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const {
+ a,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ containsURL,
+ escapeString,
+ getGripType,
+ rawCropString,
+ sanitizeString,
+ wrapRender,
+ ELLIPSIS,
+ uneatLastUrlCharsRegex,
+ urlRegex,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a string. String value is enclosed within quotes.
+ */
+
+ StringRep.propTypes = {
+ useQuotes: PropTypes.bool,
+ escapeWhitespace: PropTypes.bool,
+ style: PropTypes.object,
+ cropLimit: PropTypes.number.isRequired,
+ urlCropLimit: PropTypes.number,
+ member: PropTypes.object,
+ object: PropTypes.object.isRequired,
+ openLink: PropTypes.func,
+ className: PropTypes.string,
+ title: PropTypes.string,
+ isInContentPage: PropTypes.bool,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function StringRep(props) {
+ const {
+ className,
+ style,
+ cropLimit,
+ urlCropLimit,
+ object,
+ useQuotes = true,
+ escapeWhitespace = true,
+ member,
+ openLink,
+ title,
+ isInContentPage,
+ transformEmptyString = false,
+ shouldRenderTooltip,
+ } = props;
+
+ let text = object;
+ const config = getElementConfig({
+ className,
+ style,
+ actor: object.actor,
+ title,
+ });
+
+ if (text == "" && transformEmptyString && !useQuotes) {
+ return span(
+ {
+ ...config,
+ title: "<empty string>",
+ className: `${config.className} objectBox-empty-string`,
+ },
+ "<empty string>"
+ );
+ }
+
+ const isLong = isLongString(object);
+ const isOpen = member && member.open;
+ const shouldCrop = !isOpen && cropLimit && text.length > cropLimit;
+
+ if (isLong) {
+ text = maybeCropLongString(
+ {
+ shouldCrop,
+ cropLimit,
+ },
+ text
+ );
+
+ const { fullText } = object;
+ if (isOpen && fullText) {
+ text = fullText;
+ }
+ }
+
+ text = formatText(
+ {
+ useQuotes,
+ escapeWhitespace,
+ },
+ text
+ );
+
+ if (shouldRenderTooltip) {
+ config.title = text;
+ }
+
+ if (!isLong) {
+ if (containsURL(text)) {
+ return span(
+ config,
+ getLinkifiedElements({
+ text,
+ cropLimit: shouldCrop ? cropLimit : null,
+ urlCropLimit,
+ openLink,
+ isInContentPage,
+ })
+ );
+ }
+
+ // Cropping of longString has been handled before formatting.
+ text = maybeCropString(
+ {
+ isLong,
+ shouldCrop,
+ cropLimit,
+ },
+ text
+ );
+ }
+
+ return span(config, text);
+ }
+
+ function maybeCropLongString(opts, object) {
+ const { shouldCrop, cropLimit } = opts;
+
+ const grip = object && object.getGrip ? object.getGrip() : object;
+ const { initial, length } = grip;
+
+ let text = shouldCrop ? initial.substring(0, cropLimit) : initial;
+
+ if (text.length < length) {
+ text += ELLIPSIS;
+ }
+
+ return text;
+ }
+
+ function formatText(opts, text) {
+ const { useQuotes, escapeWhitespace } = opts;
+
+ return useQuotes
+ ? escapeString(text, escapeWhitespace)
+ : sanitizeString(text);
+ }
+
+ function getElementConfig(opts) {
+ const { className, style, actor, title } = opts;
+
+ const config = {};
+
+ if (actor) {
+ config["data-link-actor-id"] = actor;
+ }
+
+ if (title) {
+ config.title = title;
+ }
+
+ const classNames = ["objectBox", "objectBox-string"];
+ if (className) {
+ classNames.push(className);
+ }
+ config.className = classNames.join(" ");
+
+ if (style) {
+ config.style = style;
+ }
+
+ return config;
+ }
+
+ function maybeCropString(opts, text) {
+ const { shouldCrop, cropLimit } = opts;
+
+ return shouldCrop ? rawCropString(text, cropLimit) : text;
+ }
+
+ /**
+ * Get an array of the elements representing the string, cropped if needed,
+ * with actual links.
+ *
+ * @param {Object} An options object of the following shape:
+ * - text {String}: The actual string to linkify.
+ * - cropLimit {Integer}: The limit to apply on the whole text.
+ * - urlCropLimit {Integer}: The limit to apply on each URL.
+ * - openLink {Function} openLink: Function handling the link
+ * opening.
+ * - isInContentPage {Boolean}: pass true if the reps is
+ * rendered in the content page
+ * (e.g. in JSONViewer).
+ * @returns {Array<String|ReactElement>}
+ */
+ function getLinkifiedElements({
+ text,
+ cropLimit,
+ urlCropLimit,
+ openLink,
+ isInContentPage,
+ }) {
+ const halfLimit = Math.ceil((cropLimit - ELLIPSIS.length) / 2);
+ const startCropIndex = cropLimit ? halfLimit : null;
+ const endCropIndex = cropLimit ? text.length - halfLimit : null;
+
+ const items = [];
+ let currentIndex = 0;
+ let contentStart;
+ while (true) {
+ const url = urlRegex.exec(text);
+ // Pick the regexp with the earlier content; index will always be zero.
+ if (!url) {
+ break;
+ }
+ contentStart = url.index + url[1].length;
+ if (contentStart > 0) {
+ const nonUrlText = text.substring(0, contentStart);
+ items.push(
+ getCroppedString(
+ nonUrlText,
+ currentIndex,
+ startCropIndex,
+ endCropIndex
+ )
+ );
+ }
+
+ // There are some final characters for a URL that are much more likely
+ // to have been part of the enclosing text rather than the end of the
+ // URL.
+ let useUrl = url[2];
+ const uneat = uneatLastUrlCharsRegex.exec(useUrl);
+ if (uneat) {
+ useUrl = useUrl.substring(0, uneat.index);
+ }
+
+ currentIndex = currentIndex + contentStart;
+ const linkText = getCroppedString(
+ useUrl,
+ currentIndex,
+ startCropIndex,
+ endCropIndex
+ );
+
+ if (linkText) {
+ const linkItems = [];
+ const shouldCrop = urlCropLimit && useUrl.length > urlCropLimit;
+ if (shouldCrop) {
+ const urlCropHalf = Math.ceil((urlCropLimit - ELLIPSIS.length) / 2);
+ // We cut the string into 3 elements and we'll visually hide the second one
+ // in CSS. This way people can still copy the full link.
+ linkItems.push(
+ span(
+ { className: "cropped-url-start" },
+ useUrl.substring(0, urlCropHalf)
+ ),
+ span(
+ { className: "cropped-url-middle" },
+ useUrl.substring(urlCropHalf, useUrl.length - urlCropHalf)
+ ),
+ span(
+ { className: "cropped-url-end" },
+ useUrl.substring(useUrl.length - urlCropHalf)
+ )
+ );
+ } else {
+ linkItems.push(linkText);
+ }
+
+ items.push(
+ a(
+ {
+ key: `${useUrl}-${currentIndex}`,
+ className: "url" + (shouldCrop ? " cropped-url" : ""),
+ title: useUrl,
+ draggable: false,
+ // Because we don't want the link to be open in the current
+ // panel's frame, we only render the href attribute if `openLink`
+ // exists (so we can preventDefault) or if the reps will be
+ // displayed in content page (e.g. in the JSONViewer).
+ href: openLink || isInContentPage ? useUrl : null,
+ target: "_blank",
+ rel: "noopener noreferrer",
+ onClick: openLink
+ ? e => {
+ e.preventDefault();
+ openLink(useUrl, e);
+ }
+ : null,
+ },
+ linkItems
+ )
+ );
+ }
+
+ currentIndex = currentIndex + useUrl.length;
+ text = text.substring(url.index + url[1].length + useUrl.length);
+ }
+
+ // Clean up any non-URL text at the end of the source string,
+ // i.e. not handled in the loop.
+ if (text.length) {
+ if (currentIndex < endCropIndex) {
+ text = getCroppedString(
+ text,
+ currentIndex,
+ startCropIndex,
+ endCropIndex
+ );
+ }
+ items.push(text);
+ }
+
+ return items;
+ }
+
+ /**
+ * Returns a cropped substring given an offset, start and end crop indices in a
+ * parent string.
+ *
+ * @param {String} text: The substring to crop.
+ * @param {Integer} offset: The offset corresponding to the index at which
+ * the substring is in the parent string.
+ * @param {Integer|null} startCropIndex: the index where the start of the crop
+ * should happen in the parent string.
+ * @param {Integer|null} endCropIndex: the index where the end of the crop
+ * should happen in the parent string
+ * @returns {String|null} The cropped substring, or null if the text is
+ * completly cropped.
+ */
+ function getCroppedString(text, offset = 0, startCropIndex, endCropIndex) {
+ if (!startCropIndex) {
+ return text;
+ }
+
+ const start = offset;
+ const end = offset + text.length;
+
+ const shouldBeVisible = !(start >= startCropIndex && end <= endCropIndex);
+ if (!shouldBeVisible) {
+ return null;
+ }
+
+ const shouldCropEnd = start < startCropIndex && end > startCropIndex;
+ const shouldCropStart = start < endCropIndex && end > endCropIndex;
+ if (shouldCropEnd) {
+ const cutIndex = startCropIndex - start;
+ return (
+ text.substring(0, cutIndex) +
+ ELLIPSIS +
+ (shouldCropStart ? text.substring(endCropIndex - start) : "")
+ );
+ }
+
+ if (shouldCropStart) {
+ // The string should be cropped at the beginning.
+ const cutIndex = endCropIndex - start;
+ return text.substring(cutIndex);
+ }
+
+ return text;
+ }
+
+ function isLongString(object) {
+ const grip = object && object.getGrip ? object.getGrip() : object;
+ return grip && grip.type === "longString";
+ }
+
+ function supportsObject(object, noGrip = false) {
+ // Accept the object if the grip-type (or type for noGrip objects) is "string"
+ if (getGripType(object, noGrip) == "string") {
+ return true;
+ }
+
+ // Also accept longString objects if we're expecting grip
+ if (!noGrip) {
+ return isLongString(object);
+ }
+
+ return false;
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(StringRep),
+ supportsObject,
+ isLongString,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/stylesheet.js b/devtools/client/shared/components/reps/reps/stylesheet.js
new file mode 100644
index 0000000000..591442ebbf
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/stylesheet.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a grip representing CSSStyleSheet
+ */
+
+ StyleSheet.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function StyleSheet(props) {
+ const grip = props.object;
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+ const location = getLocation(grip);
+ const config = getElementConfig({ grip, shouldRenderTooltip, location });
+
+ return span(
+ config,
+ getTitle(grip),
+ span({ className: "objectPropValue" }, location)
+ );
+ }
+
+ function getElementConfig(opts) {
+ const { grip, shouldRenderTooltip, location } = opts;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip
+ ? `${getGripType(grip, false)} ${location}`
+ : null,
+ };
+ }
+
+ function getTitle(grip) {
+ return span(
+ { className: "objectBoxTitle" },
+ `${getGripType(grip, false)} `
+ );
+ }
+
+ function getLocation(grip) {
+ // Embedded stylesheets don't have URL and so, no preview.
+ const url = grip.preview ? grip.preview.url : "";
+ return url ? getURLDisplayString(url) : "";
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "CSSStyleSheet";
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(StyleSheet),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/symbol.js b/devtools/client/shared/components/reps/reps/symbol.js
new file mode 100644
index 0000000000..89c76cdbb2
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/symbol.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const {
+ rep: StringRep,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ const MAX_STRING_LENGTH = 50;
+
+ /**
+ * Renders a symbol.
+ */
+
+ SymbolRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function SymbolRep(props) {
+ const {
+ className = "objectBox objectBox-symbol",
+ object,
+ shouldRenderTooltip,
+ } = props;
+ const { name } = object;
+
+ let symbolText = name || "";
+ if (name && name !== "Symbol.iterator" && name !== "Symbol.asyncIterator") {
+ symbolText = StringRep({
+ object: symbolText,
+ shouldCrop: true,
+ cropLimit: MAX_STRING_LENGTH,
+ useQuotes: true,
+ });
+ }
+
+ const config = getElementConfig(
+ {
+ shouldRenderTooltip,
+ className,
+ name,
+ },
+ object
+ );
+
+ return span(config, "Symbol(", symbolText, ")");
+ }
+
+ function getElementConfig(opts, object) {
+ const { shouldRenderTooltip, className, name } = opts;
+
+ return {
+ "data-link-actor-id": object.actor,
+ className,
+ title: shouldRenderTooltip ? `Symbol(${name})` : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "symbol";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(SymbolRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/text-node.js b/devtools/client/shared/components/reps/reps/text-node.js
new file mode 100644
index 0000000000..5df86030f3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/text-node.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ // Reps
+ const {
+ cropString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const {
+ rep: StringRep,
+ isLongString,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ /**
+ * Renders DOM #text node.
+ */
+
+ TextNode.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function TextNode(props) {
+ const { object: grip, mode = MODE.SHORT } = props;
+
+ const isInTree = grip.preview && grip.preview.isConnected === true;
+ const config = getElementConfig({ ...props, isInTree });
+ const inspectIcon = getInspectIcon({ ...props, isInTree });
+
+ if (mode === MODE.TINY) {
+ return span(config, getTitle(grip), inspectIcon);
+ }
+
+ return span(
+ config,
+ getTitle(grip),
+ " ",
+ StringRep({
+ className: "nodeValue",
+ object: grip.preview.textContent,
+ }),
+ inspectIcon ? inspectIcon : null
+ );
+ }
+
+ function getElementConfig(opts) {
+ const {
+ object,
+ isInTree,
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ shouldRenderTooltip,
+ } = opts;
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ "data-link-content-dom-reference": JSON.stringify(
+ object.contentDomReference
+ ),
+ className: "objectBox objectBox-textNode",
+ title: shouldRenderTooltip ? `#text "${getTextContent(object)}"` : null,
+ };
+
+ if (isInTree) {
+ if (onDOMNodeMouseOver) {
+ Object.assign(config, {
+ onMouseOver: _ => onDOMNodeMouseOver(object),
+ });
+ }
+
+ if (onDOMNodeMouseOut) {
+ Object.assign(config, {
+ onMouseOut: _ => onDOMNodeMouseOut(object),
+ });
+ }
+ }
+
+ return config;
+ }
+
+ function getTextContent(grip) {
+ const text = grip.preview.textContent;
+ return cropString(isLongString(text) ? text.initial : text);
+ }
+
+ function getInspectIcon(opts) {
+ const { object, isInTree, onInspectIconClick } = opts;
+
+ if (!isInTree || !onInspectIconClick) {
+ return null;
+ }
+
+ return button({
+ className: "open-inspector",
+ draggable: false,
+ // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
+ title: "Click to select the node in the inspector",
+ onClick: e => onInspectIconClick(object, e),
+ });
+ }
+
+ function getTitle(grip) {
+ const title = "#text";
+ return span({}, title);
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return grip?.preview && grip?.class == "Text";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(TextNode),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/undefined.js b/devtools/client/shared/components/reps/reps/undefined.js
new file mode 100644
index 0000000000..7e0ac4c786
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/undefined.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders undefined value
+ */
+
+ Undefined.propTypes = {
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Undefined(props) {
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+
+ const config = getElementConfig(shouldRenderTooltip);
+
+ return span(config, "undefined");
+ }
+
+ function getElementConfig(shouldRenderTooltip) {
+ return {
+ className: "objectBox objectBox-undefined",
+ title: shouldRenderTooltip ? "undefined" : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ if (noGrip === true) {
+ return object === undefined;
+ }
+
+ return (
+ (object && object.type && object.type == "undefined") ||
+ getGripType(object, noGrip) == "undefined"
+ );
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(Undefined),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/window.js b/devtools/client/shared/components/reps/reps/window.js
new file mode 100644
index 0000000000..2c420c2b30
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/window.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders a grip representing a window.
+ */
+
+ WindowRep.propTypes = {
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function WindowRep(props) {
+ const { mode, object } = props;
+
+ if (mode === MODE.TINY) {
+ const tinyTitle = getTitle(object);
+ const title = getTitle(object, true);
+ const location = getLocation(object);
+ const config = getElementConfig({ ...props, title, location });
+
+ return span(
+ config,
+ span({ className: tinyTitle.className }, tinyTitle.content)
+ );
+ }
+
+ const title = getTitle(object, true);
+ const location = getLocation(object);
+ const config = getElementConfig({ ...props, title, location });
+
+ return span(
+ config,
+ span({ className: title.className }, title.content),
+ span({ className: "location" }, location)
+ );
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip, title, location } = opts;
+ let tooltip;
+
+ if (location) {
+ tooltip = `${title.content}${location}`;
+ } else {
+ tooltip = `${title.content}`;
+ }
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-Window",
+ title: shouldRenderTooltip ? tooltip : null,
+ };
+ }
+
+ function getTitle(object, trailingSpace) {
+ let title = object.displayClass || object.class || "Window";
+ if (trailingSpace === true) {
+ title = `${title} `;
+ }
+ return {
+ className: "objectTitle",
+ content: title,
+ };
+ }
+
+ function getLocation(object) {
+ return getURLDisplayString(object.preview.url);
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return object?.preview && getGripType(object, noGrip) == "Window";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(WindowRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/shared/dom-node-constants.js b/devtools/client/shared/components/reps/shared/dom-node-constants.js
new file mode 100644
index 0000000000..ecc1861d65
--- /dev/null
+++ b/devtools/client/shared/components/reps/shared/dom-node-constants.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ module.exports = {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12,
+
+ // DocumentPosition
+ DOCUMENT_POSITION_DISCONNECTED: 0x01,
+ DOCUMENT_POSITION_PRECEDING: 0x02,
+ DOCUMENT_POSITION_FOLLOWING: 0x04,
+ DOCUMENT_POSITION_CONTAINS: 0x08,
+ DOCUMENT_POSITION_CONTAINED_BY: 0x10,
+ DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20,
+ };
+});
diff --git a/devtools/client/shared/components/reps/shared/grip-length-bubble.js b/devtools/client/shared/components/reps/shared/grip-length-bubble.js
new file mode 100644
index 0000000000..8d0dcb8bb8
--- /dev/null
+++ b/devtools/client/shared/components/reps/shared/grip-length-bubble.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const {
+ ModePropType,
+ } = require("devtools/client/shared/components/reps/reps/array");
+
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { span } = dom;
+
+ GripLengthBubble.propTypes = {
+ object: PropTypes.object.isRequired,
+ maxLengthMap: PropTypes.instanceOf(Map).isRequired,
+ getLength: PropTypes.func.isRequired,
+ mode: ModePropType,
+ visibilityThreshold: PropTypes.number,
+ };
+
+ function GripLengthBubble(props) {
+ const {
+ object,
+ mode = MODE.SHORT,
+ visibilityThreshold = 2,
+ maxLengthMap,
+ getLength,
+ showZeroLength = false,
+ } = props;
+
+ const length = getLength(object);
+ const isEmpty = length === 0;
+ const isObvious =
+ [MODE.SHORT, MODE.LONG].includes(mode) &&
+ length > 0 &&
+ length <= maxLengthMap.get(mode) &&
+ length <= visibilityThreshold;
+ if ((isEmpty && !showZeroLength) || isObvious) {
+ return "";
+ }
+
+ return span(
+ {
+ className: "objectLengthBubble",
+ },
+ `(${length})`
+ );
+ }
+
+ module.exports = {
+ lengthBubble: wrapRender(GripLengthBubble),
+ };
+});
diff --git a/devtools/client/shared/components/reps/shared/moz.build b/devtools/client/shared/components/reps/shared/moz.build
new file mode 100644
index 0000000000..6704491b97
--- /dev/null
+++ b/devtools/client/shared/components/reps/shared/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "dom-node-constants.js",
+ "grip-length-bubble.js",
+)
diff --git a/devtools/client/shared/components/splitter/Draggable.js b/devtools/client/shared/components/splitter/Draggable.js
new file mode 100644
index 0000000000..3d18e49c34
--- /dev/null
+++ b/devtools/client/shared/components/splitter/Draggable.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createRef,
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+class Draggable extends Component {
+ static get propTypes() {
+ return {
+ onMove: PropTypes.func.isRequired,
+ onDoubleClick: PropTypes.func,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ title: PropTypes.string,
+ className: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.draggableEl = createRef();
+
+ this.startDragging = this.startDragging.bind(this);
+ this.stopDragging = this.stopDragging.bind(this);
+ this.onDoubleClick = this.onDoubleClick.bind(this);
+ this.onMove = this.onMove.bind(this);
+
+ this.mouseX = 0;
+ this.mouseY = 0;
+ }
+ startDragging(ev) {
+ const xDiff = Math.abs(this.mouseX - ev.clientX);
+ const yDiff = Math.abs(this.mouseY - ev.clientY);
+
+ // This allows for double-click.
+ if (this.props.onDoubleClick && xDiff + yDiff <= 1) {
+ return;
+ }
+ this.mouseX = ev.clientX;
+ this.mouseY = ev.clientY;
+
+ if (this.isDragging) {
+ return;
+ }
+ this.isDragging = true;
+ ev.preventDefault();
+
+ this.draggableEl.current.addEventListener("mousemove", this.onMove);
+ this.draggableEl.current.setPointerCapture(ev.pointerId);
+
+ this.props.onStart && this.props.onStart();
+ }
+
+ onDoubleClick() {
+ if (this.props.onDoubleClick) {
+ this.props.onDoubleClick();
+ }
+ }
+
+ onMove(ev) {
+ if (!this.isDragging) {
+ return;
+ }
+
+ ev.preventDefault();
+ // Use viewport coordinates so, moving mouse over iframes
+ // doesn't mangle (relative) coordinates.
+ this.props.onMove(ev.clientX, ev.clientY);
+ }
+
+ stopDragging(ev) {
+ if (!this.isDragging) {
+ return;
+ }
+ this.isDragging = false;
+ ev.preventDefault();
+
+ this.draggableEl.current.removeEventListener("mousemove", this.onMove);
+ this.draggableEl.current.releasePointerCapture(ev.pointerId);
+ this.props.onStop && this.props.onStop();
+ }
+
+ render() {
+ return dom.div({
+ ref: this.draggableEl,
+ role: "presentation",
+ style: this.props.style,
+ title: this.props.title,
+ className: this.props.className,
+ onMouseDown: this.startDragging,
+ onMouseUp: this.stopDragging,
+ onDoubleClick: this.onDoubleClick,
+ });
+ }
+}
+
+module.exports = Draggable;
diff --git a/devtools/client/shared/components/splitter/GridElementResizer.css b/devtools/client/shared/components/splitter/GridElementResizer.css
new file mode 100644
index 0000000000..dfa69592e9
--- /dev/null
+++ b/devtools/client/shared/components/splitter/GridElementResizer.css
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.grid-element-width-resizer {
+ /* The space we'll have on each side of the "splitter border" */
+ --inline-inset: 3px;
+ /* We use the --inline-inset value that we multiply by 2 and add 1px to center the splitter */
+ width: calc(1px + (2 * var(--inline-inset)));
+ position: relative;
+ cursor: ew-resize;
+ z-index: 10;
+}
+
+.grid-element-width-resizer.start {
+ justify-self: start;
+ inset-inline-start: calc(-1 * var(--inline-inset));
+}
+
+.grid-element-width-resizer.end {
+ justify-self: end;
+ inset-inline-start: var(--inline-inset);
+}
+
+.dragging,
+.dragging * {
+ /* When resizing, we keep the "resize" cursor on every element we might hover */
+ cursor: ew-resize !important;
+ /* This prevents to trigger some :hover style and is better for performance
+ * when resizing */
+ pointer-events: none !important;
+}
diff --git a/devtools/client/shared/components/splitter/GridElementWidthResizer.js b/devtools/client/shared/components/splitter/GridElementWidthResizer.js
new file mode 100644
index 0000000000..c6ab6f3e14
--- /dev/null
+++ b/devtools/client/shared/components/splitter/GridElementWidthResizer.js
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Draggable = createFactory(
+ require("resource://devtools/client/shared/components/splitter/Draggable.js")
+);
+
+class GridElementWidthResizer extends Component {
+ static get propTypes() {
+ return {
+ getControlledElementNode: PropTypes.func.isRequired,
+ enabled: PropTypes.bool,
+ position: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ onResizeEnd: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onStartMove = this.onStartMove.bind(this);
+ this.onStopMove = this.onStopMove.bind(this);
+ this.onMove = this.onMove.bind(this);
+ this.state = {
+ dragging: false,
+ isRTLElement: false,
+ defaultCursor: null,
+ defaultWidth: null,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.enabled === true && this.props.enabled === false) {
+ this.onStopMove();
+ const controlledElementNode = this.props.getControlledElementNode();
+ controlledElementNode.style.width = this.state.defaultWidth;
+ }
+ }
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const controlledElementNode = this.props.getControlledElementNode();
+ if (!controlledElementNode) {
+ return;
+ }
+
+ const doc = controlledElementNode.ownerDocument;
+ const defaultCursor = doc.documentElement.style.cursor;
+ const defaultWidth = doc.documentElement.style.width;
+ doc.documentElement.style.cursor = "ew-resize";
+ doc.firstElementChild.classList.add("dragging");
+
+ this.setState({
+ dragging: true,
+ isRTLElement:
+ controlledElementNode.ownerDocument.defaultView.getComputedStyle(
+ controlledElementNode
+ ).direction === "rtl",
+ defaultCursor,
+ defaultWidth,
+ });
+ }
+
+ onStopMove() {
+ const controlledElementNode = this.props.getControlledElementNode();
+ if (!this.state.dragging || !controlledElementNode) {
+ return;
+ }
+ const doc = controlledElementNode.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+ doc.firstElementChild.classList.remove("dragging");
+
+ this.setState({
+ dragging: false,
+ });
+
+ if (this.props.onResizeEnd) {
+ const { width } = controlledElementNode.getBoundingClientRect();
+ this.props.onResizeEnd(width);
+ }
+ }
+
+ /**
+ * Adjust size of the controlled panel.
+ */
+ onMove(x) {
+ const controlledElementNode = this.props.getControlledElementNode();
+ if (!this.state.dragging || !controlledElementNode) {
+ return;
+ }
+ const nodeBounds = controlledElementNode.getBoundingClientRect();
+ const { isRTLElement } = this.state;
+ const { position } = this.props;
+
+ const size =
+ (isRTLElement && position === "end") ||
+ (!isRTLElement && position === "start")
+ ? nodeBounds.width + (nodeBounds.left - x)
+ : x - nodeBounds.left;
+
+ controlledElementNode.style.width = `${size}px`;
+ }
+
+ render() {
+ if (!this.props.enabled) {
+ return null;
+ }
+
+ const classNames = ["grid-element-width-resizer", this.props.position];
+ if (this.props.className) {
+ classNames.push(this.props.className);
+ }
+
+ return Draggable({
+ className: classNames.join(" "),
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove,
+ });
+ }
+}
+
+module.exports = GridElementWidthResizer;
diff --git a/devtools/client/shared/components/splitter/SplitBox.css b/devtools/client/shared/components/splitter/SplitBox.css
new file mode 100644
index 0000000000..6028619e7b
--- /dev/null
+++ b/devtools/client/shared/components/splitter/SplitBox.css
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.split-box {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.split-box.vert {
+ flex-direction: row;
+}
+
+.split-box.horz {
+ flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+}
+
+.split-box > .controlled {
+ display: flex;
+ overflow: auto;
+}
+
+.split-box > .splitter {
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--theme-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.split-box.vert > .splitter {
+ min-width: var(--devtools-vertical-splitter-min-width);
+
+ border-inline-start-width: var(--devtools-splitter-inline-start-width);
+ border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+ margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+ /* Emphasize the horizontal splitter width and color */
+ min-height: var(--devtools-emphasized-horizontal-splitter-min-height);
+
+ background-color: var(--theme-emphasized-splitter-color);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: ns-resize;
+}
+
+/* Emphasized splitter has the hover style. */
+.split-box.horz > .splitter:hover {
+ background-color: var(--theme-emphasized-splitter-color-hover);
+}
+
+.split-box.disabled {
+ pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+ pointer-events: none;
+}
diff --git a/devtools/client/shared/components/splitter/SplitBox.js b/devtools/client/shared/components/splitter/SplitBox.js
new file mode 100644
index 0000000000..2bf0fdb74d
--- /dev/null
+++ b/devtools/client/shared/components/splitter/SplitBox.js
@@ -0,0 +1,351 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Draggable = createFactory(
+ require("resource://devtools/client/shared/components/splitter/Draggable.js")
+);
+
+/**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+class SplitBox extends Component {
+ static get propTypes() {
+ return {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Initial width of controlled panel.
+ initialWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Initial height of controlled panel.
+ initialHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Left/top panel collapse state.
+ startPanelCollapsed: PropTypes.bool,
+ // Min panel size.
+ minSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Max panel size.
+ maxSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // Right/bottom panel collapse state.
+ endPanelCollapsed: PropTypes.bool,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.number,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool,
+ // Style object.
+ style: PropTypes.object,
+ // Call when controlled panel was resized.
+ onControlledPanelResized: PropTypes.func,
+ // Optional callback when splitbox resize stops
+ onResizeEnd: PropTypes.func,
+ // Retrieve DOM reference to the start panel element
+ onSelectContainerElement: PropTypes.any,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ splitterSize: 5,
+ vert: true,
+ endPanelControl: false,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ /**
+ * The state stores whether or not the end panel should be controlled, the current
+ * orientation (vertical or horizontal), the splitter size, and the current size
+ * (width/height). All these values can change during the component's life time.
+ */
+ this.state = {
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: props.endPanelControl,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: props.vert,
+ // Size of the splitter handle bar.
+ splitterSize: props.splitterSize,
+ // Width of controlled panel.
+ width: props.initialWidth || props.initialSize,
+ // Height of controlled panel.
+ height: props.initialHeight || props.initialSize,
+ };
+
+ this.onStartMove = this.onStartMove.bind(this);
+ this.onStopMove = this.onStopMove.bind(this);
+ this.onMove = this.onMove.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { endPanelControl, splitterSize, vert } = nextProps;
+
+ if (endPanelControl != this.props.endPanelControl) {
+ this.setState({ endPanelControl });
+ }
+
+ if (splitterSize != this.props.splitterSize) {
+ this.setState({ splitterSize });
+ }
+
+ if (vert !== this.props.vert) {
+ this.setState({ vert });
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ nextState.width != this.state.width ||
+ nextState.endPanelControl != this.props.endPanelControl ||
+ nextState.height != this.state.height ||
+ nextState.vert != this.state.vert ||
+ nextState.splitterSize != this.state.splitterSize ||
+ nextProps.startPanel != this.props.startPanel ||
+ nextProps.endPanel != this.props.endPanel ||
+ nextProps.minSize != this.props.minSize ||
+ nextProps.maxSize != this.props.maxSize
+ );
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (
+ this.props.onControlledPanelResized &&
+ (prevState.width !== this.state.width ||
+ prevState.height !== this.state.height)
+ ) {
+ this.props.onControlledPanelResized(this.state.width, this.state.height);
+ }
+ }
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const doc = this.splitBox.ownerDocument;
+ const defaultCursor = doc.documentElement.style.cursor;
+ doc.documentElement.style.cursor = this.state.vert
+ ? "ew-resize"
+ : "ns-resize";
+
+ this.splitBox.classList.add("dragging");
+
+ this.setState({
+ defaultCursor,
+ });
+ }
+
+ onStopMove() {
+ const doc = this.splitBox.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+
+ this.splitBox.classList.remove("dragging");
+
+ if (this.props.onResizeEnd) {
+ this.props.onResizeEnd(
+ this.state.vert ? this.state.width : this.state.height
+ );
+ }
+ }
+
+ /**
+ * Adjust size of the controlled panel. Depending on the current
+ * orientation we either remember the width or height of
+ * the splitter box.
+ */
+ onMove(x, y) {
+ const nodeBounds = this.splitBox.getBoundingClientRect();
+
+ let size;
+ let { endPanelControl, vert } = this.state;
+
+ if (vert) {
+ // Use the document owning the SplitBox to detect rtl. The global document might be
+ // the one bound to the toolbox shared BrowserRequire, which is irrelevant here.
+ const doc = this.splitBox.ownerDocument;
+
+ // Switch the control flag in case of RTL. Note that RTL
+ // has impact on vertical splitter only.
+ if (doc.dir === "rtl") {
+ endPanelControl = !endPanelControl;
+ }
+
+ size = endPanelControl
+ ? nodeBounds.left + nodeBounds.width - x
+ : x - nodeBounds.left;
+
+ this.setState({
+ width: this.getConstrainedSizeInPx(size, nodeBounds.width),
+ });
+ } else {
+ size = endPanelControl
+ ? nodeBounds.top + nodeBounds.height - y
+ : y - nodeBounds.top;
+
+ this.setState({
+ height: this.getConstrainedSizeInPx(size, nodeBounds.height),
+ });
+ }
+ }
+
+ /**
+ * Calculates the constrained size taking into account the minimum width or
+ * height passed via this.props.minSize.
+ *
+ * @param {Number} requestedSize
+ * The requested size
+ * @param {Number} splitBoxWidthOrHeight
+ * The width or height of the splitBox
+ *
+ * @return {Number}
+ * The constrained size
+ */
+ getConstrainedSizeInPx(requestedSize, splitBoxWidthOrHeight) {
+ let minSize = this.props.minSize + "";
+
+ if (minSize.endsWith("%")) {
+ minSize = (parseFloat(minSize) / 100) * splitBoxWidthOrHeight;
+ } else if (minSize.endsWith("px")) {
+ minSize = parseFloat(minSize);
+ }
+ return Math.max(requestedSize, minSize);
+ }
+
+ // Rendering
+
+ // eslint-disable-next-line complexity
+ render() {
+ const { endPanelControl, splitterSize, vert } = this.state;
+ const {
+ startPanel,
+ startPanelCollapsed,
+ endPanel,
+ endPanelCollapsed,
+ minSize,
+ maxSize,
+ onSelectContainerElement,
+ } = this.props;
+
+ const style = Object.assign(
+ {
+ // Set the size of the controlled panel (height or width depending on the
+ // current state). This can be used to help with styling of dependent
+ // panels.
+ "--split-box-controlled-panel-size": `${
+ vert ? this.state.width : this.state.height
+ }`,
+ },
+ this.props.style
+ );
+
+ // Calculate class names list.
+ let classNames = ["split-box"];
+ classNames.push(vert ? "vert" : "horz");
+ if (this.props.className) {
+ classNames = classNames.concat(this.props.className.split(" "));
+ }
+
+ let leftPanelStyle;
+ let rightPanelStyle;
+
+ // Set proper size for panels depending on the current state.
+ if (vert) {
+ leftPanelStyle = {
+ maxWidth: endPanelControl ? null : maxSize,
+ minWidth: endPanelControl ? null : minSize,
+ width: endPanelControl ? null : this.state.width,
+ };
+ rightPanelStyle = {
+ maxWidth: endPanelControl ? maxSize : null,
+ minWidth: endPanelControl ? minSize : null,
+ width: endPanelControl ? this.state.width : null,
+ };
+ } else {
+ leftPanelStyle = {
+ maxHeight: endPanelControl ? null : maxSize,
+ minHeight: endPanelControl ? null : minSize,
+ height: endPanelControl ? null : this.state.height,
+ };
+ rightPanelStyle = {
+ maxHeight: endPanelControl ? maxSize : null,
+ minHeight: endPanelControl ? minSize : null,
+ height: endPanelControl ? this.state.height : null,
+ };
+ }
+
+ // Calculate splitter size
+ const splitterStyle = {
+ flex: "0 0 " + splitterSize + "px",
+ };
+
+ return dom.div(
+ {
+ className: classNames.join(" "),
+ ref: div => {
+ this.splitBox = div;
+ },
+ style,
+ },
+ startPanel && !startPanelCollapsed
+ ? dom.div(
+ {
+ className: endPanelControl ? "uncontrolled" : "controlled",
+ style: leftPanelStyle,
+ role: "presentation",
+ ref: div => {
+ this.startPanelContainer = div;
+ if (onSelectContainerElement) {
+ onSelectContainerElement(div);
+ }
+ },
+ },
+ startPanel
+ )
+ : null,
+ splitterSize > 0
+ ? Draggable({
+ className: "splitter",
+ style: splitterStyle,
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove,
+ })
+ : null,
+ endPanel && !endPanelCollapsed
+ ? dom.div(
+ {
+ className: endPanelControl ? "controlled" : "uncontrolled",
+ style: rightPanelStyle,
+ role: "presentation",
+ ref: div => {
+ this.endPanelContainer = div;
+ },
+ },
+ endPanel
+ )
+ : null
+ );
+ }
+}
+
+module.exports = SplitBox;
diff --git a/devtools/client/shared/components/splitter/moz.build b/devtools/client/shared/components/splitter/moz.build
new file mode 100644
index 0000000000..4abe762b34
--- /dev/null
+++ b/devtools/client/shared/components/splitter/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "Draggable.js",
+ "GridElementWidthResizer.js",
+ "SplitBox.js",
+)
diff --git a/devtools/client/shared/components/tabs/TabBar.js b/devtools/client/shared/components/tabs/TabBar.js
new file mode 100644
index 0000000000..730e8c7802
--- /dev/null
+++ b/devtools/client/shared/components/tabs/TabBar.js
@@ -0,0 +1,378 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+ createRef,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Sidebar = createFactory(
+ require("resource://devtools/client/shared/components/Sidebar.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "Menu",
+ "resource://devtools/client/framework/menu.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MenuItem",
+ "resource://devtools/client/framework/menu-item.js"
+);
+
+// Shortcuts
+const { div } = dom;
+
+/**
+ * Renders Tabbar component.
+ */
+class Tabbar extends Component {
+ static get propTypes() {
+ return {
+ children: PropTypes.array,
+ menuDocument: PropTypes.object,
+ onSelect: PropTypes.func,
+ showAllTabsMenu: PropTypes.bool,
+ allTabsMenuButtonTooltip: PropTypes.string,
+ activeTabId: PropTypes.string,
+ renderOnlySelected: PropTypes.bool,
+ sidebarToggleButton: PropTypes.shape({
+ // Set to true if collapsed.
+ collapsed: PropTypes.bool.isRequired,
+ // Tooltip text used when the button indicates expanded state.
+ collapsePaneTitle: PropTypes.string.isRequired,
+ // Tooltip text used when the button indicates collapsed state.
+ expandPaneTitle: PropTypes.string.isRequired,
+ // Click callback
+ onClick: PropTypes.func.isRequired,
+ // align toggle button to right
+ alignRight: PropTypes.bool,
+ // if set to true toggle-button rotate 90
+ canVerticalSplit: PropTypes.bool,
+ }),
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ menuDocument: window.parent.document,
+ showAllTabsMenu: false,
+ };
+ }
+
+ constructor(props, context) {
+ super(props, context);
+ const { activeTabId, children = [] } = props;
+ const tabs = this.createTabs(children);
+ const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
+
+ this.state = {
+ activeTab: activeTab === -1 ? 0 : activeTab,
+ tabs,
+ };
+
+ // Array of queued tabs to add to the Tabbar.
+ this.queuedTabs = [];
+
+ this.createTabs = this.createTabs.bind(this);
+ this.addTab = this.addTab.bind(this);
+ this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this);
+ this.queueTab = this.queueTab.bind(this);
+ this.toggleTab = this.toggleTab.bind(this);
+ this.removeTab = this.removeTab.bind(this);
+ this.select = this.select.bind(this);
+ this.getTabIndex = this.getTabIndex.bind(this);
+ this.getTabId = this.getTabId.bind(this);
+ this.getCurrentTabId = this.getCurrentTabId.bind(this);
+ this.onTabChanged = this.onTabChanged.bind(this);
+ this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
+ this.renderTab = this.renderTab.bind(this);
+ this.tabbarRef = createRef();
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { activeTabId, children = [] } = nextProps;
+ const tabs = this.createTabs(children);
+ const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
+
+ if (
+ activeTab !== this.state.activeTab ||
+ children !== this.props.children
+ ) {
+ this.setState({
+ activeTab: activeTab === -1 ? 0 : activeTab,
+ tabs,
+ });
+ }
+ }
+
+ createTabs(children) {
+ return children
+ .filter(panel => panel)
+ .map((panel, index) =>
+ Object.assign({}, children[index], {
+ id: panel.props.id || index,
+ panel,
+ title: panel.props.title,
+ })
+ );
+ }
+
+ // Public API
+
+ addTab(id, title, selected = false, panel, url, index = -1) {
+ const tabs = this.state.tabs.slice();
+
+ if (index >= 0) {
+ tabs.splice(index, 0, { id, title, panel, url });
+ } else {
+ tabs.push({ id, title, panel, url });
+ }
+
+ const newState = Object.assign({}, this.state, {
+ tabs,
+ });
+
+ if (selected) {
+ newState.activeTab = index >= 0 ? index : tabs.length - 1;
+ }
+
+ this.setState(newState, () => {
+ if (this.props.onSelect && selected) {
+ this.props.onSelect(id);
+ }
+ });
+ }
+
+ addAllQueuedTabs() {
+ if (!this.queuedTabs.length) {
+ return;
+ }
+
+ const tabs = this.state.tabs.slice();
+
+ // Preselect the first sidebar tab if none was explicitly selected.
+ let activeTab = 0;
+ let activeId = this.queuedTabs[0].id;
+
+ for (const { id, index, panel, selected, title, url } of this.queuedTabs) {
+ if (index >= 0) {
+ tabs.splice(index, 0, { id, title, panel, url });
+ } else {
+ tabs.push({ id, title, panel, url });
+ }
+
+ if (selected) {
+ activeId = id;
+ activeTab = index >= 0 ? index : tabs.length - 1;
+ }
+ }
+
+ const newState = Object.assign({}, this.state, {
+ activeTab,
+ tabs,
+ });
+
+ this.setState(newState, () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(activeId);
+ }
+ });
+
+ this.queuedTabs = [];
+ }
+
+ /**
+ * Queues a tab to be added. This is more performant than calling addTab for every
+ * single tab to be added since we will limit the number of renders happening when
+ * a new state is set. Once all the tabs to be added have been queued, call
+ * addAllQueuedTabs() to populate the TabBar with all the queued tabs.
+ */
+ queueTab(id, title, selected = false, panel, url, index = -1) {
+ this.queuedTabs.push({
+ id,
+ index,
+ panel,
+ selected,
+ title,
+ url,
+ });
+ }
+
+ toggleTab(tabId, isVisible) {
+ const index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ const tabs = this.state.tabs.slice();
+ tabs[index] = Object.assign({}, tabs[index], {
+ isVisible,
+ });
+
+ this.setState(
+ Object.assign({}, this.state, {
+ tabs,
+ })
+ );
+ }
+
+ removeTab(tabId) {
+ const index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ const tabs = this.state.tabs.slice();
+ tabs.splice(index, 1);
+
+ let activeTab = this.state.activeTab - 1;
+ activeTab = activeTab === -1 ? 0 : activeTab;
+
+ this.setState(
+ Object.assign({}, this.state, {
+ activeTab,
+ tabs,
+ }),
+ () => {
+ // Select the next active tab and force the select event handler to initialize
+ // the panel if needed.
+ if (tabs.length && this.props.onSelect) {
+ this.props.onSelect(this.getTabId(activeTab));
+ }
+ }
+ );
+ }
+
+ select(tabId) {
+ const docRef = this.tabbarRef.current.ownerDocument;
+
+ const index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ const newState = Object.assign({}, this.state, {
+ activeTab: index,
+ });
+
+ const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`);
+
+ if (tabDomElement) {
+ tabDomElement.scrollIntoView();
+ }
+
+ this.setState(newState, () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(tabId);
+ }
+ });
+ }
+
+ // Helpers
+
+ getTabIndex(tabId) {
+ let tabIndex = -1;
+ this.state.tabs.forEach((tab, index) => {
+ if (tab.id === tabId) {
+ tabIndex = index;
+ }
+ });
+ return tabIndex;
+ }
+
+ getTabId(index) {
+ return this.state.tabs[index].id;
+ }
+
+ getCurrentTabId() {
+ return this.state.tabs[this.state.activeTab].id;
+ }
+
+ // Event Handlers
+
+ onTabChanged(index) {
+ this.setState(
+ {
+ activeTab: index,
+ },
+ () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(this.state.tabs[index].id);
+ }
+ }
+ );
+ }
+
+ onAllTabsMenuClick(event) {
+ const menu = new Menu();
+ const target = event.target;
+
+ // Generate list of menu items from the list of tabs.
+ this.state.tabs.forEach(tab => {
+ menu.append(
+ new MenuItem({
+ label: tab.title,
+ type: "checkbox",
+ checked: this.getCurrentTabId() === tab.id,
+ click: () => this.select(tab.id),
+ })
+ );
+ });
+
+ // Show a drop down menu with frames.
+ menu.popupAtTarget(target);
+
+ return menu;
+ }
+
+ // Rendering
+
+ renderTab(tab) {
+ if (typeof tab.panel === "function") {
+ return tab.panel({
+ key: tab.id,
+ title: tab.title,
+ id: tab.id,
+ url: tab.url,
+ });
+ }
+
+ return tab.panel;
+ }
+
+ render() {
+ const tabs = this.state.tabs.map(tab => this.renderTab(tab));
+
+ return div(
+ {
+ className: "devtools-sidebar-tabs",
+ ref: this.tabbarRef,
+ },
+ Sidebar(
+ {
+ onAllTabsMenuClick: this.onAllTabsMenuClick,
+ renderOnlySelected: this.props.renderOnlySelected,
+ showAllTabsMenu: this.props.showAllTabsMenu,
+ allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip,
+ sidebarToggleButton: this.props.sidebarToggleButton,
+ activeTab: this.state.activeTab,
+ onAfterChange: this.onTabChanged,
+ },
+ tabs
+ )
+ );
+ }
+}
+
+module.exports = Tabbar;
diff --git a/devtools/client/shared/components/tabs/Tabs.css b/devtools/client/shared/components/tabs/Tabs.css
new file mode 100644
index 0000000000..876088b667
--- /dev/null
+++ b/devtools/client/shared/components/tabs/Tabs.css
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Tabs General Styles */
+
+.tabs {
+ --tab-height: var(--theme-toolbar-height);
+ height: 100%;
+ background: var(--theme-sidebar-background);
+ display: flex;
+ flex-direction: column;
+}
+
+.tabs.tabs-tall {
+ --tab-height: var(--theme-toolbar-tall-height);
+}
+
+/* Hides the tab strip in the TabBar */
+div[hidetabs=true] .tabs .tabs-navigation {
+ display: none;
+}
+
+.tabs .tabs-navigation {
+ box-sizing: border-box;
+ display: flex;
+ /* Reserve 1px for the border */
+ height: calc(var(--tab-height) + 1px);
+ position: relative;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background: var(--theme-tab-toolbar-background);
+}
+
+.tabs .tabs-menu {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ margin-inline-end: 15px;
+ flex-grow: 1;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+ should stretch accordingly. Use flexbox for the behavior.
+ Use also `overflow: hidden` so, 'overflow' and 'underflow'
+ events are fired (it's utilized by the all-tabs-menu). */
+.tabs .tabs-navigation .tabs-menu {
+ overflow: hidden;
+ display: flex;
+ overflow-x: scroll;
+ scrollbar-width: none;
+}
+
+.tabs .tabs-menu-item {
+ display: inline-block;
+ position: relative;
+ margin: 0;
+ padding: 0;
+ color: var(--theme-toolbar-color);
+}
+
+.tabs .tabs-menu-item.is-active {
+ color: var(--theme-toolbar-selected-color);
+}
+
+.tabs .tabs-menu-item:hover {
+ background-color: var(--theme-toolbar-hover);
+}
+
+.tabs .tabs-menu-item:hover:active:not(.is-active) {
+ background-color: var(--theme-toolbar-hover-active);
+}
+
+.tabs .tabs-menu-item a {
+ --text-height: 16px;
+ --devtools-tab-border-width: 1px;
+ display: flex;
+ justify-content: center;
+ /* Vertically center text, calculate space remaining by taking the full height and removing
+ the block borders and text. Divide by 2 to distribute above and below. */
+ padding: calc((var(--tab-height) - var(--text-height) - (var(--devtools-tab-border-width) * 2)) / 2) 10px;
+ border: var(--devtools-tab-border-width) solid transparent;
+ font-size: 12px;
+ line-height: var(--text-height);
+ text-decoration: none;
+ white-space: nowrap;
+ cursor: default;
+ user-select: none;
+ text-align: center;
+}
+
+/* Remove the outline focusring from tabs-menu-item. */
+.tabs .tabs-navigation .tabs-menu-item > a:-moz-focusring {
+ outline: none;
+}
+
+.tabs .tabs-menu-item .tab-badge {
+ color: var(--theme-highlight-blue);
+ font-size: 80%;
+ font-style: italic;
+ /* Tabs have a 15px padding start/end, so we set the margins here in order to center the
+ badge after the tab title, with a 5px effective margin. */
+ margin-inline-start: 5px;
+ margin-inline-end: -10px;
+}
+
+.tabs .tabs-menu-item.is-active .tab-badge {
+ /* Use the same color as the tab-item when active */
+ color: inherit;
+}
+
+/* To avoid "select all" commands from selecting content in hidden tabs */
+.tabs .hidden,
+.tabs .hidden * {
+ user-select: none !important;
+}
+
+/* Make sure panel content takes entire vertical space. */
+.tabs .panels {
+ flex: 1;
+ overflow: hidden;
+}
+
+.tabs .tab-panel {
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
diff --git a/devtools/client/shared/components/tabs/Tabs.js b/devtools/client/shared/components/tabs/Tabs.js
new file mode 100644
index 0000000000..a265032f9e
--- /dev/null
+++ b/devtools/client/shared/components/tabs/Tabs.js
@@ -0,0 +1,468 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+define(function (require, exports, module) {
+ const {
+ Component,
+ createRef,
+ } = require("devtools/client/shared/vendor/react");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ /**
+ * Renders simple 'tab' widget.
+ *
+ * Based on ReactSimpleTabs component
+ * https://github.com/pedronauck/react-simpletabs
+ *
+ * Component markup (+CSS) example:
+ *
+ * <div class='tabs'>
+ * <nav class='tabs-navigation'>
+ * <ul class='tabs-menu'>
+ * <li class='tabs-menu-item is-active'>Tab #1</li>
+ * <li class='tabs-menu-item'>Tab #2</li>
+ * </ul>
+ * </nav>
+ * <div class='panels'>
+ * The content of active panel here
+ * </div>
+ * <div>
+ */
+ class Tabs extends Component {
+ static get propTypes() {
+ return {
+ className: PropTypes.oneOfType([
+ PropTypes.array,
+ PropTypes.string,
+ PropTypes.object,
+ ]),
+ activeTab: PropTypes.number,
+ onMount: PropTypes.func,
+ onBeforeChange: PropTypes.func,
+ onAfterChange: PropTypes.func,
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.element])
+ .isRequired,
+ showAllTabsMenu: PropTypes.bool,
+ allTabsMenuButtonTooltip: PropTypes.string,
+ onAllTabsMenuClick: PropTypes.func,
+ tall: PropTypes.bool,
+
+ // To render a sidebar toggle button before the tab menu provide a function that
+ // returns a React component for the button.
+ renderSidebarToggle: PropTypes.func,
+ // Set true will only render selected panel on DOM. It's complete
+ // opposite of the created array, and it's useful if panels content
+ // is unpredictable and update frequently.
+ renderOnlySelected: PropTypes.bool,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ activeTab: 0,
+ showAllTabsMenu: false,
+ renderOnlySelected: false,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ activeTab: props.activeTab,
+
+ // This array is used to store an object containing information on whether a tab
+ // at a specified index has already been created (e.g. selected at least once) and
+ // the tab id. An example of the object structure is the following:
+ // [{ isCreated: true, tabId: "ruleview" }, { isCreated: false, tabId: "foo" }].
+ // If the tab at the specified index has already been created, it's rendered even
+ // if not currently selected. This is because in some cases we don't want
+ // to re-create tab content when it's being unselected/selected.
+ // E.g. in case of an iframe being used as a tab-content we want the iframe to
+ // stay in the DOM.
+ created: [],
+
+ // True if tabs can't fit into available horizontal space.
+ overflow: false,
+ };
+
+ this.tabsEl = createRef();
+
+ this.onOverflow = this.onOverflow.bind(this);
+ this.onUnderflow = this.onUnderflow.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onClickTab = this.onClickTab.bind(this);
+ this.setActive = this.setActive.bind(this);
+ this.renderMenuItems = this.renderMenuItems.bind(this);
+ this.renderPanels = this.renderPanels.bind(this);
+ }
+
+ componentDidMount() {
+ const node = this.tabsEl.current;
+ node.addEventListener("keydown", this.onKeyDown);
+
+ // Register overflow listeners to manage visibility
+ // of all-tabs-menu. This menu is displayed when there
+ // is not enough h-space to render all tabs.
+ // It allows the user to select a tab even if it's hidden.
+ if (this.props.showAllTabsMenu) {
+ node.addEventListener("overflow", this.onOverflow);
+ node.addEventListener("underflow", this.onUnderflow);
+ }
+
+ const index = this.state.activeTab;
+ if (this.props.onMount) {
+ this.props.onMount(index);
+ }
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ let { children, activeTab } = nextProps;
+ const panels = children.filter(panel => panel);
+ let created = [...this.state.created];
+
+ // If the children props has changed due to an addition or removal of a tab,
+ // update the state's created array with the latest tab ids and whether or not
+ // the tab is already created.
+ if (this.state.created.length != panels.length) {
+ created = panels.map(panel => {
+ // Get whether or not the tab has already been created from the previous state.
+ const createdEntry = this.state.created.find(entry => {
+ return entry && entry.tabId === panel.props.id;
+ });
+ const isCreated = !!createdEntry && createdEntry.isCreated;
+ const tabId = panel.props.id;
+
+ return {
+ isCreated,
+ tabId,
+ };
+ });
+ }
+
+ // Check type of 'activeTab' props to see if it's valid (it's 0-based index).
+ if (typeof activeTab === "number") {
+ // Reset to index 0 if index overflows the range of panel array
+ activeTab = activeTab < panels.length && activeTab >= 0 ? activeTab : 0;
+
+ created[activeTab] = Object.assign({}, created[activeTab], {
+ isCreated: true,
+ });
+
+ this.setState({
+ activeTab,
+ });
+ }
+
+ this.setState({
+ created,
+ });
+ }
+
+ componentWillUnmount() {
+ const node = this.tabsEl.current;
+ node.removeEventListener("keydown", this.onKeyDown);
+
+ if (this.props.showAllTabsMenu) {
+ node.removeEventListener("overflow", this.onOverflow);
+ node.removeEventListener("underflow", this.onUnderflow);
+ }
+ }
+
+ // DOM Events
+
+ onOverflow(event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: true,
+ });
+ }
+ }
+
+ onUnderflow(event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: false,
+ });
+ }
+ }
+
+ onKeyDown(event) {
+ // Bail out if the focus isn't on a tab.
+ if (!event.target.closest(".tabs-menu-item")) {
+ return;
+ }
+
+ let activeTab = this.state.activeTab;
+ const tabCount = this.props.children.length;
+
+ const ltr = event.target.ownerDocument.dir == "ltr";
+ const nextOrLastTab = Math.min(tabCount - 1, activeTab + 1);
+ const previousOrFirstTab = Math.max(0, activeTab - 1);
+
+ switch (event.code) {
+ case "ArrowRight":
+ if (ltr) {
+ activeTab = nextOrLastTab;
+ } else {
+ activeTab = previousOrFirstTab;
+ }
+ break;
+ case "ArrowLeft":
+ if (ltr) {
+ activeTab = previousOrFirstTab;
+ } else {
+ activeTab = nextOrLastTab;
+ }
+ break;
+ }
+
+ if (this.state.activeTab != activeTab) {
+ this.setActive(activeTab);
+ }
+ }
+
+ onClickTab(index, event) {
+ this.setActive(index);
+
+ if (event) {
+ event.preventDefault();
+ }
+ }
+
+ onMouseDown(event) {
+ // Prevents click-dragging the tab headers
+ if (event) {
+ event.preventDefault();
+ }
+ }
+
+ // API
+
+ setActive(index) {
+ const onAfterChange = this.props.onAfterChange;
+ const onBeforeChange = this.props.onBeforeChange;
+
+ if (onBeforeChange) {
+ const cancel = onBeforeChange(index);
+ if (cancel) {
+ return;
+ }
+ }
+
+ const created = [...this.state.created];
+ created[index] = Object.assign({}, created[index], {
+ isCreated: true,
+ });
+
+ const newState = Object.assign({}, this.state, {
+ created,
+ activeTab: index,
+ });
+
+ this.setState(newState, () => {
+ // Properly set focus on selected tab.
+ const selectedTab = this.tabsEl.current.querySelector(".is-active > a");
+ if (selectedTab) {
+ selectedTab.focus();
+ }
+
+ if (onAfterChange) {
+ onAfterChange(index);
+ }
+ });
+ }
+
+ // Rendering
+
+ renderMenuItems() {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ const tabs = this.props.children
+ .map(tab => (typeof tab === "function" ? tab() : tab))
+ .filter(tab => tab)
+ .map((tab, index) => {
+ const {
+ id,
+ className: tabClassName,
+ title,
+ badge,
+ showBadge,
+ } = tab.props;
+
+ const ref = "tab-menu-" + index;
+ const isTabSelected = this.state.activeTab === index;
+
+ const className = [
+ "tabs-menu-item",
+ tabClassName,
+ isTabSelected ? "is-active" : "",
+ ].join(" ");
+
+ // Set tabindex to -1 (except the selected tab) so, it's focusable,
+ // but not reachable via sequential tab-key navigation.
+ // Changing selected tab (and so, moving focus) is done through
+ // left and right arrow keys.
+ // See also `onKeyDown()` event handler.
+ return dom.li(
+ {
+ className,
+ key: index,
+ ref,
+ role: "presentation",
+ },
+ dom.span({ className: "devtools-tab-line" }),
+ dom.a(
+ {
+ id: id ? id + "-tab" : "tab-" + index,
+ tabIndex: isTabSelected ? 0 : -1,
+ title,
+ "aria-controls": id ? id + "-panel" : "panel-" + index,
+ "aria-selected": isTabSelected,
+ role: "tab",
+ onClick: this.onClickTab.bind(this, index),
+ onMouseDown: this.onMouseDown.bind(this),
+ "data-tab-index": index,
+ },
+ title,
+ badge && !isTabSelected && showBadge()
+ ? dom.span({ className: "tab-badge" }, badge)
+ : null
+ )
+ );
+ });
+
+ // Display the menu only if there is not enough horizontal
+ // space for all tabs (and overflow happened).
+ const allTabsMenu = this.state.overflow
+ ? dom.button({
+ className: "all-tabs-menu",
+ title: this.props.allTabsMenuButtonTooltip,
+ onClick: this.props.onAllTabsMenuClick,
+ })
+ : null;
+
+ // Get the sidebar toggle button if a renderSidebarToggle function is provided.
+ const sidebarToggle = this.props.renderSidebarToggle
+ ? this.props.renderSidebarToggle()
+ : null;
+
+ return dom.nav(
+ { className: "tabs-navigation" },
+ sidebarToggle,
+ dom.ul({ className: "tabs-menu", role: "tablist" }, tabs),
+ allTabsMenu
+ );
+ }
+
+ renderPanels() {
+ let { children, renderOnlySelected } = this.props;
+
+ if (!children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(children)) {
+ children = [children];
+ }
+
+ const selectedIndex = this.state.activeTab;
+
+ const panels = children
+ .map(tab => (typeof tab === "function" ? tab() : tab))
+ .filter(tab => tab)
+ .map((tab, index) => {
+ const selected = selectedIndex === index;
+ if (renderOnlySelected && !selected) {
+ return null;
+ }
+
+ const id = tab.props.id;
+ const isCreated =
+ this.state.created[index] && this.state.created[index].isCreated;
+
+ // Use 'visibility:hidden' + 'height:0' for hiding content of non-selected
+ // tab. It's faster than 'display:none' because it avoids triggering frame
+ // destruction and reconstruction. 'width' is not changed to avoid relayout.
+ const style = {
+ visibility: selected ? "visible" : "hidden",
+ height: selected ? "100%" : "0",
+ };
+
+ // Allows lazy loading panels by creating them only if they are selected,
+ // then store a copy of the lazy created panel in `tab.panel`.
+ if (typeof tab.panel == "function" && selected) {
+ tab.panel = tab.panel(tab);
+ }
+ const panel = tab.panel || tab;
+
+ return dom.div(
+ {
+ id: id ? id + "-panel" : "panel-" + index,
+ key: id,
+ style,
+ className: selected ? "tab-panel-box" : "tab-panel-box hidden",
+ role: "tabpanel",
+ "aria-labelledby": id ? id + "-tab" : "tab-" + index,
+ },
+ selected || isCreated ? panel : null
+ );
+ });
+
+ return dom.div({ className: "panels" }, panels);
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: [
+ "tabs",
+ ...(this.props.tall ? ["tabs-tall"] : []),
+ this.props.className,
+ ].join(" "),
+ ref: this.tabsEl,
+ },
+ this.renderMenuItems(),
+ this.renderPanels()
+ );
+ }
+ }
+
+ /**
+ * Renders simple tab 'panel'.
+ */
+ class Panel extends Component {
+ static get propTypes() {
+ return {
+ id: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.element])
+ .isRequired,
+ };
+ }
+
+ render() {
+ const { className } = this.props;
+ return dom.div(
+ { className: `tab-panel ${className || ""}` },
+ this.props.children
+ );
+ }
+ }
+
+ // Exports from this module
+ exports.TabPanel = Panel;
+ exports.Tabs = Tabs;
+});
diff --git a/devtools/client/shared/components/tabs/moz.build b/devtools/client/shared/components/tabs/moz.build
new file mode 100644
index 0000000000..15ede75b9d
--- /dev/null
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "TabBar.js",
+ "Tabs.js",
+)
diff --git a/devtools/client/shared/components/test/browser/browser.ini b/devtools/client/shared/components/test/browser/browser.ini
new file mode 100644
index 0000000000..619662db8f
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_notification_box_basic.js]
+[browser_reps_stubs.js]
diff --git a/devtools/client/shared/components/test/browser/browser_notification_box_basic.js b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
new file mode 100644
index 0000000000..0103c677d2
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const TEST_URI = "data:text/html;charset=utf-8,Test page";
+
+/**
+ * Basic test that checks existence of the Notification box.
+ */
+add_task(async function () {
+ info("Test Notification box basic started");
+
+ const toolbox = await openNewTabAndToolbox(TEST_URI, "webconsole");
+
+ // Append a notification
+ const notificationBox = toolbox.getNotificationBox();
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ notificationBox.PRIORITY_INFO_HIGH
+ );
+
+ // Verify existence of one notification.
+ const parentNode = toolbox.doc.getElementById("toolbox-notificationbox");
+ const nodes = parentNode.querySelectorAll(".notification");
+ is(nodes.length, 1, "There must be one notification");
+});
diff --git a/devtools/client/shared/components/test/browser/browser_reps_stubs.js b/devtools/client/shared/components/test/browser/browser_reps_stubs.js
new file mode 100644
index 0000000000..5f5b8f3e77
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_reps_stubs.js
@@ -0,0 +1,347 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const TEST_URI = "data:text/html;charset=utf-8,stub generation";
+/**
+ * A Map keyed by filename, and for which the value is also a Map, with the key being the
+ * label for the stub, and the value the expression to evaluate to get the stub.
+ */
+const EXPRESSIONS_BY_FILE = {
+ "attribute.js": new Map([
+ [
+ "Attribute",
+ `{
+ const a = document.createAttribute("class")
+ a.value = "autocomplete-suggestions";
+ a;
+ }`,
+ ],
+ ]),
+ "comment-node.js": new Map([
+ [
+ "Comment",
+ `{
+ document.createComment("test\\nand test\\nand test\\nand test\\nand test\\nand test\\nand test")
+ }`,
+ ],
+ ]),
+ "date-time.js": new Map([
+ ["DateTime", `new Date(1459372644859)`],
+ ["InvalidDateTime", `new Date("invalid")`],
+ ]),
+ "infinity.js": new Map([
+ ["Infinity", `Infinity`],
+ ["NegativeInfinity", `-Infinity`],
+ ]),
+ "nan.js": new Map([["NaN", `2 * document`]]),
+ "null.js": new Map([["Null", `null`]]),
+ "number.js": new Map([
+ ["Int", `2 + 3`],
+ ["True", `true`],
+ ["False", `false`],
+ ["NegZeroGrip", `1 / -Infinity`],
+ ]),
+ "stylesheet.js": new Map([
+ [
+ "StyleSheet",
+ {
+ expression: `
+ (async function() {
+ const link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.type = "text/css";
+ link.href = "https://example.com/styles.css";
+ const onStylesheetHandled = new Promise(res => {
+ // The file does not exist so we'll get an error event, but it will
+ // still be put in document.styleSheets with its src, which is what we want.
+ link.addEventListener("error", () => res(), { once: true});
+ })
+ document.head.appendChild(link);
+ await onStylesheetHandled;
+ return document.styleSheets[0];
+ })()
+ `,
+ async: true,
+ },
+ ],
+ ]),
+ "symbol.js": new Map([
+ ["Symbol", `Symbol("foo")`],
+ ["SymbolWithoutIdentifier", `Symbol()`],
+ ["SymbolWithLongString", `Symbol("aa".repeat(10000))`],
+ ]),
+ "text-node.js": new Map([
+ [
+ "testRendering",
+ `let tn = document.createTextNode("hello world");
+ document.body.append(tn);
+ tn;`,
+ ],
+ ["testRenderingDisconnected", `document.createTextNode("hello world")`],
+ ["testRenderingWithEOL", `document.createTextNode("hello\\nworld")`],
+ ["testRenderingWithDoubleQuote", `document.createTextNode('hello"world')`],
+ [
+ "testRenderingWithLongString",
+ `document.createTextNode("a\\n" + ("a").repeat(20000))`,
+ ],
+ ]),
+ "undefined.js": new Map([["Undefined", `undefined`]]),
+ "window.js": new Map([["Window", `window`]]),
+ // XXX: File a bug blocking Bug 1671400 for enabling automatic generation for one of
+ // the following file.
+ // "accessible.js",
+ // "accessor.js",
+ // "big-int.js",
+ // "document-type.js",
+ // "document.js",
+ // "element-node.js",
+ // "error.js",
+ // "event.js",
+ // "failure.js",
+ // "function.js",
+ // "grip-array.js",
+ // "grip-entry.js",
+ // "grip-map.js",
+ // "grip.js",
+ // "long-string.js",
+ // "object-with-text.js",
+ // "object-with-url.js",
+ // "promise.js",
+ // "regexp.js",
+};
+
+add_task(async function () {
+ const isStubsUpdate = Services.env.get(STUBS_UPDATE_ENV) == "true";
+
+ const tab = await addTab(TEST_URI);
+ const {
+ CommandsFactory,
+ } = require("devtools/shared/commands/commands-factory");
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ let failed = false;
+ for (const stubFile of Object.keys(EXPRESSIONS_BY_FILE)) {
+ info(`${isStubsUpdate ? "Update" : "Check"} ${stubFile}`);
+
+ const generatedStubs = await generateStubs(commands, stubFile);
+ if (isStubsUpdate) {
+ await writeStubsToFile(stubFile, generatedStubs);
+ ok(true, `${stubFile} was updated`);
+ continue;
+ }
+
+ const existingStubs = getStubFile(stubFile);
+ if (generatedStubs.size !== existingStubs.size) {
+ failed = true;
+ continue;
+ }
+
+ for (const [key, packet] of generatedStubs) {
+ const packetStr = getSerializedPacket(packet, {
+ sortKeys: true,
+ replaceActorIds: true,
+ });
+ const grip = getSerializedPacket(existingStubs.get(key), {
+ sortKeys: true,
+ replaceActorIds: true,
+ });
+ is(packetStr, grip, `"${key}" packet has expected value`);
+ failed = failed || packetStr !== grip;
+ }
+ }
+
+ if (failed) {
+ ok(
+ false,
+ "The reps stubs need to be updated by running `" +
+ `mach test ${getCurrentTestFilePath()} --headless --setenv STUBS_UPDATE=true` +
+ "`"
+ );
+ } else {
+ ok(true, "Stubs are up to date");
+ }
+
+ await removeTab(tab);
+});
+
+async function generateStubs(commands, stubFile) {
+ const stubs = new Map();
+
+ for (const [key, options] of EXPRESSIONS_BY_FILE[stubFile]) {
+ const expression =
+ typeof options == "string" ? options : options.expression;
+ const executeOptions = {};
+ if (options.async === true) {
+ executeOptions.mapped = { await: true };
+ }
+ const { result } = await commands.scriptCommand.execute(
+ expression,
+ executeOptions
+ );
+ stubs.set(key, getCleanedPacket(stubFile, key, result));
+ }
+
+ return stubs;
+}
+
+function getCleanedPacket(stubFile, key, packet) {
+ // Remove the targetFront property that has a cyclical reference and that we don't need
+ // in our node tests.
+ delete packet.targetFront;
+
+ const existingStubs = getStubFile(stubFile);
+ if (!existingStubs) {
+ return packet;
+ }
+
+ // Strip escaped characters.
+ const safeKey = key
+ .replace(/\\n/g, "\n")
+ .replace(/\\r/g, "\r")
+ .replace(/\\\"/g, `\"`)
+ .replace(/\\\'/g, `\'`);
+ if (!existingStubs.has(safeKey)) {
+ return packet;
+ }
+
+ // If the stub already exist, we want to ignore irrelevant properties (generated id, timer, …)
+ // that might changed and "pollute" the diff resulting from this stub generation.
+ const existingPacket = existingStubs.get(safeKey);
+
+ // copy existing contentDomReference
+ if (
+ packet._grip?.contentDomReference?.id &&
+ existingPacket._grip?.contentDomReference?.id
+ ) {
+ packet._grip.contentDomReference = existingPacket._grip.contentDomReference;
+ }
+
+ // `window`'s properties count can vary from OS to OS, so we clean `ownPropertyLength`.
+ if (
+ existingPacket &&
+ packet._grip?.class === "Window" &&
+ typeof packet._grip.ownPropertyLength ==
+ typeof existingPacket._grip.ownPropertyLength
+ ) {
+ packet._grip.ownPropertyLength = existingPacket._grip.ownPropertyLength;
+ }
+
+ return packet;
+}
+
+// HELPER
+
+const CHROME_PREFIX = "chrome://mochitests/content/browser/";
+const STUBS_FOLDER = "devtools/client/shared/components/test/node/stubs/reps/";
+const STUBS_UPDATE_ENV = "STUBS_UPDATE";
+
+/**
+ * Write stubs to a given file
+ *
+ * @param {String} fileName: The file to write the stubs in.
+ * @param {Map} packets: A Map of the packets.
+ */
+async function writeStubsToFile(fileName, packets) {
+ const mozRepo = Services.env.get("MOZ_DEVELOPER_REPO_DIR");
+ const filePath = `${mozRepo}/${STUBS_FOLDER + fileName}`;
+
+ const stubs = Array.from(packets.entries()).map(([key, packet]) => {
+ const stringifiedPacket = getSerializedPacket(packet);
+ return `stubs.set(\`${key}\`, ${stringifiedPacket});`;
+ });
+
+ const fileContent = `/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+${stubs.join("\n\n")}
+
+module.exports = stubs;
+`;
+
+ const textEncoder = new TextEncoder();
+ await IOUtils.write(filePath, textEncoder.encode(fileContent));
+}
+
+function getStubFile(fileName) {
+ return require(CHROME_PREFIX + STUBS_FOLDER + fileName);
+}
+
+function sortObjectKeys(obj) {
+ const isArray = Array.isArray(obj);
+ const isObject = Object.prototype.toString.call(obj) === "[object Object]";
+ const isFront = obj?._grip;
+
+ if (isObject && !isFront) {
+ // Reorder keys for objects, but skip fronts to avoid infinite recursion.
+ const sortedKeys = Object.keys(obj).sort((k1, k2) => k1.localeCompare(k2));
+ const withSortedKeys = {};
+ sortedKeys.forEach(k => {
+ withSortedKeys[k] = k !== "stacktrace" ? sortObjectKeys(obj[k]) : obj[k];
+ });
+ return withSortedKeys;
+ } else if (isArray) {
+ return obj.map(item => sortObjectKeys(item));
+ }
+ return obj;
+}
+
+/**
+ * @param {Object} packet
+ * The packet to serialize.
+ * @param {Object} options
+ * @param {Boolean} options.sortKeys
+ * Pass true to sort all keys alphabetically in the packet before serialization.
+ * For instance stub comparison should not fail if the order of properties changed.
+ * @param {Boolean} options.replaceActorIds
+ * Pass true to replace actorIDs with a fake one so it's easier to compare stubs
+ * that includes grips.
+ */
+function getSerializedPacket(
+ packet,
+ { sortKeys = false, replaceActorIds = false } = {}
+) {
+ if (sortKeys) {
+ packet = sortObjectKeys(packet);
+ }
+
+ const actorIdPlaceholder = "XXX";
+
+ return JSON.stringify(
+ packet,
+ function (key, value) {
+ // The message can have fronts that we need to serialize
+ if (value && value._grip) {
+ return {
+ _grip: value._grip,
+ actorID: replaceActorIds ? actorIdPlaceholder : value.actorID,
+ };
+ }
+
+ if (
+ replaceActorIds &&
+ (key === "actor" || key === "actorID" || key === "sourceId") &&
+ typeof value === "string"
+ ) {
+ return actorIdPlaceholder;
+ }
+
+ return value;
+ },
+ 2
+ );
+}
diff --git a/devtools/client/shared/components/test/chrome/accordion.snapshots.js b/devtools/client/shared/components/test/chrome/accordion.snapshots.js
new file mode 100644
index 0000000000..0f649b52d6
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/accordion.snapshots.js
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+window._snapshots = {
+ "Accordion basic render.": {
+ type: "ul",
+ props: {
+ className: "accordion",
+ tabIndex: -1,
+ },
+ children: [
+ {
+ type: "li",
+ props: {
+ id: "accordion-item-1",
+ className: "accordion-item",
+ "aria-labelledby": "accordion-item-1-header",
+ },
+ children: [
+ {
+ type: "h2",
+ props: {
+ id: "accordion-item-1-header",
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": false,
+ "aria-label": "Test Accordion Item 1",
+ onKeyDown: "event => this.onHeaderKeyDown(event, item)",
+ onClick: "event => this.onHeaderClick(event, item)",
+ },
+ children: [
+ {
+ type: "span",
+ props: {
+ className: "theme-twisty",
+ role: "presentation",
+ },
+ children: null,
+ },
+ {
+ type: "span",
+ props: { className: "accordion-header-label" },
+ children: ["Test Accordion Item 1"],
+ },
+ ],
+ },
+ {
+ type: "div",
+ props: {
+ className: "accordion-content",
+ hidden: true,
+ role: "presentation",
+ },
+ children: null,
+ },
+ ],
+ },
+ {
+ type: "li",
+ props: {
+ id: "accordion-item-2",
+ className: "accordion-item",
+ "aria-labelledby": "accordion-item-2-header",
+ },
+ children: [
+ {
+ type: "h2",
+ props: {
+ id: "accordion-item-2-header",
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": false,
+ "aria-label": "Test Accordion Item 2",
+ onKeyDown: "event => this.onHeaderKeyDown(event, item)",
+ onClick: "event => this.onHeaderClick(event, item)",
+ },
+ children: [
+ {
+ type: "span",
+ props: {
+ className: "theme-twisty",
+ role: "presentation",
+ },
+ children: null,
+ },
+ {
+ type: "span",
+ props: { className: "accordion-header-label" },
+ children: ["Test Accordion Item 2"],
+ },
+ {
+ type: "span",
+ props: {
+ className: "accordion-header-buttons",
+ role: "presentation",
+ },
+ children: [
+ {
+ type: "button",
+ props: {},
+ children: null,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "div",
+ props: {
+ className: "accordion-content",
+ hidden: true,
+ role: "presentation",
+ },
+ children: null,
+ },
+ ],
+ },
+ {
+ type: "li",
+ props: {
+ id: "accordion-item-3",
+ className: "accordion-item accordion-open",
+ "aria-labelledby": "accordion-item-3-header",
+ },
+ children: [
+ {
+ type: "h2",
+ props: {
+ id: "accordion-item-3-header",
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": true,
+ "aria-label": "Test Accordion Item 3",
+ onKeyDown: "event => this.onHeaderKeyDown(event, item)",
+ onClick: "event => this.onHeaderClick(event, item)",
+ },
+ children: [
+ {
+ type: "span",
+ props: {
+ className: "theme-twisty open",
+ role: "presentation",
+ },
+ children: null,
+ },
+ {
+ type: "span",
+ props: {
+ className: "accordion-header-label",
+ },
+ children: ["Test Accordion Item 3"],
+ },
+ ],
+ },
+ {
+ type: "div",
+ props: {
+ className: "accordion-content",
+ hidden: false,
+ role: "presentation",
+ },
+ children: [
+ {
+ type: "div",
+ props: {},
+ children: null,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+};
diff --git a/devtools/client/shared/components/test/chrome/chrome.ini b/devtools/client/shared/components/test/chrome/chrome.ini
new file mode 100644
index 0000000000..4d5a480ab7
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/chrome.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+support-files =
+ head.js
+ accordion.snapshots.js
+
+[test_accordion.html]
+[test_frame_01.html]
+[test_frame_02.html]
+[test_GridElementWidthResizer.html]
+[test_GridElementWidthResizer_RTL.html]
+[test_HSplitBox_01.html]
+[test_list.html]
+[test_list_keyboard.html]
+[test_notification_box_01.html]
+[test_notification_box_02.html]
+[test_notification_box_03.html]
+[test_notification_box_04.html]
+[test_notification_box_05.html]
+[test_searchbox.html]
+[test_searchbox-with-autocomplete.html]
+[test_sidebar_toggle.html]
+[test_smart-trace-grouping.html]
+[test_smart-trace-source-maps.html]
+[test_smart-trace.html]
+[test_stack-trace.html]
+[test_stack-trace-source-maps.html]
+[test_tabs_accessibility.html]
+[test_tabs_menu.html]
+[test_tree_01.html]
+[test_tree_02.html]
+[test_tree_03.html]
+[test_tree_04.html]
+[test_tree_05.html]
+[test_tree_06.html]
+[test_tree_07.html]
+[test_tree_08.html]
+[test_tree_09.html]
+[test_tree_10.html]
+[test_tree_11.html]
+[test_tree_12.html]
+[test_tree_13.html]
+[test_tree_14.html]
+[test_tree_15.html]
+[test_tree_16.html]
+[test_tree-view_01.html]
+[test_tree-view_02.html]
diff --git a/devtools/client/shared/components/test/chrome/head.js b/devtools/client/shared/components/test/chrome/head.js
new file mode 100644
index 0000000000..7abe54942f
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/head.js
@@ -0,0 +1,379 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+/* global _snapshots */
+
+"use strict";
+
+var { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+var { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+var { gDevTools } = require("resource://devtools/client/framework/devtools.js");
+var { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+var {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+var { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
+
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window,
+});
+
+const React = browserRequire("devtools/client/shared/vendor/react");
+const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+const dom = browserRequire("devtools/client/shared/vendor/react-dom-factories");
+const TestUtils = browserRequire(
+ "devtools/client/shared/vendor/react-dom-test-utils"
+);
+
+const ShallowRenderer = browserRequire(
+ "devtools/client/shared/vendor/react-test-renderer-shallow"
+);
+const TestRenderer = browserRequire(
+ "devtools/client/shared/vendor/react-test-renderer"
+);
+
+var EXAMPLE_URL = "https://example.com/browser/browser/devtools/shared/test/";
+
+SimpleTest.registerCleanupFunction(() => {
+ window._snapshots = null;
+});
+
+function forceRender(comp) {
+ return setState(comp, {}).then(() => setState(comp, {}));
+}
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+function onNextAnimationFrame(fn) {
+ return () => requestAnimationFrame(() => requestAnimationFrame(fn));
+}
+
+function setState(component, newState) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`SHARED-COMPONENTS-TEST: ${msg}\n`);
+}
+
+/**
+ * Tree View
+ */
+
+const TEST_TREE_VIEW = {
+ A: { label: "A", value: "A" },
+ B: { label: "B", value: "B" },
+ C: { label: "C", value: "C" },
+ D: { label: "D", value: "D" },
+ E: { label: "E", value: "E" },
+ F: { label: "F", value: "F" },
+ G: { label: "G", value: "G" },
+ H: { label: "H", value: "H" },
+ I: { label: "I", value: "I" },
+ J: { label: "J", value: "J" },
+ K: { label: "K", value: "K" },
+ L: { label: "L", value: "L" },
+};
+
+TEST_TREE_VIEW.children = {
+ A: [TEST_TREE_VIEW.B, TEST_TREE_VIEW.C, TEST_TREE_VIEW.D],
+ B: [TEST_TREE_VIEW.E, TEST_TREE_VIEW.F, TEST_TREE_VIEW.G],
+ C: [TEST_TREE_VIEW.H, TEST_TREE_VIEW.I],
+ D: [TEST_TREE_VIEW.J],
+ E: [TEST_TREE_VIEW.K, TEST_TREE_VIEW.L],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+};
+
+const TEST_TREE_VIEW_INTERFACE = {
+ provider: {
+ getChildren: x => TEST_TREE_VIEW.children[x.label],
+ hasChildren: x => !!TEST_TREE_VIEW.children[x.label].length,
+ getLabel: x => x.label,
+ getValue: x => x.value,
+ getKey: x => x.label,
+ getType: () => "string",
+ },
+ object: TEST_TREE_VIEW.A,
+ columns: [{ id: "default" }, { id: "value" }],
+};
+
+/**
+ * Tree
+ */
+
+var TEST_TREE_INTERFACE = {
+ getParent: x => TEST_TREE.parent[x],
+ getChildren: x => TEST_TREE.children[x],
+ renderItem: (x, depth, focused) =>
+ "-".repeat(depth) + x + ":" + focused + "\n",
+ getRoots: () => ["A", "M"],
+ getKey: x => "key-" + x,
+ itemHeight: 1,
+ onExpand: x => TEST_TREE.expanded.add(x),
+ onCollapse: x => TEST_TREE.expanded.delete(x),
+ isExpanded: x => TEST_TREE.expanded.has(x),
+};
+
+function isRenderedTree(actual, expectedDescription, msg) {
+ const expected = expectedDescription.map(x => x + "\n").join("");
+ dumpn(`Expected tree:\n${expected}`);
+ dumpn(`Actual tree:\n${actual}`);
+ is(actual, expected, msg);
+}
+
+function isAccessibleTree(tree, options = {}) {
+ const treeNode = tree.refs.tree;
+ is(treeNode.getAttribute("tabindex"), "0", "Tab index is set");
+ is(treeNode.getAttribute("role"), "tree", "Tree semantics is present");
+ if (options.hasActiveDescendant) {
+ ok(
+ treeNode.hasAttribute("aria-activedescendant"),
+ "Tree has an active descendant set"
+ );
+ }
+
+ const treeNodes = [...treeNode.querySelectorAll(".tree-node")];
+ for (const node of treeNodes) {
+ ok(node.id, "TreeNode has an id");
+ is(node.getAttribute("role"), "treeitem", "Tree item semantics is present");
+ is(
+ parseInt(node.getAttribute("aria-level"), 10),
+ parseInt(node.getAttribute("data-depth"), 10) + 1,
+ "Aria level attribute is set correctly"
+ );
+ }
+}
+
+// Encoding of the following tree/forest:
+//
+// A
+// |-- B
+// | |-- E
+// | | |-- K
+// | | `-- L
+// | |-- F
+// | `-- G
+// |-- C
+// | |-- H
+// | `-- I
+// `-- D
+// `-- J
+// M
+// `-- N
+// `-- O
+var TEST_TREE = {
+ children: {
+ A: ["B", "C", "D"],
+ B: ["E", "F", "G"],
+ C: ["H", "I"],
+ D: ["J"],
+ E: ["K", "L"],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+ M: ["N"],
+ N: ["O"],
+ O: [],
+ },
+ parent: {
+ A: null,
+ B: "A",
+ C: "A",
+ D: "A",
+ E: "B",
+ F: "B",
+ G: "B",
+ H: "C",
+ I: "C",
+ J: "D",
+ K: "E",
+ L: "E",
+ M: null,
+ N: "M",
+ O: "N",
+ },
+ expanded: new Set(),
+};
+
+/**
+ * Frame
+ */
+function checkFrameString({
+ el,
+ file,
+ line,
+ column,
+ source,
+ functionName,
+ shouldLink,
+ tooltip,
+ locationPrefix,
+}) {
+ const $ = selector => el.querySelector(selector);
+
+ const $func = $(".frame-link-function-display-name");
+ const $source = $(".frame-link-source");
+ const $locationPrefix = $(".frame-link-prefix");
+ const $filename = $(".frame-link-filename");
+ const $line = $(".frame-link-line");
+
+ is($filename.textContent, file, "Correct filename");
+ is(
+ el.getAttribute("data-line"),
+ line ? `${line}` : null,
+ "Expected `data-line` found"
+ );
+ is(
+ el.getAttribute("data-column"),
+ column ? `${column}` : null,
+ "Expected `data-column` found"
+ );
+ is($source.getAttribute("title"), tooltip, "Correct tooltip");
+ is($source.tagName, shouldLink ? "A" : "SPAN", "Correct linkable status");
+ if (shouldLink) {
+ is($source.getAttribute("href"), source, "Correct source");
+ }
+
+ if (line != null) {
+ let lineText = `:${line}`;
+ if (column != null) {
+ lineText += `:${column}`;
+ }
+
+ is($line.textContent, lineText, "Correct line number");
+ } else {
+ ok(!$line, "Should not have an element for `line`");
+ }
+
+ if (functionName != null) {
+ is($func.textContent, functionName, "Correct function name");
+ } else {
+ ok(!$func, "Should not have an element for `functionName`");
+ }
+
+ if (locationPrefix != null) {
+ is($locationPrefix.textContent, locationPrefix, "Correct prefix");
+ } else {
+ ok(!$locationPrefix, "Should not have an element for `locationPrefix`");
+ }
+}
+
+function checkSmartFrameString({ el, location, functionName, tooltip }) {
+ const $ = selector => el.querySelector(selector);
+
+ const $func = $(".title");
+ const $location = $(".location");
+
+ is($location.textContent, location, "Correct filename");
+ is(el.getAttribute("title"), tooltip, "Correct tooltip");
+ if (functionName != null) {
+ is($func.textContent, functionName, "Correct function name");
+ } else {
+ ok(!$func, "Should not have an element for `functionName`");
+ }
+}
+
+function renderComponent(component, props) {
+ const el = React.createElement(component, props, {});
+ // By default, renderIntoDocument() won't work for stateless components, but
+ // it will work if the stateless component is wrapped in a stateful one.
+ // See https://github.com/facebook/react/issues/4839
+ const wrappedEl = dom.span({}, [el]);
+ const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+ return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+ const el = React.createElement(component, props);
+ const renderer = new ShallowRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+/**
+ * Creates a React Component for testing
+ *
+ * @param {string} factory - factory object of the component to be created
+ * @param {object} props - React props for the component
+ * @returns {object} - container Node, Object with React component
+ * and querySelector function with $ as name.
+ */
+async function createComponentTest(factory, props) {
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+
+ const component = ReactDOM.render(factory(props), container);
+ await forceRender(component);
+
+ return {
+ container,
+ component,
+ $: s => container.querySelector(s),
+ };
+}
+
+async function waitFor(condition = () => true, delay = 50) {
+ do {
+ const res = condition();
+ if (res) {
+ return res;
+ }
+ await new Promise(resolve => setTimeout(resolve, delay));
+ } while (true);
+}
+
+/**
+ * Matches a component tree rendererd using TestRenderer to a given expected JSON
+ * snapshot.
+ * @param {String} name
+ * Name of the function derived from a test [step] name.
+ * @param {Object} el
+ * React element to be rendered using TestRenderer.
+ */
+function matchSnapshot(name, el) {
+ if (!_snapshots) {
+ is(false, "No snapshots were loaded into test.");
+ }
+
+ const snapshot = _snapshots[name];
+ if (snapshot === undefined) {
+ is(false, `Snapshot for "${name}" not found.`);
+ }
+
+ const renderer = TestRenderer.create(el, {});
+ const tree = renderer.toJSON();
+
+ is(
+ JSON.stringify(tree, (key, value) =>
+ typeof value === "function" ? value.toString() : value
+ ),
+ JSON.stringify(snapshot),
+ name
+ );
+}
diff --git a/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html
new file mode 100644
index 0000000000..cf30255141
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html
@@ -0,0 +1,209 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+ <!-- Basic tests for the GridElementWidthResizer component. -->
+ <head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link href="chrome://mochikit/content/tests/SimpleTest/test.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/skin/splitters.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/content/shared/components/splitter/GridElementResizer.css" rel="stylesheet" type="text/css"/>
+ <style>
+ * {
+ box-sizing: border-box;
+ }
+
+ html {
+ --theme-splitter-color: red;
+ }
+
+ main {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: 20px 20px 20px;
+ grid-gap: 10px;
+ }
+
+ .a,
+ .b,
+ .c,
+ .d {
+ border: 1px solid green;
+ }
+
+ header {
+ grid-column: 1 / -1;
+ }
+ .a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ min-width: 100px;
+ }
+ .b {
+ grid-column: 2 / 3;
+ grid-row: 2 / -1;
+ }
+
+ .c {
+ grid-column: 3 / 4;
+ grid-row: 2 / 3;
+ }
+
+ .d {
+ grid-column: 3 / 4;
+ grid-row: 3 / 4;
+ min-width: 150px;
+ }
+
+ .resizer-a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ }
+
+ .resizer-d {
+ grid-column: 3 / 4;
+ grid-row: 2 / -1;
+ }
+ </style>
+ </head>
+ <body>
+ <main></main>
+ <pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+const FUDGE_FACTOR = .5;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = async function () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const GridElementWidthResizer = React.createFactory(browserRequire("devtools/client/shared/components/splitter/GridElementWidthResizer"));
+ ok(GridElementWidthResizer, "Should get GridElementWidthResizer");
+
+ const resizedA = [];
+ const resizedD = [];
+
+ ReactDOM.render([
+ dom.header({}, "header"),
+ dom.aside({className: "a"}, "A"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => a,
+ enabled: true,
+ position: "end",
+ className: "resizer-a",
+ onResizeEnd: width => resizedA.push(width),
+ }),
+ dom.section({className: "b"}, "B"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => window.document.querySelector(".b"),
+ enabled: false,
+ position: "start",
+ className: "resizer-disabled",
+ }),
+ dom.aside({className: "c"}, "C"),
+ dom.aside({className: "d"}, "D"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => d,
+ enabled: true,
+ position: "start",
+ className: "resizer-d",
+ onResizeEnd: width => resizedD.push(width),
+ }),
+ ], window.document.querySelector("main"));
+
+ // wait for a bit as we may not have everything setup yet.
+ await new Promise(res => setTimeout(res, 10));
+
+ const a = window.document.querySelector(".a");
+ const d = window.document.querySelector(".d");
+
+ // Test that we properly rendered our two resizers, and not the disabled one.
+ const resizers = window.document.querySelectorAll(".grid-element-width-resizer");
+ is(resizers.length, 2, "The 2 enabled resizers are rendered");
+
+ const [resizerA, resizerD] = resizers;
+
+ ok(resizerA.classList.contains("resizer-a")
+ && resizerA.classList.contains("end"), "resizerA has expected classes");
+ ok(resizerD.classList.contains("resizer-d")
+ && resizerD.classList.contains("start"), "resizerD has expected classes");
+
+ const aBoundingRect = a.getBoundingClientRect();
+ const aOriginalWidth = aBoundingRect.width;
+
+ info("Resize element A");
+ await resize(resizerA, aBoundingRect.right + 20);
+
+ is(resizedA.length, 1, "onResizeEnd was called once");
+ is(resizedD.length, 0, "resizerD was not impacted");
+ let aWidth = a.getBoundingClientRect().width;
+ is(resizedA[0], aWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(aWidth, aOriginalWidth + 20),
+ "controlled element was resized to the expected size");
+
+ info("Resize element A below its min width");
+ await resize(resizerA, [aBoundingRect.left + 10]);
+ aWidth = a.getBoundingClientRect().width;
+ ok(aboutEq(aWidth, 100), "controlled element was resized to its min width");
+
+ info("Resize element D below its min width");
+ const dBoundingRect = d.getBoundingClientRect();
+ const dOriginalWidth = dBoundingRect.width;
+
+ await resize(resizerD, dBoundingRect.left + 100);
+ is(resizedD.length, 1, "onResizeEnd was called once for d");
+ is(resizedA.length, 2, "onResizeEnd wasn't called for a");
+ let dWidth = d.getBoundingClientRect().width;
+ is(resizedD[0], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, 150), "controlled element wasn't resized below its min-width");
+
+ info("Resize element D");
+ await resize(resizerD, dBoundingRect.left - 15);
+ dWidth = d.getBoundingClientRect().width;
+ is(resizedD[1], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, dOriginalWidth + 15), "element was resized");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ async function resize(resizer, clientX) {
+ info("Mouse down to start dragging");
+ synthesizeMouseAtCenter(resizer, { button: 0, type: "mousedown" }, window);
+ await SimpleTest.promiseWaitForCondition(
+ () => document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never set on the root element"
+ );
+ ok(true, "The dragging class is set on the root element");
+
+ const event = new MouseEvent("mousemove", { clientX });
+ resizer.dispatchEvent(event);
+
+ info("Mouse up to stop resizing");
+ synthesizeMouseAtCenter(document.body, { button: 0, type: "mouseup" }, window);
+
+ await SimpleTest.promiseWaitForCondition(
+ () => !document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never removed from the root element"
+ );
+ ok(true, "The dragging class is removed from the root element");
+ }
+};
+</script>
+</pre>
+ </body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html
new file mode 100644
index 0000000000..3768ecf0a0
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html
@@ -0,0 +1,210 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+ <!-- Basic tests for the GridElementWidthResizer component. -->
+ <head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link href="chrome://mochikit/content/tests/SimpleTest/test.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/skin/splitters.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/content/shared/components/splitter/GridElementResizer.css" rel="stylesheet" type="text/css"/>
+ <style>
+ * {
+ box-sizing: border-box;
+ }
+
+ html {
+ --theme-splitter-color: red;
+ }
+
+ main {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: 20px 20px 20px;
+ grid-gap: 10px;
+ direction: rtl;
+ }
+
+ .a,
+ .b,
+ .c,
+ .d {
+ border: 1px solid green;
+ }
+
+ header {
+ grid-column: 1 / -1;
+ }
+ .a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ min-width: 100px;
+ }
+ .b {
+ grid-column: 2 / 3;
+ grid-row: 2 / -1;
+ }
+
+ .c {
+ grid-column: 3 / 4;
+ grid-row: 2 / 3;
+ }
+
+ .d {
+ grid-column: 3 / 4;
+ grid-row: 3 / 4;
+ min-width: 150px;
+ }
+
+ .resizer-a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ }
+
+ .resizer-d {
+ grid-column: 3 / 4;
+ grid-row: 2 / -1;
+ }
+ </style>
+ </head>
+ <body>
+ <main></main>
+ <pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+const FUDGE_FACTOR = .5;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = async function () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const GridElementWidthResizer = React.createFactory(browserRequire("devtools/client/shared/components/splitter/GridElementWidthResizer"));
+ ok(GridElementWidthResizer, "Should get GridElementWidthResizer");
+
+ const resizedA = [];
+ const resizedD = [];
+
+ ReactDOM.render([
+ dom.header({}, "header"),
+ dom.aside({className: "a"}, "A"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => a,
+ enabled: true,
+ position: "end",
+ className: "resizer-a",
+ onResizeEnd: width => resizedA.push(width),
+ }),
+ dom.section({className: "b"}, "B"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => window.document.querySelector(".b"),
+ enabled: false,
+ position: "start",
+ className: "resizer-disabled",
+ }),
+ dom.aside({className: "c"}, "C"),
+ dom.aside({className: "d"}, "D"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => d,
+ enabled: true,
+ position: "start",
+ className: "resizer-d",
+ onResizeEnd: width => resizedD.push(width),
+ }),
+ ], window.document.querySelector("main"));
+
+ // wait for a bit as we may not have everything setup yet.
+ await new Promise(res => setTimeout(res, 10));
+
+ const a = window.document.querySelector(".a");
+ const d = window.document.querySelector(".d");
+
+ // Test that we properly rendered our two resizers, and not the disabled one.
+ const resizers = window.document.querySelectorAll(".grid-element-width-resizer");
+ is(resizers.length, 2, "The 2 enabled resizers are rendered");
+
+ const [resizerA, resizerD] = resizers;
+
+ ok(resizerA.classList.contains("resizer-a")
+ && resizerA.classList.contains("end"), "resizerA has expected classes");
+ ok(resizerD.classList.contains("resizer-d")
+ && resizerD.classList.contains("start"), "resizerD has expected classes");
+
+ const aBoundingRect = a.getBoundingClientRect();
+ const aOriginalWidth = aBoundingRect.width;
+
+ info("Resize element A");
+ await resize(resizerA, aBoundingRect.left - 20);
+
+ is(resizedA.length, 1, "onResizeEnd was called once");
+ is(resizedD.length, 0, "resizerD was not impacted");
+ let aWidth = a.getBoundingClientRect().width;
+ is(resizedA[0], aWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(aWidth, aOriginalWidth + 20),
+ "controlled element was resized to the expected size");
+
+ info("Resize element A below its min width");
+ await resize(resizerA, [aBoundingRect.right - 10]);
+ aWidth = a.getBoundingClientRect().width;
+ ok(aboutEq(aWidth, 100), "controlled element was resized to its min width");
+
+ info("Resize element D below its min width");
+ const dBoundingRect = d.getBoundingClientRect();
+ const dOriginalWidth = dBoundingRect.width;
+
+ await resize(resizerD, dBoundingRect.right - 100);
+ is(resizedD.length, 1, "onResizeEnd was called once for d");
+ is(resizedA.length, 2, "onResizeEnd wasn't called for a");
+ let dWidth = d.getBoundingClientRect().width;
+ is(resizedD[0], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, 150), "controlled element wasn't resized below its min-width");
+
+ info("Resize element D");
+ await resize(resizerD, dBoundingRect.right + 15);
+ dWidth = d.getBoundingClientRect().width;
+ is(resizedD[1], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, dOriginalWidth + 15), "element was resized");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ async function resize(resizer, clientX) {
+ info("Mouse down to start dragging");
+ synthesizeMouseAtCenter(resizer, { button: 0, type: "mousedown" }, window);
+ await SimpleTest.promiseWaitForCondition(
+ () => document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never set on the root element"
+ );
+ ok(true, "The dragging class is set on the root element");
+
+ const event = new MouseEvent("mousemove", { clientX });
+ resizer.dispatchEvent(event);
+
+ info("Mouse up to stop resizing");
+ synthesizeMouseAtCenter(document.body, { button: 0, type: "mouseup" }, window);
+
+ await SimpleTest.promiseWaitForCondition(
+ () => !document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never removed from the root element"
+ );
+ ok(true, "The dragging class is removed from the root element");
+ }
+};
+</script>
+</pre>
+ </body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_HSplitBox_01.html b/devtools/client/shared/components/test/chrome/test_HSplitBox_01.html
new file mode 100644
index 0000000000..09da5dac6b
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_HSplitBox_01.html
@@ -0,0 +1,140 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Basic tests for the HSplitBox component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/splitters.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+ <style>
+ html {
+ --theme-splitter-color: black;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+const FUDGE_FACTOR = .1;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = async function () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ const HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/HSplitBox"));
+ ok(HSplitBox, "Should get HSplitBox");
+
+ const newSizes = [];
+
+ async function renderBox(props) {
+ const boxProps = Object.assign({
+ start: "hello!",
+ end: "world!",
+ startWidth: .5,
+ onResize(newSize) {
+ newSizes.push(newSize);
+ }
+ }, props);
+ const el = ReactDOM.render(HSplitBox(boxProps), window.document.body);
+ // wait until the element is rendered.
+ await SimpleTest.promiseWaitForCondition(
+ () => document.querySelector(".devtools-side-splitter")
+ );
+ return el;
+ }
+
+ await renderBox();
+
+ // Test that we properly rendered our two panes.
+
+ let panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should get two panes");
+ is(panes[0].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[1].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[0].textContent.trim(), "hello!", "First pane should be hello");
+ is(panes[1].textContent.trim(), "world!", "Second pane should be world");
+
+ // Now change the left width and assert that the changes are reflected.
+
+ await renderBox({ startWidth: .25 });
+ panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should still have two panes");
+ is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");
+ is(panes[1].style.flexGrow, "0.75", "Second pane's width should be .75");
+
+ // Mouse moves without having grabbed the splitter should have no effect.
+
+ const container = document.querySelector(".h-split-box");
+ ok(container, "Should get our container .h-split-box");
+
+ const { left, top, width } = container.getBoundingClientRect();
+ const middle = left + width / 2;
+ const oneQuarter = left + width / 4;
+ const threeQuarters = left + 3 * width / 4;
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 0, "Mouse moves without dragging the splitter should have no effect");
+
+ // Send a mouse down on the splitter, and then move the mouse a couple
+ // times. Now we should get resizes.
+
+ const splitter = document.querySelector(".devtools-side-splitter");
+ ok(splitter, "Should get our splitter");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mousedown" }, window);
+
+ function mouseMove(clientX) {
+ const event = new MouseEvent("mousemove", { clientX });
+ document.defaultView.top.dispatchEvent(event);
+ }
+
+ mouseMove(middle);
+ is(newSizes.length, 1, "Should get 1 resize");
+ ok(aboutEq(newSizes[0], .5), "New size should be ~.5");
+
+ mouseMove(left);
+ is(newSizes.length, 2, "Should get 2 resizes");
+ ok(aboutEq(newSizes[1], 0), "New size should be ~0");
+
+ mouseMove(oneQuarter);
+ is(newSizes.length, 3, "Sould get 3 resizes");
+ ok(aboutEq(newSizes[2], .25), "New size should be ~.25");
+
+ mouseMove(threeQuarters);
+ is(newSizes.length, 4, "Should get 4 resizes");
+ ok(aboutEq(newSizes[3], .75), "New size should be ~.75");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mouseup" }, window);
+
+ // Now that we have let go of the splitter, mouse moves should not result in resizes.
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 4, "Should still have 4 resizes");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_accordion.html b/devtools/client/shared/components/test/chrome/test_accordion.html
new file mode 100644
index 0000000000..60d179be6f
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_accordion.html
@@ -0,0 +1,141 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that Accordion renders correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Accordion component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="resource://testing-common/sinon-7.2.7.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script src="accordion.snapshots.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+/* global sinon */
+
+window.onload = async function() {
+ try {
+ const { button, div } = require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ renderIntoDocument,
+ findAllInRenderedTree,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Accordion =
+ browserRequire("devtools/client/shared/components/Accordion");
+
+ const testItems = [
+ {
+ header: "Test Accordion Item 1",
+ id: "accordion-item-1",
+ component: div({}),
+ opened: false,
+ onToggle: sinon.spy(),
+ },
+ {
+ header: "Test Accordion Item 2",
+ id: "accordion-item-2",
+ component: div({}),
+ buttons: button({}),
+ opened: false,
+ onToggle: sinon.spy(),
+ },
+ {
+ header: "Test Accordion Item 3",
+ id: "accordion-item-3",
+ component: div({}),
+ opened: true,
+ onToggle: sinon.spy(),
+ },
+ ];
+
+ // Accordion basic render
+ const accordion = React.createElement(Accordion, { items: testItems });
+
+ matchSnapshot("Accordion basic render.", accordion);
+
+ const tree = renderIntoDocument(accordion);
+ const headers = findAllInRenderedTree(tree, c => c.className === "accordion-header");
+
+ Simulate.click(headers[0]);
+ ok(testItems[0].onToggle.calledWith(true), "Handle toggling with click.");
+ ok(testItems[1].onToggle.notCalled,
+ "onToggle wasn't called on element we didn't click on.");
+
+ isDeeply(
+ tree.state,
+ {
+ everOpened: {
+ "accordion-item-1": true,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ opened: {
+ "accordion-item-1": true,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ },
+ "State updated correctly"
+ );
+
+ Simulate.keyDown(headers[0], { key: "Enter" });
+ ok(testItems[0].onToggle.calledWith(false), "Handle toggling with Enter key.");
+ isDeeply(
+ tree.state,
+ {
+ everOpened: {
+ "accordion-item-1": true,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ opened: {
+ "accordion-item-1": false,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ },
+ "State updated correctly"
+ );
+
+ Simulate.keyDown(headers[1], { key: " " });
+ ok(testItems[1].onToggle.calledWith(true), "Handle toggling with Space key.");
+ isDeeply(
+ tree.state,
+ {
+ everOpened: {
+ "accordion-item-1": true,
+ "accordion-item-2": true,
+ "accordion-item-3": true,
+ },
+ opened: {
+ "accordion-item-1": false,
+ "accordion-item-2": true,
+ "accordion-item-3": true,
+ },
+ },
+ "State updated correctly"
+ );
+
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_frame_01.html b/devtools/client/shared/components/test/chrome/test_frame_01.html
new file mode 100644
index 0000000000..f763b21395
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_frame_01.html
@@ -0,0 +1,361 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html>
+ <!--
+Test the formatting of the file name, line and columns are correct in frame components,
+with optional columns, unknown and non-URL sources.
+-->
+ <head>
+ <meta charset="utf-8" />
+ <title>Frame component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link
+ rel="stylesheet"
+ type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ />
+ </head>
+ <body>
+ <pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Frame = React.createFactory(browserRequire("devtools/client/shared/components/Frame"));
+ ok(Frame, "Should get Frame");
+
+ // Check when there's a column
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check when there's no column
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when column === 0
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 0,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when there's an error in CSS (View source in Style Editor)
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/cafebabe.css",
+ line: 13,
+ },
+ messageSource: "css",
+ }, {
+ file: "cafebabe.css",
+ line: 13,
+ shouldLink: true,
+ tooltip: "View source in Style Editor → https://myfile.com/cafebabe.css:13",
+ });
+
+
+ // Check when there's no parseable URL source;
+ // should not link but should render line/columns
+ await checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ shouldLink: false,
+ tooltip: "self-hosted:1",
+ });
+ await checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "self-hosted:1:10",
+ });
+
+ // Check when there's no source;
+ // should not link but should render line/columns
+ await checkFrameComponent({
+ frame: {
+ line: 1,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ shouldLink: false,
+ tooltip: "(unknown):1",
+ });
+ await checkFrameComponent({
+ frame: {
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "(unknown):1:10",
+ });
+
+ // Check when there's a column, but no line;
+ // no line/column info should render
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check when line is 0; this should be an invalid
+ // line option, so don't render line/column
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that line and column can be strings
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: "10",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 10,
+ column: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:10:55",
+ });
+
+ // Check that line and column can be strings,
+ // and that the `0` rendering rules apply when they are strings as well
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: "0",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFullSourceUrl option works correctly
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFullSourceUrl: true
+ }, {
+ file: "https://myfile.com/mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFunctionName option works correctly
+ await checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ }
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ await checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: "myfun",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that anonymous function name is not displayed unless explicitly enabled
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true
+ }, {
+ functionName: "<anonymous>",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check if file is rendered with "/" for root documents when showEmptyPathAsHost is false
+ await checkFrameComponent({
+ frame: {
+ source: "https://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: false,
+ }, {
+ file: "/",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://www.cnn.com/:1",
+ });
+
+ // Check if file is rendered with hostname for root documents when showEmptyPathAsHost is true
+ await checkFrameComponent({
+ frame: {
+ source: "https://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: true,
+ }, {
+ file: "www.cnn.com",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://www.cnn.com/:1",
+ });
+
+ const resolvedLocation = {
+ sourceId: "whatever",
+ line: 23,
+ sourceUrl: "https://bugzilla.mozilla.org/original.js",
+ };
+ const mockSourceMapURLService = {
+ subscribeByLocation (loc, callback) {
+ // Resolve immediately.
+ callback({
+ url: resolvedLocation.sourceUrl,
+ line: resolvedLocation.line,
+ column: undefined,
+ });
+ return () => {};
+ },
+ };
+ await checkFrameComponent({
+ frame: {
+ line: 97,
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ },
+ sourceMapURLService: mockSourceMapURLService,
+ }, {
+ file: "original.js",
+ line: resolvedLocation.line,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:23",
+ source: "https://bugzilla.mozilla.org/original.js",
+ });
+
+ // Check when a message comes from a logPoint,
+ // a prefix should render before source
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ options: { logPoint: true },
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55:10",
+ });
+
+ function checkFrameComponent(input, expected) {
+ const props = Object.assign({ onClick: () => {} }, input);
+ const frame = ReactDOM.render(Frame(props), window.document.body);
+ const el = ReactDOM.findDOMNode(frame);
+ const { source } = input.frame;
+ checkFrameString(Object.assign({ el, source }, expected));
+ ReactDOM.unmountComponentAtNode(window.document.body);
+ }
+
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+ </body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_frame_02.html b/devtools/client/shared/components/test/chrome/test_frame_02.html
new file mode 100644
index 0000000000..dd4bf9c2b7
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_frame_02.html
@@ -0,0 +1,103 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the frame component reacts to source-map pref changse.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Frame component source-map test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Frame = React.createFactory(browserRequire("devtools/client/shared/components/Frame"));
+
+ const resolvedLocation = {
+ sourceId: "whatever",
+ line: 23,
+ sourceUrl: "https://bugzilla.mozilla.org/original.js",
+ };
+ const mockSourceMapURLService = {
+ _update () {
+ this._callback(Services.prefs.getBoolPref(PREF)
+ ? {
+ url: resolvedLocation.sourceUrl,
+ line: resolvedLocation.line,
+ column: undefined,
+ }
+ : null);
+ },
+ subscribeByLocation (loc, callback) {
+ this._callback = callback;
+ // Resolve immediately.
+ this._update();
+
+ return () => {};
+ },
+ };
+
+ const props = {
+ onClick: () => {},
+ frame: {
+ line: 97,
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ },
+ sourceMapURLService: mockSourceMapURLService,
+ };
+
+ const PREF = "devtools.source-map.client-service.enabled";
+ Services.prefs.setBoolPref(PREF, false);
+
+ const frame = ReactDOM.render(Frame(props), window.document.body);
+ const el = ReactDOM.findDOMNode(frame);
+ const { source } = props.frame;
+
+ const expectedOriginal = {
+ file: "original.js",
+ line: resolvedLocation.line,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:23",
+ source: "https://bugzilla.mozilla.org/original.js",
+ };
+ const expectedGenerated = {
+ file: "bundle.js",
+ line: 97,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/bundle.js:97",
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ };
+
+ checkFrameString(Object.assign({ el, source }, expectedGenerated));
+
+ Services.prefs.setBoolPref(PREF, true);
+ mockSourceMapURLService._update();
+ checkFrameString(Object.assign({ el, source }, expectedOriginal));
+
+ Services.prefs.setBoolPref(PREF, false);
+ mockSourceMapURLService._update();
+ checkFrameString(Object.assign({ el, source }, expectedGenerated));
+
+ Services.prefs.clearUserPref(PREF);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_list.html b/devtools/client/shared/components/test/chrome/test_list.html
new file mode 100644
index 0000000000..8be8907b5d
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_list.html
@@ -0,0 +1,127 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that List renders correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>List component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script src="list.snapshots.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function() {
+ try {
+ const { div } = require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ renderIntoDocument,
+ findRenderedDOMComponentWithClass,
+ scryRenderedDOMComponentsWithTag,
+ scryRenderedComponentsWithType,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const { List, ListItem } =
+ browserRequire("devtools/client/shared/components/List");
+
+ const testItems = [
+ {
+ component: div({ className: "item-1" }, "Test List Item 1"),
+ className: "list-item-1",
+ key: "list-item-1",
+ },
+ {
+ component: div({ className: "item-2" }, "Test List Item 2"),
+ className: "list-item-2",
+ key: "list-item-2",
+ },
+ {
+ component: div({ className: "item-3" }, "Test List Item 3"),
+ className: "list-item-3",
+ key: "list-item-3",
+ },
+ ];
+
+ const listReactEl = React.createElement(List, {
+ items: testItems,
+ labelledBy: "test-labelledby",
+ });
+
+ const list = renderIntoDocument(listReactEl);
+ const listEl = findRenderedDOMComponentWithClass(list, "list");
+ const items = scryRenderedComponentsWithType(list, ListItem);
+ const itemEls = scryRenderedDOMComponentsWithTag(list, "li");
+
+ function testCurrent(index) {
+ is(list.state.current, index, "Correct current item.");
+ is(listEl.getAttribute("aria-activedescendant"), testItems[index].key,
+ "Correct active descendant.");
+ }
+
+ is(items.length, 3, "Correct number of list item components in tree.");
+ is(itemEls.length, 3, "Correct number of list items is rendered.");
+ info("Testing initial tree properties.");
+ for (let index = 0; index < items.length; index++) {
+ const item = items[index];
+ const itemEl = itemEls[index];
+ const { active, current, item: itemProp } = item.props;
+ const content = itemEl.querySelector(".list-item-content");
+
+ is(active, false, "Correct active state.");
+ is(current, false, "Correct current state.");
+ is(itemProp, testItems[index], "Correct rendered item.");
+ is(item.contentRef.current, content, "Correct content ref.");
+
+ is(itemEl.className, testItems[index].className, "Correct list item class.");
+ is(itemEl.id, testItems[index].key, "Correct list item it.");
+ is(content.getAttribute("role"), "presentation", "Correct content role.");
+
+ is(content.innerHTML,
+ `<div class="item-${index + 1}">Test List Item ${index + 1}</div>`,
+ "Content rendered correctly.");
+ }
+
+ is(list.state.current, null, "Current item is not set by default.");
+ is(list.state.active, null, "Active item is not set by default.");
+ is(list.listRef.current, listEl, "Correct list ref.");
+
+ is(listEl.className, "list", "Correct list class.");
+ is(listEl.tabIndex, 0, "List is focusable.");
+ ok(!listEl.hasAttribute("aria-label"), "List has no label.");
+ is(listEl.getAttribute("aria-labelledby"), "test-labelledby",
+ "Correct list labelled by attribute.");
+ ok(!listEl.hasAttribute("aria-activedescendant"),
+ "No active descendant set by default.");
+
+ Simulate.focus(listEl);
+ testCurrent(0);
+
+ Simulate.click(itemEls[2]);
+ testCurrent(2);
+
+ Simulate.blur(listEl);
+ testCurrent(2);
+
+ Simulate.focus(listEl);
+ testCurrent(2);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_list_keyboard.html b/devtools/client/shared/components/test/chrome/test_list_keyboard.html
new file mode 100644
index 0000000000..7558404ed2
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_list_keyboard.html
@@ -0,0 +1,283 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that List component has working keyboard interactions.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>List component keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = function() {
+ try {
+ const { a, button, div } =
+ require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ findRenderedDOMComponentWithTag,
+ scryRenderedDOMComponentsWithTag,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const { List } =
+ browserRequire("devtools/client/shared/components/List");
+
+ const testItems = [
+ {
+ component: div({}, "Test List Item 1"),
+ className: "list-item-1",
+ key: "list-item-1",
+ },
+ {
+ component: div({},
+ "Test List Item 2",
+ a({ href: "#" }, "Focusable 1"),
+ button({ }, "Focusable 2")),
+ className: "list-item-2",
+ key: "list-item-2",
+ },
+ {
+ component: div({}, "Test List Item 3"),
+ className: "list-item-3",
+ key: "list-item-3",
+ },
+ ];
+
+ const list = React.createElement(List, {
+ items: testItems,
+ labelledby: "test-labelledby",
+ });
+
+ const tree = ReactDOM.render(list, document.body);
+ const listEl = findRenderedDOMComponentWithClass(tree, "list");
+ scryRenderedDOMComponentsWithTag(tree, "li");
+ const defaultFocus = listEl.ownerDocument.body;
+
+ function blurEl(el) {
+ // Simulate.blur does not seem to update the activeElement.
+ el.blur();
+ }
+
+ function focusEl(el) {
+ // Simulate.focus does not seem to update the activeElement.
+ el.focus();
+ }
+
+ const tests = [{
+ name: "Test default List state. Keyboard focus is set to document body by default.",
+ state: { current: null, active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Current item must be set to the first list item on initial focus. " +
+ "Keyboard focus should be set on list's conatiner (<ul>).",
+ action: () => focusEl(listEl),
+ activeElement: listEl,
+ state: { current: 0 },
+ }, {
+ name: "Current item should remain set even when the list is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => blurEl(listEl),
+ state: { current: 0 },
+ activeElement: defaultFocus,
+ }, {
+ name: "Unset list's current state.",
+ action: () => tree.setState({ current: null }),
+ state: { current: null },
+ }, {
+ name: "Current item must be re-set again to the first list item on initial " +
+ "focus. Keyboard focus should be set on list's conatiner (<ul>).",
+ action: () => focusEl(listEl),
+ activeElement: listEl,
+ state: { current: 0 },
+ }, {
+ name: "Current item should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 1 },
+ }, {
+ name: "Current item should be updated to last on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 2 },
+ }, {
+ name: "Current item should remain on last on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 2 },
+ }, {
+ name: "Current item should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 1 },
+ }, {
+ name: "Current item should be updated to first on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 0 },
+ }, {
+ name: "Current item should remain on first on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 0 },
+ }, {
+ name: "Current item should be updated to last on End.",
+ event: { type: "keyDown", el: listEl, options: { key: "End" }},
+ state: { current: 2 },
+ }, {
+ name: "Current item should be updated to first on Home.",
+ event: { type: "keyDown", el: listEl, options: { key: "Home" }},
+ state: { current: 0 },
+ }, {
+ name: "Current item should be set as active on Enter.",
+ event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
+ state: { current: 0, active: 0 },
+ activeElement: listEl,
+ }, {
+ name: "Active item should be unset on Escape.",
+ event: { type: "keyDown", el: listEl, options: { key: "Escape" }},
+ state: { current: 0, active: null },
+ }, {
+ name: "Current item should be set as active on Space.",
+ event: { type: "keyDown", el: listEl, options: { key: " " }},
+ state: { current: 0, active: 0 },
+ activeElement: listEl,
+ }, {
+ name: "Current item should unset when focus leaves the list.",
+ action: () => blurEl(listEl),
+ state: { current: 0, active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.",
+ action: () => focusEl(listEl),
+ activeElement: listEl,
+ }, {
+ name: "Current item should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 1, active: null },
+ }, {
+ name: "Current item should be set as active on Enter. Keyboard focus should be " +
+ "set on the first focusable element inside the list item, if available.",
+ event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ // When list item becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should be set to next tabbable element inside the active " +
+ "list item on Tab.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ // When list item becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the list item when focused on last " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the list item when focused on first " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Active item should be unset on Escape. Focus should move back to the " +
+ "list container.",
+ event: { type: "keyDown", el: listEl, options: { key: "Escape" }},
+ state: { current: 1, active: null },
+ activeElement: listEl,
+ }, {
+ name: "Current item should be set as active on Space. Keyboard focus should be " +
+ "set on the first focusable element inside the list item, if available.",
+ event: { type: "keyDown", el: listEl, options: { key: " " }},
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ // When list item becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Current item should remain set even when the list is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => listEl.ownerDocument.activeElement.blur(),
+ state: { current: 1, active: null, },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.",
+ action: () => focusEl(listEl),
+ state: { current: 1, active: null },
+ activeElement: listEl,
+ }, {
+ name: "Current item should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 0, active: null },
+ }, {
+ name: "Current item should be set as active on Enter.",
+ event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
+ state: { current: 0, active: 0 },
+ activeElement: listEl,
+ }, {
+ name: "Keyboard focus should move to another focusable element outside of the " +
+ "list when there's nothing to focus on inside the list item.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { current: 0, active: null },
+ activeElement: listEl.ownerDocument.documentElement,
+ }];
+
+ for (const test of tests) {
+ const { action, event, state, name } = test;
+
+ is(listEl, findRenderedDOMComponentWithClass(tree, "list"), "Sanity check");
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ Simulate[type](el, options);
+ } else if (action) {
+ action();
+ }
+
+ if (test.activeElement) {
+ is(listEl.ownerDocument.activeElement, test.activeElement,
+ "Focus is set correctly.");
+ }
+
+ for (const key in state) {
+ is(tree.state[key], state[key], `${key} state is correct.`);
+ }
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_01.html b/devtools/client/shared/components/test/chrome/test_notification_box_01.html
new file mode 100644
index 0000000000..2921d607c3
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_01.html
@@ -0,0 +1,136 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Basic rendering
+* Appending correct classname on wrapping
+* Appending a notification
+* Notification priority
+* Closing notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ const renderedBox = shallowRenderComponent(NotificationBox, {});
+ is(renderedBox.type, "div", "NotificationBox is rendered as <div>");
+
+ info("Test rendering NotificationBox with default props");
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ ok(notificationNode.classList.contains("notificationbox"),
+ "NotificationBox has expected class");
+ ok(notificationNode.classList.contains("border-bottom"),
+ "NotificationBox has expected class");
+ is(notificationNode.textContent, "",
+ "Empty NotificationBox has no text content");
+
+ checkNumberOfNotifications(notificationBox, 0);
+
+ // Append a notification
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_HIGH
+ );
+
+ is (notificationNode.textContent, "Info message",
+ "The box must display notification message");
+ checkNumberOfNotifications(notificationBox, 1);
+
+ // Append more important notification
+ notificationBox.appendNotification(
+ "Critical message",
+ "id2",
+ null,
+ PriorityLevels.PRIORITY_CRITICAL_BLOCK
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must display more important notification message");
+
+ // Append less important notification
+ notificationBox.appendNotification(
+ "Warning message",
+ "id3",
+ null,
+ PriorityLevels.PRIORITY_WARNING_HIGH
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must still display the more important notification");
+
+ ok(notificationBox.getCurrentNotification(),
+ "There must be current notification");
+
+ notificationBox.getNotificationWithValue("id1").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id2").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id3").close();
+ checkNumberOfNotifications(notificationBox, 0);
+
+ info(`Check "wrapping" prop works as expected`);
+ // Append wrapping classname to the dom element when passing wrapping prop
+ const boxElementWrapped = React.createElement(NotificationBox, {wrapping: true});
+ const notificationBoxWrapped = TestUtils.renderIntoDocument(boxElementWrapped);
+ const wrappedNotificationNode = ReactDOM.findDOMNode(notificationBoxWrapped);
+
+ ok(wrappedNotificationNode.classList.contains("wrapping"),
+ "Wrapped notificationBox has expected class");
+
+ info(`Check "displayBorderTop/displayBorderBottom" props work as expected`);
+ const element = React.createElement(NotificationBox, {
+ displayBorderTop: true,
+ displayBorderBottom: false,
+ });
+ const renderedElement = TestUtils.renderIntoDocument(element);
+ const elementNode = ReactDOM.findDOMNode(renderedElement);
+
+ ok(elementNode.classList.contains("border-top"),
+ "truthy displayBorderTop render a border-top className");
+ ok(!elementNode.classList.contains("border-bottom"),
+ "falsy displayBorderBottom does not render a border-bottom className");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+
+function checkNumberOfNotifications(notificationBox, expected) {
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, expected,
+ "The notification box must have expected number of notifications");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_02.html b/devtools/client/shared/components/test/chrome/test_notification_box_02.html
new file mode 100644
index 0000000000..f74194d128
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_02.html
@@ -0,0 +1,73 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom callback in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Test rendering
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let callbackExecuted = false;
+
+ // Append a notification.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ undefined,
+ (reason) => {
+ callbackExecuted = true;
+ is(reason, "removed", "The reason must be expected string");
+ }
+ );
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ const closeButton = notificationNode.querySelector(
+ ".messageCloseButton");
+
+ // Click the close button to close the notification.
+ TestUtils.Simulate.click(closeButton);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+
+ ok(callbackExecuted, "Event callback must be executed.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_03.html b/devtools/client/shared/components/test/chrome/test_notification_box_03.html
new file mode 100644
index 0000000000..816456edd3
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_03.html
@@ -0,0 +1,87 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom buttons in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Test rendering
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let buttonCallbackExecuted = false;
+ const buttons = [{
+ label: "Button1",
+ callback: () => {
+ buttonCallbackExecuted = true;
+
+ // Do not close the notification
+ return true;
+ },
+ }, {
+ label: "Button2",
+ callback: () => {
+ // Close the notification (return value undefined)
+ },
+ }];
+
+ // Append a notification with buttons.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ buttons
+ );
+
+ const buttonNodes = notificationNode.querySelectorAll(
+ ".notificationButton");
+
+ is(buttonNodes.length, 2, "There must be two buttons");
+
+ // Click the first button
+ TestUtils.Simulate.click(buttonNodes[0]);
+ ok(buttonCallbackExecuted, "Button callback must be executed.");
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ // Click the second button (closing the notification)
+ TestUtils.Simulate.click(buttonNodes[1]);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_04.html b/devtools/client/shared/components/test/chrome/test_notification_box_04.html
new file mode 100644
index 0000000000..07ad9af25c
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_04.html
@@ -0,0 +1,67 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Adding a mdnLink to a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Render notification
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ const mdnLink = "https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors"
+
+ const mdnLinkButton = {mdnUrl: mdnLink, label: "learn more about error" }
+
+ // Append a notification with a learn-more link
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ [mdnLinkButton],
+ (e) => false,
+ );
+
+ const linkNode = notificationNode.querySelector(
+ "a.learn-more-link");
+
+ ok(linkNode, "Link is present");
+
+ is(linkNode.title, "learn more about error", "link has correct title");
+ ok(linkNode.classList.contains("devtools-button"), "link has correct class")
+
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_05.html b/devtools/client/shared/components/test/chrome/test_notification_box_05.html
new file mode 100644
index 0000000000..b3a4e96378
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_05.html
@@ -0,0 +1,63 @@
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* the close button is not present when displayCloseButton is false
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Test rendering with close button disabled
+ const boxElement = React.createElement(NotificationBox, {displayCloseButton: false});
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+
+
+ // Append a notification.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ [],
+ (e) => false,
+ );
+
+ // Ensure close button is not present
+ const linkNode = notificationNode.querySelector(
+ ".messageCloseButton");
+
+ ok(!linkNode, "Close button is not present");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html b/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html
new file mode 100644
index 0000000000..110d5640c1
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html
@@ -0,0 +1,301 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html>
+<!--
+Test the searchbox and autocomplete-popup components
+-->
+<head>
+ <meta charset="utf-8">
+ <title>SearchBox component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+window.onload = async function () {
+ /**
+ * Takes a DOMNode with its children as list items,
+ * Typically UL > LI and each item's text value is
+ * compared with the reference item's value as a test
+ *
+ * @params {Node} - Node to be compared
+ * @reference {array} - Reference array for comparison. The selected index is
+ * highlighted as a single element array ie. ["[abc]", "ab", "abcPQR"],
+ * Here the element "abc" is highlighted
+ */
+ function compareAutocompleteList(list, reference) {
+ const delimiter = " - ";
+ const observedList = [...list.children].map(el => {
+ return el.classList.contains("autocomplete-selected")
+ ? `[${el.textContent}]`
+ : el.textContent
+ });
+ is(observedList.join(delimiter), reference.join(delimiter),
+ "Autocomplete items are rendered as expected");
+ }
+
+ function compareCursorPosition(initialElement) {
+ const initialPosition = initialElement.selectionStart;
+ return (element) => {
+ is(element.selectionStart, initialPosition, "Input cursor position is not changed");
+ }
+ }
+
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SearchBox = React.createFactory(
+ browserRequire("devtools/client/shared/components/SearchBox")
+ );
+ const { component, $ } = await createComponentTest(SearchBox, {
+ type: "search",
+ autocompleteProvider: (filter) => {
+ const baseList = [
+ "foo",
+ "BAR",
+ "baZ",
+ "abc",
+ "pqr",
+ "xyz",
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ ];
+ if (!filter) {
+ return [];
+ }
+
+ const tokens = filter.split(/\s+/g);
+ const lastToken = tokens[tokens.length - 1];
+ const previousTokens = tokens.slice(0, tokens.length - 1);
+
+ if (!lastToken) {
+ return [];
+ }
+
+ return baseList
+ .filter((item) => {
+ return item.toLowerCase().startsWith(lastToken.toLowerCase())
+ && item.toLowerCase() !== lastToken.toLowerCase();
+ })
+ .sort()
+ .map(item => ({
+ value: [...previousTokens, item].join(" "),
+ displayValue: item,
+ }));
+ },
+ onChange: () => null,
+ });
+
+ async function testSearchBoxWithAutocomplete() {
+ ok(!$(".devtools-autocomplete-popup"), "Autocomplete list not visible");
+
+ $(".devtools-searchinput").focus();
+ await forceRender(component); // Wait for state update
+ ok(!$(".devtools-autocomplete-popup"), "Autocomplete list not visible");
+
+ sendString("a");
+ await forceRender(component);
+
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+
+ // Blur event
+ $(".devtools-searchinput").blur();
+ await forceRender(component);
+ ok(!component.state.focused, "focused state was properly set");
+ ok(!$(".devtools-autocomplete-popup"), "Autocomplete list removed from DOM");
+ }
+
+ async function testKeyEventsWithAutocomplete() {
+ // Clear the initial input
+ $(".devtools-searchinput").focus();
+ const cursorPositionIsNotChanged = compareCursorPosition($(".devtools-searchinput"));
+
+ // ArrowDown
+ synthesizeKey("KEY_ArrowDown");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "[a1]",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+ ok($(".devtools-autocomplete-listbox .autocomplete-item:nth-child(2)")
+ .className.includes("autocomplete-selected"),
+ "Selection class applied");
+
+ // A double ArrowUp should roll back to the bottom of the list
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowUp");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "[abc]",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // PageDown should take -5 places up
+ synthesizeKey("KEY_PageUp");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "[a1]",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // PageDown should take +5 places down
+ synthesizeKey("KEY_PageDown");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "[abc]",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // Home should take to the top of the list
+ synthesizeKey("KEY_Home");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // End should take to the bottom of the list
+ synthesizeKey("KEY_End");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "[abc]",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // Key down in existing state should rollover to the top
+ synthesizeKey("KEY_ArrowDown");
+ await forceRender(component);
+ // Tab should select the component and hide popup
+ synthesizeKey("KEY_Tab");
+ await forceRender(component);
+ is(component.state.value, "ABC", "Tab hit selects the item");
+ ok(!$(".devtools-autocomplete-popup"), "Tab hit hides the popup");
+
+ // Activate popup by removing a key
+ synthesizeKey("KEY_Backspace");
+ await forceRender(component);
+ ok($(".devtools-autocomplete-popup"), "Popup is up");
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "abc"
+ ]);
+
+ // Enter key selection
+ synthesizeKey("KEY_ArrowUp");
+ await forceRender(component);
+ synthesizeKey("KEY_Enter");
+ is(component.state.value, "abc", "Enter selection");
+ ok(!$(".devtools-autocomplete-popup"), "Enter/Return hides the popup");
+
+ // Escape should remove the autocomplete component
+ synthesizeKey("KEY_Backspace");
+ await forceRender(component);
+ synthesizeKey("KEY_Escape");
+ await forceRender(component);
+ ok(!$(".devtools-autocomplete-popup"),
+ "Autocomplete list removed from DOM on Escape");
+ }
+
+ async function testMouseEventsWithAutocomplete() {
+ $(".devtools-searchinput").focus();
+ await setState(component, {
+ value: "",
+ focused: true,
+ });
+ await forceRender(component);
+
+ // ArrowDown
+ synthesizeKey("KEY_ArrowDown");
+ await forceRender(component);
+ synthesizeMouseAtCenter($(".devtools-searchinput"), {}, window);
+ await forceRender(component);
+ is(component.state.focused, true, "Component should now be focused");
+
+ sendString("pq");
+ await forceRender(component);
+ synthesizeMouseAtCenter(
+ $(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
+ {}, window
+ );
+ await forceRender(component);
+ is(component.state.value, "pqr", "Mouse click selects the item.");
+ ok(!$(".devtools-autocomplete-popup"), "Mouse click on item hides the popup");
+ }
+
+ async function testTokenizedAutocomplete() {
+ // Test for string "pqr ab" which should show list of ABC, abc
+ sendString(" ab");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "abc"
+ ]);
+
+ // Select the first element, value now should be "pqr ABC"
+ synthesizeMouseAtCenter(
+ $(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
+ {}, window
+ );
+ is(component.state.value, "pqr ABC", "Post Tokenization value selection");
+ }
+
+ add_task(async function () {
+ await testSearchBoxWithAutocomplete();
+ await testKeyEventsWithAutocomplete();
+ await testMouseEventsWithAutocomplete();
+ await testTokenizedAutocomplete();
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_searchbox.html b/devtools/client/shared/components/test/chrome/test_searchbox.html
new file mode 100644
index 0000000000..8e2f76c1b9
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_searchbox.html
@@ -0,0 +1,74 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html>
+<!--
+Test the searchbox component
+-->
+<head>
+ <meta charset="utf-8">
+ <title>SearchBox component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+window.onload = function () {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SearchBox = React.createFactory(
+ browserRequire("devtools/client/shared/components/SearchBox")
+ );
+ ok(SearchBox, "Got the SearchBox factory");
+
+ async function testSimpleSearchBox() {
+ // Test initial state
+ const { component, $ } = await createComponentTest(SearchBox, {
+ type: "search",
+ keyShortcut: "CmdOrCtrl+F",
+ placeholder: "crazy placeholder",
+ });
+
+ is(component.state.value, "", "Initial value is blank");
+ ok(!component.state.focused, "Input isn't initially focused");
+ ok($(".devtools-searchinput-clear").hidden, "Clear button hidden");
+ is($(".devtools-searchinput").placeholder, "crazy placeholder",
+ "Placeholder is properly set");
+
+ synthesizeKey("f", { accelKey: true });
+ await forceRender(component); // Wait for state update
+ ok(component.state.focused, "Shortcut key focused the input box");
+
+ $(".devtools-searchinput").blur();
+ await forceRender(component);
+ ok(!component.state.focused, "`focused` state set to false after blur");
+
+ // Test changing value in state
+ await setState(component, {
+ value: "foo",
+ });
+
+ is(component.state.value, "foo", "value was properly set on state");
+ is($(".devtools-searchinput").value, "foo", "value was properly set on element");
+
+ // Filling input should show clear button
+ ok(!$(".devtools-searchinput-clear").hidden, "Clear button shown");
+
+ // Clearing value should hide clear button
+ await setState(component, {
+ value: "",
+ });
+ await forceRender(component);
+ ok($(".devtools-searchinput-clear").hidden, "Clear button was hidden");
+ }
+
+ add_task(async function () {
+ await testSimpleSearchBox();
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_sidebar_toggle.html b/devtools/client/shared/components/test/chrome/test_sidebar_toggle.html
new file mode 100644
index 0000000000..0a31037a84
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_sidebar_toggle.html
@@ -0,0 +1,59 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test sidebar toggle button
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Sidebar toggle button test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ const SidebarToggle = browserRequire("devtools/client/shared/components/SidebarToggle.js");
+
+ try {
+ await test();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function test() {
+ const output1 = shallowRenderComponent(SidebarToggle, {
+ collapsed: false,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output1.type, "button", "Output is a button element");
+ is(output1.props.title, "Expand", "Proper title is set");
+ is(output1.props.className.indexOf("pane-collapsed"), -1,
+ "Proper class name is set");
+
+ const output2 = shallowRenderComponent(SidebarToggle, {
+ collapsed: true,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output2.props.title, "Collapse", "Proper title is set");
+ ok(output2.props.className.includes("pane-collapsed"),
+ "Proper class name is set");
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html b/devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html
new file mode 100644
index 0000000000..174d0f87b4
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html
@@ -0,0 +1,141 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SmartTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/SmartTrace"));
+ ok(SmartTrace, "Got the SmartTrace factory");
+
+ add_task(async function() {
+ const REACT_FRAMES_COUNT = 10;
+
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10,
+ functionName: null,
+ },
+ // Simulated Redux frame
+ {
+ functionName: "rootReducer",
+ filename: "https://myfile.com/loader.js -> https://myfile.com/redux.js",
+ lineNumber: 2,
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://myfile.com/loader.js -> https://myfile.com/loadee.js",
+ lineNumber: 10,
+ },
+ // Simulated react frames
+ ...(Array.from({length: REACT_FRAMES_COUNT}, (_, i) => ({
+ functionName: "internalReact" + (REACT_FRAMES_COUNT - i),
+ filename: "https://myfile.com/loader.js -> https://myfile.com/react.js",
+ lineNumber: Number(i.toString().repeat(2)),
+ }))),
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 10,
+ columnNumber: 3,
+ functionName: "onClick",
+ },
+ ];
+
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props), window.document.body);
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered SmartTrace has an element");
+
+ isDeeply(getStacktraceText(traceEl), [
+ `<anonymous> https://myfile.com/mahscripts.js:55`,
+ `rootReducer Redux`,
+ `loadFunc https://myfile.com/loadee.js:10`,
+ `▶︎ React 10`,
+ `onClick https://myfile.com/mahscripts.js:10`,
+ ], "React frames are grouped - Redux frame is not");
+
+ info("Expand React group");
+ let onReactGroupExpanded = waitFor(() =>
+ traceEl.querySelector(".frames-group.expanded"));
+ traceEl.querySelector(".group").click();
+ await onReactGroupExpanded;
+
+ isDeeply(getStacktraceText(traceEl), [
+ `<anonymous> https://myfile.com/mahscripts.js:55`,
+ `rootReducer Redux`,
+ `loadFunc https://myfile.com/loadee.js:10`,
+ `â–¼ React 10`,
+ `| internalReact10`,
+ `| internalReact9`,
+ `| internalReact8`,
+ `| internalReact7`,
+ `| internalReact6`,
+ `| internalReact5`,
+ `| internalReact4`,
+ `| internalReact3`,
+ `| internalReact2`,
+ `| internalReact1`,
+ `onClick https://myfile.com/mahscripts.js:10`,
+ ], "React frames can be expanded");
+
+ info("Collapse React group");
+ onReactGroupExpanded = waitFor(() =>
+ !traceEl.querySelector(".frames-group.expanded"));
+ traceEl.querySelector(".group").click();
+ await onReactGroupExpanded;
+
+ isDeeply(getStacktraceText(traceEl), [
+ `<anonymous> https://myfile.com/mahscripts.js:55`,
+ `rootReducer Redux`,
+ `loadFunc https://myfile.com/loadee.js:10`,
+ `▶︎ React 10`,
+ `onClick https://myfile.com/mahscripts.js:10`,
+ ], "React frames can be collapsed");
+ });
+
+ function getStacktraceText(traceElement) {
+ return Array.from(traceElement.querySelectorAll(".frame, .frames-group")).map(el => {
+ // If it's a group, we want to append an arrow representing the group state
+ if (el.classList.contains("frames-group")) {
+ const arrow = el.classList.contains("expanded") ? "▼" : "▶︎";
+ const content = el.querySelector(".group").textContent.trim();
+ return `${arrow} ${content}`;
+ }
+
+ const title = el.querySelector(".title");
+ if (el.closest(".frames-group")) {
+ return `| ${title.textContent}`;
+ }
+
+ const location = el.querySelector(".location");
+ return `${title.textContent} ${location.textContent}`;
+ });
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html b/devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html
new file mode 100644
index 0000000000..1beade0c0c
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html
@@ -0,0 +1,290 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SmartTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/SmartTrace"));
+ ok(SmartTrace, "Got the SmartTrace factory");
+
+ add_task(async function testHappyPath() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/bundle.js",
+ lineNumber: 1,
+ columnNumber: 10,
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://myfile.com/bundle.js",
+ lineNumber: 2,
+ },
+ ];
+
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ initialRenderDelay: 2000,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ const newLine = Number(line.toString().repeat(2));
+ // Resolve immediately.
+ callback({
+ url: "https://bugzilla.mozilla.org/original.js",
+ line: newLine,
+ column,
+ });
+ return () => {};
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props),
+ window.document.body.querySelector("#s1"));
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered SmartTrace has an element");
+
+ const frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 2, "SmartTrace has 2 frames");
+
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ location: "original.js:11",
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:11",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "loadFunc",
+ location: "original.js:22",
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:22",
+ });
+
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testSlowSourcemapService() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "last",
+ lineNumber: 1,
+ columnNumber: 10,
+ },
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "first",
+ lineNumber: 2,
+ columnNumber: 10,
+ },
+ ];
+
+ const sourcemapTimeout = 2000;
+ const initialRenderDelay = 300;
+ let onReadyCount = 0;
+
+ const props = {
+ stacktrace,
+ initialRenderDelay,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ // Resolve after a while.
+ setTimeout(() => {
+ const newLine = Number(line.toString().repeat(2));
+ callback({
+ url: "https://myfile.com/react.js",
+ line: newLine,
+ column,
+ });
+ }, sourcemapTimeout)
+
+ return () => {};
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props),
+ window.document.body.querySelector("#s2"));
+
+ let traceEl = ReactDOM.findDOMNode(trace);
+ ok(!traceEl, "Nothing was rendered at first");
+ is(onReadyCount, 0, "onReady isn't called if SmartTrace isn't rendered");
+
+ info("Wait for the initial delay to be over");
+ await new Promise(res => setTimeout(res, initialRenderDelay));
+
+ traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "The trace was rendered");
+
+ let frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 2, "SmartTrace has 2 frames");
+
+ info("Check that the original frames are displayed after the initial delay");
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "last",
+ location: "https://myfile.com/bundle.js:1",
+ tooltip: "View source in Debugger → https://myfile.com/bundle.js:1",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "first",
+ location: "https://myfile.com/bundle.js:2",
+ tooltip: "View source in Debugger → https://myfile.com/bundle.js:2",
+ });
+
+ is(onReadyCount, 1, "onReady was called once");
+
+ info("Check the the sourcemapped version is rendered after the sourcemapTimeout");
+ await waitFor(() => !!traceEl.querySelector(".group"));
+
+ frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ is(frameEls.length, 0, "SmartTrace has no frame");
+
+ const groups = Array.from(traceEl.querySelectorAll(".group"));
+ is(groups.length, 1, "SmartTrace has a group");
+ is(groups[0].textContent.trim(), "React 2", "A collapsed React group is displayed");
+
+ is(onReadyCount, 1, "onReady was only called once");
+ });
+
+ add_task(async function testFlakySourcemapService() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "last",
+ lineNumber: 1,
+ columnNumber: 10,
+ },
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "pending",
+ lineNumber: 2,
+ columnNumber: 10,
+ },
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "first",
+ lineNumber: 3,
+ columnNumber: 10,
+ },
+ ];
+
+ const initialRenderDelay = 300;
+ const onSourceMapResultDebounceDelay = 50;
+ let onReadyCount = 0;
+
+ const props = {
+ stacktrace,
+ initialRenderDelay,
+ onSourceMapResultDebounceDelay,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ // Don't call the callback for the second frame to simulate a flaky sourcemap
+ // service request.
+ if (line === 2) {
+ return () => {};
+ }
+
+ const newLine = Number(line.toString().repeat(2));
+ callback({
+ url: `https://myfile.com/file-${line}.js`,
+ line: newLine,
+ column,
+ });
+ return () => {};
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props),
+ window.document.body.querySelector("#s3"));
+
+ let traceEl = ReactDOM.findDOMNode(trace);
+ ok(!traceEl, "Nothing was rendered at first");
+ is(onReadyCount, 0, "onReady isn't called if SmartTrace isn't rendered");
+
+ info("Wait for the initial delay + debounce to be over");
+ await waitFor(() => {
+ const el = ReactDOM.findDOMNode(trace)
+ return el && el.textContent.includes("file-1.js");
+ });
+
+ traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "The trace was rendered");
+
+ const frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 3, "SmartTrace has 3 frames");
+
+ info("Check that the original frames are displayed even if there's no sourcemap " +
+ "response for some frames");
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "last",
+ location: "file-1.js:11",
+ tooltip: "View source in Debugger → https://myfile.com/file-1.js:11",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "pending",
+ location: "bundle.js:2",
+ tooltip: "View source in Debugger → https://myfile.com/bundle.js:2",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[2],
+ functionName: "first",
+ location: "file-3.js:33",
+ tooltip: "View source in Debugger → https://myfile.com/file-3.js:33",
+ });
+
+ is(onReadyCount, 1, "onReady was only called once");
+ });
+
+};
+</script>
+<section id=s1></section>
+<section id=s2></section>
+<section id=s3></section>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_smart-trace.html b/devtools/client/shared/components/test/chrome/test_smart-trace.html
new file mode 100644
index 0000000000..eedb72cc13
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_smart-trace.html
@@ -0,0 +1,172 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <section id=s1></section>
+ <section id=s2></section>
+ <section id=s3></section>
+ <section id=s4></section>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SmartTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/SmartTrace"));
+ ok(SmartTrace, "Got the SmartTrace factory");
+
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10,
+ functionName: null,
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://myfile.com/loader.js -> https://myfile.com/loadee.js",
+ lineNumber: 10,
+ },
+ ];
+
+ add_task(async function testBasic() {
+ info("Check basic rendering");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ };
+ await renderSmartTraceAndAssertContent(
+ window.document.body.querySelector("#s1"),
+ props
+ );
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testZeroDelay() {
+ info("Check rendering with source map service and 0 initial delay");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ initialRenderDelay: 0,
+ sourceMapURLService: {
+ subscribeByLocation: () => {}
+ },
+ };
+ await renderSmartTraceAndAssertContent(
+ window.document.body.querySelector("#s2"),
+ props
+ );
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testNullDelay() {
+ info("Check rendering with source map service and null initial delay");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ initialRenderDelay: 0,
+ sourceMapURLService: {
+ subscribeByLocation: () => {}
+ },
+ };
+ await renderSmartTraceAndAssertContent(
+ window.document.body.querySelector("#s3"),
+ props
+ );
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testDelay() {
+ info("Check rendering with source map service and initial delay");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ initialRenderDelay: 500,
+ sourceMapURLService: {
+ subscribeByLocation: () => {}
+ },
+ };
+ const el = window.document.body.querySelector("#s4");
+ await renderSmartTraceAndAssertContent(
+ el,
+ props,
+ false
+ );
+ is(onReadyCount, 0, "onReady wasn't called at first");
+ info(`Wait for ${props.initialRenderDelay}ms so the stacktrace should be rendered`)
+ await new Promise(res => setTimeout(res, props.initialRenderDelay))
+ is(onReadyCount, 1, "onReady was called after waiting for the initial delay");
+ assertRenderedElementContent(el);
+ });
+
+ async function renderSmartTraceAndAssertContent(el, props, shouldBeRendered = true) {
+ let trace;
+ await new Promise(resolve => {
+ trace = ReactDOM.render(SmartTrace(props), el, resolve);
+ });
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+
+ if (!shouldBeRendered) {
+ ok(!traceEl, "SmartTrace wasn't rendered initially");
+ return;
+ }
+
+ ok(traceEl, "Rendered SmartTrace has an element");
+ assertRenderedElementContent(traceEl);
+ }
+
+ function assertRenderedElementContent(el) {
+ const frameEls = Array.from(el.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 2, "SmartTrace has 2 frames");
+
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ location: "https://myfile.com/mahscripts.js:55",
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55",
+ });
+
+ // Check the third frame, the source should be parsed into a valid source URL
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "loadFunc",
+ location: "https://myfile.com/loadee.js:10",
+ tooltip: "View source in Debugger → https://myfile.com/loadee.js:10",
+ });
+ }
+
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html b/devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html
new file mode 100644
index 0000000000..7535d4d2df
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html
@@ -0,0 +1,98 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace with source maps
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function () {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/StackTrace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(async function () {
+ const stacktrace = [
+ {
+ filename: "https://bugzilla.mozilla.org/bundle.js",
+ lineNumber: 99,
+ columnNumber: 10
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://bugzilla.mozilla.org/bundle.js",
+ lineNumber: 108,
+ }
+ ];
+
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ const newLine = line === 99 ? 1 : 7;
+ // Resolve immediately.
+ callback({
+ url: "https://bugzilla.mozilla.org/original.js",
+ line: newLine,
+ column,
+ });
+
+ return () => {}
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(StackTrace(props), window.document.body);
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ const frameEls = Array.from(traceEl.childNodes)
+ .filter(n => n.className && n.className.includes("frame"));
+ ok(frameEls, "Rendered StackTrace has frames");
+ is(frameEls.length, 2, "StackTrace has 2 frames");
+
+ checkFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ source: "https://bugzilla.mozilla.org/original.js",
+ file: "original.js",
+ line: 1,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:1:10",
+ });
+
+ checkFrameString({
+ el: frameEls[1],
+ functionName: "loadFunc",
+ source: "https://bugzilla.mozilla.org/original.js",
+ file: "original.js",
+ line: 7,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:7",
+ });
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_stack-trace.html b/devtools/client/shared/components/test/chrome/test_stack-trace.html
new file mode 100644
index 0000000000..56d9288f06
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_stack-trace.html
@@ -0,0 +1,100 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/StackTrace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(async function() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10,
+ },
+ {
+ asyncCause: "because",
+ functionName: "loadFunc",
+ filename: "https://myfile.com/loadee.js",
+ lineNumber: 10,
+ },
+ ];
+
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ };
+
+ const trace = ReactDOM.render(StackTrace(props), window.document.body);
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ const frameEls = Array.from(traceEl.childNodes)
+ .filter(n => n.className && n.className.includes("frame"));
+ ok(frameEls, "Rendered StackTrace has frames");
+ is(frameEls.length, 3, "StackTrace has 3 frames");
+
+ // Check the top frame, function name should be anonymous
+ checkFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ source: "https://myfile.com/mahscripts.js",
+ file: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check the async cause node
+ is(frameEls[1].className, "frame-link-async-cause",
+ "Async cause has the right class");
+ is(frameEls[1].textContent, "(Async: because)", "Async cause has the right label");
+
+ // Check the third frame, the source should be parsed into a valid source URL
+ checkFrameString({
+ el: frameEls[2],
+ functionName: "loadFunc",
+ source: "https://myfile.com/loadee.js",
+ file: "https://myfile.com/loadee.js",
+ line: 10,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/loadee.js:10",
+ });
+
+ // Check the tabs and newlines in the stack trace textContent
+ const traceText = traceEl.textContent;
+ const traceLines = traceText.split("\n");
+ ok(!!traceLines.length, "There are newlines in the stack trace text");
+ is(traceLines.pop(), "", "There is a newline at the end of the stack trace text");
+ is(traceLines.length, 3, "The stack trace text has 3 lines");
+ ok(traceLines.every(l => l[0] == "\t"), "Every stack trace line starts with tab");
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tabs_accessibility.html b/devtools/client/shared/components/test/chrome/test_tabs_accessibility.html
new file mode 100644
index 0000000000..4d0ea6ef96
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tabs_accessibility.html
@@ -0,0 +1,82 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test tabs accessibility.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component accessibility test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const InspectorTabPanel = createFactory(browserRequire("devtools/client/inspector/components/InspectorTabPanel"));
+ const Tabbar =
+ createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
+ const tabbar = Tabbar();
+ const tabbarReact = ReactDOM.render(tabbar, window.document.body);
+ const tabbarEl = ReactDOM.findDOMNode(tabbarReact);
+
+ // Setup for InspectorTabPanel
+ const tabpanels = document.createElement("div");
+ tabpanels.id = "tabpanels";
+ document.body.appendChild(tabpanels);
+
+ await addTabWithPanel(0);
+ await addTabWithPanel(1);
+
+ const tabAnchors = tabbarEl.querySelectorAll("li.tabs-menu-item a");
+
+ is(tabAnchors[0].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[0].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[0].getAttribute("aria-selected"), "true", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[0].getAttribute("aria-controls"), "sidebar-0-panel", "Anchor aria-controls is set correctly");
+ is(tabAnchors[1].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[1].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "false", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[1].getAttribute("aria-controls"), "sidebar-1-panel", "Anchor aria-controls is set correctly");
+
+ await setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ activeTab: 1
+ }));
+
+ is(tabAnchors[0].getAttribute("aria-selected"), "false", "Anchor aria-selected is reset correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "true", "Anchor aria-selected is reset correctly");
+
+ function addTabWithPanel(tabId) {
+ // Setup for InspectorTabPanel
+ const panel = document.createElement("div");
+ panel.id = `sidebar-${tabId}`;
+ document.body.appendChild(panel);
+
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({
+ id: `sidebar-${tabId}`,
+ title: `tab-${tabId}`,
+ panel: InspectorTabPanel
+ }),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tabs_menu.html b/devtools/client/shared/components/test/chrome/test_tabs_menu.html
new file mode 100644
index 0000000000..cc4638e05a
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tabs_menu.html
@@ -0,0 +1,84 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html class="theme-light">
+<!--
+Test all-tabs menu.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component All-tabs menu test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/skin/variables.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/skin/common.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/content/shared/components/tabs/Tabs.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/content/inspector/components/InspectorTabPanel.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { Component, createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const Tabbar = createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
+
+ // Create container for the TabBar. Set smaller width
+ // to ensure that tabs won't fit and the all-tabs menu
+ // needs to appear.
+ const tabBarBox = document.createElement("div");
+ tabBarBox.style.width = "200px";
+ tabBarBox.style.height = "200px";
+ tabBarBox.style.border = "1px solid lightgray";
+ document.body.appendChild(tabBarBox);
+
+ // Render the tab-bar.
+ const tabbar = Tabbar({
+ showAllTabsMenu: true,
+ });
+
+ const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
+
+ class TabPanelClass extends Component {
+ render() {
+ return dom.div({}, "content");
+ }
+ }
+
+ // Test panel.
+ const TabPanel = createFactory(TabPanelClass);
+
+ // Create a few panels.
+ await addTabWithPanel(1);
+ await addTabWithPanel(2);
+ await addTabWithPanel(3);
+ await addTabWithPanel(4);
+ await addTabWithPanel(5);
+
+ // Make sure the all-tabs menu is there.
+ const allTabsMenu = tabBarBox.querySelector(".all-tabs-menu");
+ ok(allTabsMenu, "All-tabs menu must be rendered");
+
+ function addTabWithPanel(tabId) {
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({id: `${tabId}`,
+ title: `tab-${tabId}`, panel: TabPanel}),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree-view_01.html b/devtools/client/shared/components/test/chrome/test_tree-view_01.html
new file mode 100644
index 0000000000..0acae4c1dc
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree-view_01.html
@@ -0,0 +1,290 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+- License, v. 2.0. If a copy of the MPL was not distributed with this
+- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that TreeView component has working keyboard interactions.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>TreeView component keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = function() {
+ try {
+ const { a, button, div } =
+ require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ findRenderedDOMComponentWithTag,
+ scryRenderedDOMComponentsWithClass,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const TreeView =
+ browserRequire("devtools/client/shared/components/tree/TreeView");
+
+ const _props = {
+ ...TEST_TREE_VIEW_INTERFACE,
+ renderValue: props => {
+ return (props.value === "C" ?
+ div({},
+ props.value + " ",
+ a({ href: "#" }, "Focusable 1"),
+ button({ }, "Focusable 2")) :
+ props.value + ""
+ );
+ },
+ };
+ const treeView = React.createElement(TreeView, _props);
+ const tree = ReactDOM.render(treeView, document.body);
+ const treeViewEl = findRenderedDOMComponentWithClass(tree, "treeTable");
+ const rows = scryRenderedDOMComponentsWithClass(tree, "treeRow");
+ const defaultFocus = treeViewEl.ownerDocument.body;
+
+ function blurEl(el) {
+ // Simulate.blur does not seem to update the activeElement.
+ el.blur();
+ }
+
+ function focusEl(el) {
+ // Simulate.focus does not seem to update the activeElement.
+ el.focus();
+ }
+
+ const tests = [{
+ name: "Test default TreeView state. Keyboard focus is set to document " +
+ "body by default.",
+ state: { selected: null, active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Selected row must be set to the first row on initial focus. " +
+ "Keyboard focus should be set on TreeView's conatiner.",
+ action: () => {
+ focusEl(treeViewEl);
+ Simulate.click(rows[0]);
+ },
+ activeElement: treeViewEl,
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should remain set even when the treeView is " +
+ "blured. Keyboard focus should be set back to document body.",
+ action: () => blurEl(treeViewEl),
+ state: { selected: "/B" },
+ activeElement: defaultFocus,
+ }, {
+ name: "Selected row must be re-set again to the first row on initial " +
+ "focus. Keyboard focus should be set on treeView's conatiner.",
+ action: () => focusEl(treeViewEl),
+ activeElement: treeViewEl,
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should be updated to last on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/D" },
+ }, {
+ name: "Selected row should remain on last on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/D" },
+ }, {
+ name: "Selected row should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should be updated to first on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should remain on first on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should move to the next matching row with first letter navigation.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "C" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should not change when there are no more visible nodes matching first letter navigation.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "C" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should be updated to last on End.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "End" }},
+ state: { selected: "/D" },
+ }, {
+ name: "Selected row should be updated to first on Home.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Home" }},
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should be set as active on Enter.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Enter" }},
+ state: { selected: "/B", active: "/B" },
+ activeElement: treeViewEl,
+ }, {
+ name: "Active row should be unset on Escape.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Escape" }},
+ state: { selected: "/B", active: null },
+ }, {
+ name: "Selected row should be set as active on Space.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: " " }},
+ state: { selected: "/B", active: "/B" },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should unset when focus leaves the treeView.",
+ action: () => blurEl(treeViewEl),
+ state: { selected: "/B", active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on treeView's conatiner on focus.",
+ action: () => focusEl(treeViewEl),
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/C", active: null },
+ }, {
+ name: "Selected row should be set as active on Enter. Keyboard focus " +
+ "should be set on the first focusable element inside the row, if " +
+ "available.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Enter" }},
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should be set to next tabbable element inside " +
+ "the active row on Tab.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the row when focused on last " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the row when focused on first " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Active row should be unset on Escape. Focus should move back to " +
+ "the treeView container.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Escape" }},
+ state: { selected: "/C", active: null },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be set as active on Space. Keyboard focus " +
+ "should be set on the first focusable element inside the row, if " +
+ "available.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: " " }},
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Selected row should remain set even when the treeView is " +
+ "blured. Keyboard focus should be set back to document body.",
+ action: () => treeViewEl.ownerDocument.activeElement.blur(),
+ state: { selected: "/C", active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on treeView's conatiner on focus.",
+ action: () => focusEl(treeViewEl),
+ state: { selected: "/C", active: null },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be set as active on Space. Keyboard focus " +
+ "should be set on the first focusable element inside the row, if " +
+ "available.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: " " }},
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Selected row should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/B", active: null },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be set as active on Enter.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Enter" }},
+ state: { selected: "/B", active: "/B" },
+ activeElement: treeViewEl,
+ }, {
+ name: "Keyboard focus should move to another focusable element outside " +
+ "of the treeView when there's nothing to focus on inside the row.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { selected: "/B", active: null },
+ activeElement: treeViewEl.ownerDocument.documentElement,
+ }];
+
+ for (const test of tests) {
+ const { action, event, state, name } = test;
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ Simulate[type](el, options);
+ } else if (action) {
+ action();
+ }
+
+ if (test.activeElement) {
+ is(treeViewEl.ownerDocument.activeElement, test.activeElement,
+ "Focus is set correctly.");
+ }
+
+ for (const key in state) {
+ is(tree.state[key], state[key], `${key} state is correct.`);
+ }
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree-view_02.html b/devtools/client/shared/components/test/chrome/test_tree-view_02.html
new file mode 100644
index 0000000000..77c5934a66
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree-view_02.html
@@ -0,0 +1,136 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+- License, v. 2.0. If a copy of the MPL was not distributed with this
+- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that TreeView component filtering works with keyboard.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>TreeView component filtering keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <link rel="stylesheet" href="chrome://devtools/content/shared/components/tree/TreeView.css" type="text/css">
+ <style>
+ .treeRow.hide {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = function() {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ scryRenderedDOMComponentsWithClass,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const TreeView =
+ browserRequire("devtools/client/shared/components/tree/TreeView");
+
+ function testKeyboardInteraction(tree, treeViewEl, rows) {
+ // Expected tree when filtered (C is filtered)
+ //
+ // A
+ // |-- B
+ // `-- D
+ is(window.getComputedStyle(rows[1]).getPropertyValue("display"), "none",
+ "Row C must be hidden by default.");
+
+ const tests = [{
+ name: "Selected row must be set to the first row on initial focus. " +
+ "Keyboard focus must be set on TreeView's conatiner.",
+ action: () => {
+ Simulate.click(rows[0]);
+ },
+ activeElement: treeViewEl,
+ state: { selected: "/B" },
+ }, {
+ name: "Selecting next row must skip hidden row on ArrowDown.",
+ event: {
+ type: "keyDown",
+ el: treeViewEl,
+ options: { key: "ArrowDown" },
+ },
+ state: { selected: "/D" },
+ }, {
+ name: "Selecting previous row must be skip hidden row on ArrowUp.",
+ event: {
+ type: "keyDown",
+ el: treeViewEl,
+ options: { key: "ArrowUp" },
+ },
+ state: { selected: "/B" },
+ }];
+
+ for (const test of tests) {
+ const { action, event, state, name } = test;
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ Simulate[type](el, options);
+ } else if (action) {
+ action();
+ }
+
+ for (const key in state) {
+ is(tree.state[key], state[key], `${key} state is correct.`);
+ }
+ }
+ }
+
+ info("Test hiding rows via decorator.");
+ const props = {
+ ...TEST_TREE_VIEW_INTERFACE,
+ decorator: {
+ getRowClass: ({ label }) => {
+ if (label === "C") {
+ return ["hide"];
+ }
+ return [];
+ }
+ }
+ };
+ let treeView = React.createElement(TreeView, props);
+ let tree = ReactDOM.render(treeView, document.body);
+ let treeViewEl = findRenderedDOMComponentWithClass(tree, "treeTable");
+ let rows = scryRenderedDOMComponentsWithClass(tree, "treeRow");
+
+ testKeyboardInteraction(tree, treeViewEl, rows);
+
+ // Remove TreeView component.
+ ReactDOM.unmountComponentAtNode(document.body);
+
+ info("Test hiding rows via onFilter.");
+ props.decorator = null;
+ props.onFilter = ({ label }) => {
+ console.log(`onFILTER ${label !== "C"}`)
+ return label !== "C";
+ };
+ treeView = React.createElement(TreeView, props);
+ tree = ReactDOM.render(treeView, document.body);
+ treeViewEl = findRenderedDOMComponentWithClass(tree, "treeTable");
+ rows = scryRenderedDOMComponentsWithClass(tree, "treeRow");
+
+ testKeyboardInteraction(tree, treeViewEl, rows);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_01.html b/devtools/client/shared/components/test/chrome/test_tree_01.html
new file mode 100644
index 0000000000..0740c8957e
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_01.html
@@ -0,0 +1,68 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test trees get displayed with the items in correct order and at the correct
+depth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ ok(React, "Should get React");
+ ok(Tree, "Should get Tree");
+
+ const t = Tree(TEST_TREE_INTERFACE);
+ ok(t, "Should be able to create Tree instances");
+
+ const tree = ReactDOM.render(t, window.document.body);
+ ok(tree, "Should be able to mount Tree instances");
+ isAccessibleTree(tree);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Should get the items rendered and indented as expected");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_02.html b/devtools/client/shared/components/test/chrome/test_tree_02.html
new file mode 100644
index 0000000000..f538965572
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_02.html
@@ -0,0 +1,49 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that collapsed subtrees aren't rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ isAccessibleTree(tree);
+ TEST_TREE.expanded = new Set("MNO".split(""));
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Collapsed subtrees shouldn't be rendered");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_03.html b/devtools/client/shared/components/test/chrome/test_tree_03.html
new file mode 100644
index 0000000000..6ebefa1fb7
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_03.html
@@ -0,0 +1,50 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Tree's autoExpandDepth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 1
+ })), window.document.body);
+
+ isAccessibleTree(tree);
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Tree should be auto expanded one level");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_04.html b/devtools/client/shared/components/test/chrome/test_tree_04.html
new file mode 100644
index 0000000000..2213f72497
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_04.html
@@ -0,0 +1,133 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we only render visible tree items.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ function getSpacerHeights() {
+ return {
+ top: document.querySelector(".tree > div:first-of-type").clientHeight,
+ bottom: document.querySelector(".tree > div:last-of-type").clientHeight,
+ };
+ }
+
+ const ITEM_HEIGHT = 3;
+
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const tree = ReactDOM.render(
+ Tree(Object.assign({}, TEST_TREE_INTERFACE, { itemHeight: ITEM_HEIGHT })),
+ window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ await setState(tree, {
+ height: 3 * ITEM_HEIGHT,
+ scroll: 1 * ITEM_HEIGHT
+ });
+
+ isAccessibleTree(tree);
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ ], "Tree should show the 2nd, 3rd, and 4th items + buffer of 1 item at each end");
+
+ let spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has the correct height");
+ is(spacers.bottom, 10 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ await setState(tree, {
+ height: 2 * ITEM_HEIGHT,
+ scroll: 3 * ITEM_HEIGHT
+ });
+
+ isAccessibleTree(tree);
+ isRenderedTree(document.body.textContent, [
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ ], "Tree should show the 4th and 5th item + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 2 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 9 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ // Set height to 2 items + 1 pixel at each end, scroll so that 4 items are visible
+ // (2 fully, 2 partially with 1 visible pixel)
+ await setState(tree, {
+ height: 2 * ITEM_HEIGHT + 2,
+ scroll: 3 * ITEM_HEIGHT - 1
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ ], "Tree should show the 4 visible items + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 1 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 8 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ await setState(tree, {
+ height: 20 * ITEM_HEIGHT,
+ scroll: 0
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Tree should show all rows");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has zero height");
+ is(spacers.bottom, 0, "Bottom spacer has zero height");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_05.html b/devtools/client/shared/components/test/chrome/test_tree_05.html
new file mode 100644
index 0000000000..5427a1bd8d
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_05.html
@@ -0,0 +1,195 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test focusing with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { onFocus: x => renderTree({ focused: x }) },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+ const treeElem = document.querySelector(".tree");
+
+ isAccessibleTree(tree);
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ renderTree({ focused: "G" });
+ isAccessibleTree(tree, { hasActiveDescendant: true });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:true",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "G should be focused");
+
+ // When tree gets focus by means other than mouse, do not set first node as
+ // focused node when there is already a focused node.
+ Simulate.focus(treeElem);
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:true",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "G should remain focused");
+
+ // Click the first tree node
+ document.querySelector(".tree-node").click();
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "A should be focused");
+
+ // Mouse down and mouse up events set tree "mouseDown" state correctly.
+ ok(!tree.state.mouseDown, "Mouse down state is not set.");
+ Simulate.mouseDown(document.querySelector(".tree-node"));
+ ok(tree.state.mouseDown, "Mouse down state is set.");
+ Simulate.mouseUp(document.querySelector(".tree-node"));
+ ok(!tree.state.mouseDown, "Mouse down state is reset.");
+
+ // Unset focused tree state.
+ renderTree({ focused: null });
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "No node should be focused");
+
+ // When tree gets focus while mouse is down, do not set first node as
+ // focused node.
+ Simulate.mouseDown(document.querySelector(".tree-node"));
+ Simulate.focus(treeElem);
+ Simulate.mouseUp(document.querySelector(".tree-node"));
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "No node should have been focused");
+
+ // When tree gets focus by means other than mouse, set first node as focused
+ // node if no nodes are focused.
+ Simulate.focus(treeElem);
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "A should be focused");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_06.html b/devtools/client/shared/components/test/chrome/test_tree_06.html
new file mode 100644
index 0000000000..c8d1aa5e9f
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_06.html
@@ -0,0 +1,340 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test keyboard navigation with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { onFocus: x => renderTree({ focused: x }) },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+
+ isAccessibleTree(tree);
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // UP ----------------------------------------------------------------------
+
+ info("Up to the previous sibling.");
+ renderTree({ focused: "L" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, K should be focused.");
+
+ info("Up to the parent.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, E should be focused.");
+
+ info("Try and navigate up, past the first item.");
+ renderTree({ focused: "A" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, A should be focused and we shouldn't have overflowed past it.");
+
+ // DOWN --------------------------------------------------------------------
+
+ info("Down to next sibling.");
+ renderTree({ focused: "K" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, L should be focused.");
+
+ info("Down to parent's next sibling.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:true",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, F should be focused.");
+
+ info("Try and go down past the last item.");
+ renderTree({ focused: "O" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the DOWN, O should still be focused " +
+ "and we shouldn't have overflowed past it.");
+
+ // LEFT --------------------------------------------------------------------
+
+ info("Left to go to parent.");
+ renderTree({ focused: "L" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E should be focused.");
+
+ info("Left to collapse children.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E's children should be collapsed.");
+
+ // RIGHT -------------------------------------------------------------------
+
+ info("Right to expand children.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, E's children should be expanded again.");
+
+ info("Right on already expanded node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT on already expanded node, E should remain focused.");
+
+ info("Right when preventNavigationOnArrowRight is unset to go to next item.");
+ renderTree({ focused: "E", preventNavigationOnArrowRight: false });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, K should be focused.");
+
+ // Check that keys are ignored if any modifier is present.
+ const keysWithModifier = [
+ { key: "ArrowDown", altKey: true },
+ { key: "ArrowDown", ctrlKey: true },
+ { key: "ArrowDown", metaKey: true },
+ { key: "ArrowDown", shiftKey: true },
+ ];
+ await forceRender(tree);
+
+ for (const key of keysWithModifier) {
+ Simulate.keyDown(document.querySelector(".tree"), key);
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After DOWN + (alt|ctrl|meta|shift), K should remain focused.");
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_07.html b/devtools/client/shared/components/test/chrome/test_tree_07.html
new file mode 100644
index 0000000000..2e763aaf20
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_07.html
@@ -0,0 +1,69 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that arrows get the open attribute when their item's children are expanded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const Tree =
+ React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const treeProps = Object.assign({}, TEST_TREE_INTERFACE, {
+ renderItem: (item, depth, focused, arrow) => {
+ return dom.div(
+ {
+ id: item,
+ style: { marginLeft: depth * 16 + "px" }
+ },
+ arrow,
+ item
+ );
+ }
+ });
+ const tree = ReactDOM.render(Tree(treeProps), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ await forceRender(tree);
+
+ let arrows = document.querySelectorAll(".arrow");
+ for (const a of arrows) {
+ ok(a.classList.contains("open"), "Every arrow should be open.");
+ }
+
+ TEST_TREE.expanded = new Set();
+ await forceRender(tree);
+
+ arrows = document.querySelectorAll(".arrow");
+ for (const a of arrows) {
+ ok(!a.classList.contains("open"), "Every arrow should be closed.");
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_08.html b/devtools/client/shared/components/test/chrome/test_tree_08.html
new file mode 100644
index 0000000000..cfdff8090d
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_08.html
@@ -0,0 +1,61 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is clicked, it steals focus from
+other inputs.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { onFocus: x => renderTree({ focused: x }) },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+
+ input.focus();
+ is(document.activeElement, input, "The text input should be focused.");
+
+ document.querySelector(".tree-node").click();
+ await forceRender(tree);
+
+ isnot(document.activeElement, input,
+ "The input should have had it's focus stolen by clicking on a tree item.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_09.html b/devtools/client/shared/components/test/chrome/test_tree_09.html
new file mode 100644
index 0000000000..4d6a1010b5
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_09.html
@@ -0,0 +1,85 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ let numberOfExpands = 0;
+ let lastExpandedItem = null;
+
+ let numberOfCollapses = 0;
+ let lastCollapsedItem = null;
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ autoExpandDepth: 0,
+ onExpand: item => {
+ lastExpandedItem = item;
+ numberOfExpands++;
+ TEST_TREE.expanded.add(item);
+ },
+ onCollapse: item => {
+ lastCollapsedItem = item;
+ numberOfCollapses++;
+ TEST_TREE.expanded.delete(item);
+ },
+ onFocus: item => renderTree({ focused: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree({ focused: "A" });
+
+ is(lastExpandedItem, null);
+ is(lastCollapsedItem, null);
+
+ // Expand "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ is(lastExpandedItem, "A", "Our onExpand callback should have been fired.");
+ is(numberOfExpands, 1);
+
+ // Now collapse "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ await forceRender(tree);
+
+ is(lastCollapsedItem, "A", "Our onCollapsed callback should have been fired.");
+ is(numberOfCollapses, 1);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_10.html b/devtools/client/shared/components/test/chrome/test_tree_10.html
new file mode 100644
index 0000000000..7cda9e4348
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_10.html
@@ -0,0 +1,57 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { autoExpandDepth: 1 },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ renderTree({ focused: "A" });
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Should have auto-expanded one level.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_11.html b/devtools/client/shared/components/test/chrome/test_tree_11.html
new file mode 100644
index 0000000000..612a851018
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_11.html
@@ -0,0 +1,100 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is focused by arrow key, the view is scrolled.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ .tree {
+ height: 30px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ itemHeight: 10,
+ onFocus: item => renderTree({ focused: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree({ focused: "K" });
+
+ tree.setState({ scroll: 10 });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ ], "Should render initial correctly");
+
+ await new Promise(resolve => {
+ const treeElem = document.querySelector(".tree");
+ treeElem.addEventListener("scroll", function onScroll() {
+ dumpn("Got scroll event");
+ treeElem.removeEventListener("scroll", onScroll);
+ resolve();
+ });
+
+ dumpn("Sending ArrowDown key");
+ Simulate.keyDown(treeElem, { key: "ArrowDown" });
+ });
+
+ dumpn("Forcing re-render");
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ ], "Should have scrolled down one");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_12.html b/devtools/client/shared/components/test/chrome/test_tree_12.html
new file mode 100644
index 0000000000..4bcf7ef705
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_12.html
@@ -0,0 +1,146 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test keyboard navigation/activation with the VirtualizedTree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = {
+ ...TEST_TREE_INTERFACE,
+ onFocus: x => renderTree({ focused: x }),
+ ...props
+ };
+
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // Test Home key -----------------------------------------------------------
+
+ info("Press Home to move to the first node.");
+ renderTree({ focused: "L" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "Home" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the Home key, A should be focused.");
+
+ info("Press Home again when already on first node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "Home" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the Home key again, A should still be focused.");
+
+ // Test End key ------------------------------------------------------------
+
+ info("Press End to move to the last node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "End" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the End key, O should be focused.");
+
+ info("Press End again when already on last node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "End" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the End key again, O should still be focused.");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_13.html b/devtools/client/shared/components/test/chrome/test_tree_13.html
new file mode 100644
index 0000000000..183e144c82
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_13.html
@@ -0,0 +1,88 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test trees have the correct scroll position when they are resized.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <style>
+ .tree {
+ height: 50px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function() {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(
+ browserRequire("devtools/client/shared/components/VirtualizedTree"));
+ const ITEM_HEIGHT = 10;
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = {
+ ...TEST_TREE_INTERFACE,
+ itemHeight: ITEM_HEIGHT,
+ onFocus: item => renderTree({ focused: item }),
+ ...props
+ };
+ return ReactDOM.render(Tree(treeProps), document.body);
+ }
+
+ const tree = renderTree({ focused: "L" });
+ const treeEl = tree.refs.tree;
+
+ is(tree.state.scroll, 0, "Scroll position should be 0 by default");
+ is(treeEl.scrollTop, 0, "Tree scrollTop should be 0 by default");
+
+ info(`Focus on the next node and scroll by ${ITEM_HEIGHT}`);
+ Simulate.keyDown(treeEl, { key: "ArrowDown" });
+ await forceRender(tree);
+
+ is(tree.state.scroll, ITEM_HEIGHT, `Scroll position should now be ${ITEM_HEIGHT}`);
+ is(treeEl.scrollTop, ITEM_HEIGHT,
+ `Tree scrollTop should now be ${ITEM_HEIGHT}`);
+
+ info("Simulate window resize along with scroll back to top");
+ treeEl.scrollTo({ left: 0, top: 0 });
+ window.dispatchEvent(new Event("resize"));
+ await forceRender(tree);
+
+ is(tree.state.scroll, ITEM_HEIGHT,
+ `Scroll position should remain at ${ITEM_HEIGHT}`);
+ is(treeEl.scrollTop, ITEM_HEIGHT,
+ `Tree scrollTop should remain at ${ITEM_HEIGHT}`);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_14.html b/devtools/client/shared/components/test/chrome/test_tree_14.html
new file mode 100644
index 0000000000..d68d87d6c5
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_14.html
@@ -0,0 +1,245 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that Tree component has working keyboard interactions.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function() {
+ try {
+ const { a, button, div } =
+ require("devtools/client/shared/vendor/react-dom-factories");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ findRenderedDOMComponentWithTag,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(
+ browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ let gTree, gFocused, gActive;
+ function renderTree(props = {}) {
+ let toggle = true;
+ const treeProps = {
+ ...TEST_TREE_INTERFACE,
+ onFocus: x => {
+ gFocused = x;
+ renderTree({ focused: gFocused, active: gActive });
+ },
+ onActivate: x => {
+ gActive = x;
+ renderTree({ focused: gFocused, active: gActive });
+ },
+ renderItem: (x, depth, focused) => {
+ toggle = !toggle;
+ return toggle ?
+ (div(
+ {},
+ `${"-".repeat(depth)}${x}:${focused}`,
+ a({ href: "#" }, "Focusable 1"),
+ button({ }, "Focusable 2"),
+ "\n",
+ )
+ ) : `${"-".repeat(depth)}${x}:${focused}`;
+ },
+ ...props
+ };
+
+ gTree = ReactDOM.render(Tree(treeProps), document.body);
+ }
+
+ renderTree();
+ const els = {
+ get tree() {
+ // React will replace the tree via renderTree.
+ return findRenderedDOMComponentWithClass(gTree, "tree");
+ },
+ get anchor() {
+ // When tree node becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(gTree, "a");
+ },
+ get button() {
+ // When tree node becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(gTree, "button");
+ },
+ };
+
+ const tests = [{
+ name: "Test default Tree props. Keyboard focus is set to document body by default.",
+ props: { focused: undefined, active: undefined },
+ activeElement: document.body,
+ }, {
+ name: "Focused props must be set to the first node on initial focus. " +
+ "Keyboard focus should be set on the tree.",
+ action: () => els.tree.focus(),
+ activeElement: "tree",
+ props: { focused: "A" },
+ }, {
+ name: "Focused node should remain set even when the tree is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => els.tree.blur(),
+ props: { focused: "A" },
+ activeElement: document.body,
+ }, {
+ name: "Unset tree's focused prop.",
+ action: () => renderTree({ focused: null }),
+ props: { focused: null },
+ }, {
+ name: "Focused node must be re-set again to the first tree node on initial " +
+ "focus. Keyboard focus should be set on tree's conatiner.",
+ action: () => els.tree.focus(),
+ activeElement: "tree",
+ props: { focused: "A" },
+ }, {
+ name: "Focused node should be set as active on Enter.",
+ event: { type: "keyDown", el: "tree", options: { key: "Enter" }},
+ props: { focused: "A", active: "A" },
+ activeElement: "tree",
+ }, {
+ name: "Active node should be unset on Escape.",
+ event: { type: "keyDown", el: "tree", options: { key: "Escape" }},
+ props: { focused: "A", active: null },
+ }, {
+ name: "Focused node should be set as active on Space.",
+ event: { type: "keyDown", el: "tree", options: { key: " " }},
+ props: { focused: "A", active: "A" },
+ activeElement: "tree",
+ }, {
+ name: "Active node should unset when focus leaves the tree.",
+ action: () => els.tree.blur(),
+ props: { focused: "A", active: null },
+ activeElement: document.body,
+ }, {
+ name: "Keyboard focus should be set on tree's conatiner on focus.",
+ action: () => els.tree.focus(),
+ activeElement: "tree",
+ }, {
+ name: "Focused node should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: "tree", options: { key: "ArrowDown" }},
+ props: { focused: "M", active: null },
+ }, {
+ name: "Focused item should be set as active on Enter. Keyboard focus should be " +
+ "set on the first focusable element inside the tree node, if available.",
+ event: { type: "keyDown", el: "tree", options: { key: "Enter" }},
+ props: { focused: "M", active: "M" },
+ activeElement: "anchor",
+ }, {
+ name: "Keyboard focus should be set to next tabbable element inside the active " +
+ "node on Tab.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ props: { focused: "M", active: "M" },
+ activeElement: "button",
+ }, {
+ name: "Keyboard focus should wrap inside the tree node when focused on last " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ props: { focused: "M", active: "M" },
+ activeElement: "anchor",
+ }, {
+ name: "Keyboard focus should wrap inside the tree node when focused on first " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ props: { focused: "M", active: "M" },
+ activeElement: "button",
+ }, {
+ name: "Active tree node should be unset on Escape. Focus should move back to the " +
+ "tree container.",
+ event: { type: "keyDown", el: "tree", options: { key: "Escape" }},
+ props: { focused: "M", active: null },
+ activeElement: "tree",
+ }, {
+ name: "Focused node should be set as active on Space. Keyboard focus should be " +
+ "set on the first focusable element inside the tree node, if available.",
+ event: { type: "keyDown", el: "tree", options: { key: " " }},
+ props: { focused: "M", active: "M" },
+ activeElement: "anchor",
+ }, {
+ name: "Focused tree node should remain set even when the tree is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => document.activeElement.blur(),
+ props: { focused: "M", active: null, },
+ activeElement: document.body,
+ }, {
+ name: "Keyboard focus should be set on tree's conatiner on focus.",
+ action: () => els.tree.focus(),
+ props: { focused: "M", active: null },
+ activeElement: "tree",
+ }, {
+ name: "Focused tree node should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: "tree", options: { key: "ArrowUp" }},
+ props: { focused: "A", active: null },
+ }, {
+ name: "Focused item should be set as active on Enter.",
+ event: { type: "keyDown", el: "tree", options: { key: "Enter" }},
+ props: { focused: "A", active: "A" },
+ activeElement: "tree",
+ }, {
+ name: "Keyboard focus should move to another focusable element outside of the " +
+ "tree when there's nothing to focus on inside the tree node.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ props: { focused: "A", active: null },
+ activeElement: document.documentElement,
+ }];
+
+ for (const test of tests) {
+ const { action, event, props, name } = test;
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ const target = typeof el === "string" ? els[el] : el;
+ Simulate[type](target, options);
+ } else if (action) {
+ action();
+ }
+
+ await forceRender(gTree);
+
+ if (test.activeElement) {
+ const expected = typeof test.activeElement === "string" ?
+ els[test.activeElement] : test.activeElement;
+ // eslint-disable-next-line no-debugger
+ if (document.activeElement!==expected) {debugger;}
+ is(document.activeElement, expected, "Focus is set correctly.");
+ }
+
+ for (const key in props) {
+ is(gTree.props[key], props[key], `${key} prop is correct.`);
+ }
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_15.html b/devtools/client/shared/components/test/chrome/test_tree_15.html
new file mode 100644
index 0000000000..399e3d9ecd
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_15.html
@@ -0,0 +1,99 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test scroll position when focusing items in traversal but not rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ .tree {
+ height: 30px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ itemHeight: 10,
+ onFocus: item => renderTree({ focused: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ info("Test first focused.");
+ const tree = renderTree({ focused: "A" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render initial correctly");
+
+ info("Test last item focused when it was not yet rendered.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "End" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "Should render last focused item correctly");
+
+ info("Test first item focused when it was not yet rendered.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "Home" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render first focused item correctly");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_16.html b/devtools/client/shared/components/test/chrome/test_tree_16.html
new file mode 100644
index 0000000000..b70e63eade
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_16.html
@@ -0,0 +1,145 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test scroll position when showing items both in traversal and/or rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ .tree {
+ height: 30px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ itemHeight: 10,
+ onFocus: item => renderTree({ shown: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ info("Test first shown.");
+ const tree = renderTree({ shown: "A" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render initial correctly");
+
+ info("Test last as shown when it was not yet rendered.");
+ renderTree({ shown: "O" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Should render shown item correctly");
+
+ info("Test first item shown when it's not first rendered.");
+ renderTree({ shown: "A" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render shown item correctly");
+
+ info("Test mid item shown when it's not first rendered.");
+ renderTree({ shown: "G" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ ], "Should render shown item correctly");
+
+ info("Test mid item shown when it's already rendered.");
+ renderTree({ shown: "C" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ ], "Should render shown item correctly");
+
+ info("Test item that is not in traversal.");
+ renderTree({ shown: "Z" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ ], "Should render without changes");
+
+ info("Test item that is already shown.");
+ renderTree({ shown: "F" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ ], "Should render without changes");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/node/.eslintrc.js b/devtools/client/shared/components/test/node/.eslintrc.js
new file mode 100644
index 0000000000..ffb3e70473
--- /dev/null
+++ b/devtools/client/shared/components/test/node/.eslintrc.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ jest: true,
+ },
+};
diff --git a/devtools/client/shared/components/test/node/__mocks__/Services.js b/devtools/client/shared/components/test/node/__mocks__/Services.js
new file mode 100644
index 0000000000..14581e8fda
--- /dev/null
+++ b/devtools/client/shared/components/test/node/__mocks__/Services.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ appinfo: "",
+ prefs: {
+ getBoolPref(name, defaultVal) {
+ return defaultVal;
+ },
+ },
+};
diff --git a/devtools/client/shared/components/test/node/__mocks__/object-front.js b/devtools/client/shared/components/test/node/__mocks__/object-front.js
new file mode 100644
index 0000000000..def182111d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/__mocks__/object-front.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+function ObjectFront(grip, overrides) {
+ return {
+ grip,
+ enumEntries() {
+ return Promise.resolve(
+ this.getIterator({
+ ownProperties: {},
+ })
+ );
+ },
+ enumProperties(options) {
+ return Promise.resolve(
+ this.getIterator({
+ ownProperties: {},
+ })
+ );
+ },
+ enumSymbols() {
+ return Promise.resolve(
+ this.getIterator({
+ ownSymbols: [],
+ })
+ );
+ },
+ enumPrivateProperties() {
+ return Promise.resolve(
+ this.getIterator({
+ privateProperties: [],
+ })
+ );
+ },
+ getPrototype() {
+ return Promise.resolve({
+ prototype: {},
+ });
+ },
+ // Declared here so we can override it.
+ getIterator(res) {
+ return {
+ slice(start, count) {
+ return Promise.resolve(res);
+ },
+ };
+ },
+ ...overrides,
+ };
+}
+
+module.exports = ObjectFront;
diff --git a/devtools/client/shared/components/test/node/__mocks__/string-front.js b/devtools/client/shared/components/test/node/__mocks__/string-front.js
new file mode 100644
index 0000000000..d743f79e8b
--- /dev/null
+++ b/devtools/client/shared/components/test/node/__mocks__/string-front.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+function LongStringFront(grip, overrides) {
+ return {
+ grip,
+ substring: async () => "",
+ ...overrides,
+ };
+}
+
+module.exports = { LongStringFront };
diff --git a/devtools/client/shared/components/test/node/babel.config.js b/devtools/client/shared/components/test/node/babel.config.js
new file mode 100644
index 0000000000..2a95c9f71c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/babel.config.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ plugins: [
+ "@babel/plugin-proposal-class-properties",
+ "@babel/plugin-proposal-optional-chaining",
+ "@babel/plugin-proposal-nullish-coalescing-operator",
+ "transform-amd-to-commonjs",
+ ],
+};
diff --git a/devtools/client/shared/components/test/node/components/__snapshots__/tree.test.js.snap b/devtools/client/shared/components/test/node/components/__snapshots__/tree.test.js.snap
new file mode 100644
index 0000000000..aad7d06189
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/__snapshots__/tree.test.js.snap
@@ -0,0 +1,1171 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tree Don't auto expand root with very large number of children 1`] = `
+Array [
+ "key-A",
+ "key-B",
+ "key-E",
+ "key-F",
+ "key-G",
+ "key-C",
+ "key-H",
+ "key-I",
+ "key-D",
+ "key-J",
+ "key-M",
+ "key-N",
+]
+`;
+
+exports[`Tree active item - focus is inside the tree node and then blur 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L anchor]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - focus is inside the tree node when possible 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L anchor]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - focus is inside the tree node when possible 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L anchor]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - navigate inside the tree node 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L anchor]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - navigate inside the tree node 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L anchor]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - navigate inside the tree node 3`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L anchor]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when clicking away 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | [G]
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when clicking away 2`] = `
+"
+▶︎ [A]
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when moving away with keyboard 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when tree blurs 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | [G]
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when tree blurs 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | [G]
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when using keyboard and Enter 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree active item - renders as expected when using keyboard and Space 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree calls shouldItemUpdate when provided 1`] = `
+"
+▶︎ A
+▶︎ M
+"
+`;
+
+exports[`Tree calls shouldItemUpdate when provided 2`] = `
+"
+▶︎ A
+▶︎ M
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 3`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 4`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 5`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 6`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 7`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 8`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 9`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 10`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 11`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 12`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 13`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 14`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 15`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 16`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree ignores key strokes when pressing modifiers 17`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders arrows as expected when nodes are collapsed 1`] = `
+"
+▶︎ A
+▶︎ M
+"
+`;
+
+exports[`Tree renders arrows as expected when nodes are expanded 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected navigating down with keyboard on last node 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | [O]
+"
+`;
+
+exports[`Tree renders as expected navigating down with keyboard on last node 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | [O]
+"
+`;
+
+exports[`Tree renders as expected navigating up with the keyboard on a root 1`] = `
+"
+â–¼ [A]
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected navigating up with the keyboard on a root 2`] = `
+"
+â–¼ [A]
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected navigating with arrows on unexpandable roots 1`] = `
+"
+ [A]
+ M
+"
+`;
+
+exports[`Tree renders as expected navigating with arrows on unexpandable roots 2`] = `
+"
+ A
+ [M]
+"
+`;
+
+exports[`Tree renders as expected navigating with arrows on unexpandable roots 3`] = `
+"
+ [A]
+ M
+"
+`;
+
+exports[`Tree renders as expected when given a focused item 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | [G]
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when given a focused item 2`] = `
+"
+▶︎ [A]
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when given a focused item 3`] = `
+"
+â–¼ [A]
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when given a focused item 4`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating down with the keyboard 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | [K]
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating down with the keyboard 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating down with the keyboard 3`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | L
+| | [F]
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating up with the keyboard 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating up with the keyboard 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | [K]
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating up with the keyboard 3`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ [E]
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 1`] = `
+"
+▶︎ A
+▶︎ [M]
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 2`] = `
+"
+▶︎ [A]
+▶︎ M
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 3`] = `
+"
+▶︎ [A]
+▶︎ M
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 4`] = `
+"
+▶︎ A
+▶︎ [M]
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 5`] = `
+"
+▶︎ A
+▶︎ [M]
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 6`] = `
+"
+▶︎ A
+â–¼ [M]
+| ▶︎ N
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 7`] = `
+"
+▶︎ A
+â–¼ M
+| ▶︎ [N]
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 8`] = `
+"
+▶︎ A
+â–¼ M
+| ▶︎ [N]
+"
+`;
+
+exports[`Tree renders as expected when navigating with home/end 9`] = `
+"
+▶︎ [A]
+â–¼ M
+| ▶︎ N
+"
+`;
+
+exports[`Tree renders as expected when navigating with left arrows on roots 1`] = `
+"
+▶︎ A
+▶︎ [M]
+"
+`;
+
+exports[`Tree renders as expected when navigating with left arrows on roots 2`] = `
+"
+▶︎ [A]
+▶︎ M
+"
+`;
+
+exports[`Tree renders as expected when navigating with left arrows on roots 3`] = `
+"
+▶︎ [A]
+▶︎ M
+"
+`;
+
+exports[`Tree renders as expected when navigating with right/left arrows 1`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | K
+| | | [L]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating with right/left arrows 2`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ [E]
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating with right/left arrows 3`] = `
+"
+â–¼ A
+| â–¼ B
+| | ▶︎ [E]
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating with right/left arrows 4`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ [E]
+| | | K
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when navigating with right/left arrows 5`] = `
+"
+â–¼ A
+| â–¼ B
+| | â–¼ E
+| | | [K]
+| | | L
+| | F
+| | G
+| â–¼ C
+| | H
+| | I
+| â–¼ D
+| | J
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree renders as expected when passed autoDepth:1 1`] = `
+"
+â–¼ A
+| ▶︎ B
+| ▶︎ C
+| ▶︎ D
+â–¼ M
+| ▶︎ N
+"
+`;
+
+exports[`Tree renders as expected with collapsed nodes 1`] = `
+"
+▶︎ A
+â–¼ M
+| â–¼ N
+| | O
+"
+`;
+
+exports[`Tree uses isExpandable prop if it exists to render tree nodes 1`] = `
+"
+▶︎ A
+ M
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/basic.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/basic.test.js.snap
new file mode 100644
index 0000000000..9db3eadc93
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/basic.test.js.snap
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - renders renders as expected 1`] = `
+"
+▶︎ {…}
+"
+`;
+
+exports[`ObjectInspector - renders renders as expected 2`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", … }
+"
+`;
+
+exports[`ObjectInspector - renders renders as expected 3`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+"
+`;
+
+exports[`ObjectInspector - renders renders as expected 4`] = `
+"
+▶︎ {…}
+"
+`;
+
+exports[`ObjectInspector - renders renders as expected when not provided a name 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", … }
+"
+`;
+
+exports[`ObjectInspector - renders renders block nodes as expected 1`] = `
+"
+▼ ☲ Block
+| a: 30
+| b: 32
+"
+`;
+
+exports[`ObjectInspector - renders renders objects as expected when provided a name 1`] = `
+"
+▶︎ myproperty: Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", … }
+"
+`;
+
+exports[`ObjectInspector - renders renders primitives as expected when provided a name 1`] = `
+"
+ myproperty: 42
+"
+`;
+
+exports[`ObjectInspector - renders updates when the root changes 1`] = `
+"
+[ ▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+"
+`;
+
+exports[`ObjectInspector - renders updates when the root changes 2`] = `
+"
+[ ▶︎ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" } ]
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/classnames.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/classnames.test.js.snap
new file mode 100644
index 0000000000..81cc9f7028
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/classnames.test.js.snap
@@ -0,0 +1,351 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - classnames has the expected class 1`] = `
+<div
+ className="tree object-inspector"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={false}
+ focused={false}
+ id="root"
+ index={0}
+ isExpandable={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ }
+ }
+ key="root-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="root"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={null}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={Set {}}
+ focused={false}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <span
+ className="object-label"
+ >
+ root
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-number"
+ title={null}
+ >
+ 42
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+</div>
+`;
+
+exports[`ObjectInspector - classnames has the inline class when inline prop is true 1`] = `
+<div
+ className="tree object-inspector inline"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={false}
+ focused={false}
+ id="root"
+ index={0}
+ isExpandable={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ }
+ }
+ key="root-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="root"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={null}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={Set {}}
+ focused={false}
+ inline={true}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <span
+ className="object-label"
+ >
+ root
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-number"
+ title={null}
+ >
+ 42
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+</div>
+`;
+
+exports[`ObjectInspector - classnames has the nowrap class when disableWrap prop is true 1`] = `
+<div
+ className="tree object-inspector nowrap"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={false}
+ focused={false}
+ id="root"
+ index={0}
+ isExpandable={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ }
+ }
+ key="root-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="root"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={null}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ disableWrap={true}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={Set {}}
+ focused={false}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": 42,
+ },
+ "name": "root",
+ "path": "root",
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <span
+ className="object-label"
+ >
+ root
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-number"
+ title={null}
+ >
+ 42
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+</div>
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/entries.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/entries.test.js.snap
new file mode 100644
index 0000000000..5208db3ebb
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/entries.test.js.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - entries calls ObjectFront.enumEntries when expected 1`] = `
+"
+â–¼ Map(11)
+| ▶︎ <entries>
+"
+`;
+
+exports[`ObjectInspector - entries calls ObjectFront.enumEntries when expected 2`] = `
+"
+ â–¼ Map(11)
+[ | â–¼ <entries> ]
+ | | ▶︎ 0: \\"key-0\\" → \\"value-0\\"
+ | | ▶︎ 1: \\"key-1\\" → \\"value-1\\"
+ | | ▶︎ 2: \\"key-2\\" → \\"value-2\\"
+ | | ▶︎ 3: \\"key-3\\" → \\"value-3\\"
+ | | ▶︎ 4: \\"key-4\\" → \\"value-4\\"
+ | | ▶︎ 5: \\"key-5\\" → \\"value-5\\"
+ | | ▶︎ 6: \\"key-6\\" → \\"value-6\\"
+ | | ▶︎ 7: \\"key-7\\" → \\"value-7\\"
+ | | ▶︎ 8: \\"key-8\\" → \\"value-8\\"
+ | | ▶︎ 9: \\"key-9\\" → \\"value-9\\"
+ | | ▶︎ 10: \\"key-10\\" → \\"value-10\\"
+"
+`;
+
+exports[`ObjectInspector - entries calls ObjectFront.enumEntries when expected 3`] = `
+"
+ â–¼ Map(11)
+[ | ▶︎ <entries> ]
+"
+`;
+
+exports[`ObjectInspector - entries calls ObjectFront.enumEntries when expected 4`] = `
+"
+ â–¼ Map(11)
+[ | â–¼ <entries> ]
+ | | ▶︎ 0: \\"key-0\\" → \\"value-0\\"
+ | | ▶︎ 1: \\"key-1\\" → \\"value-1\\"
+ | | ▶︎ 2: \\"key-2\\" → \\"value-2\\"
+ | | ▶︎ 3: \\"key-3\\" → \\"value-3\\"
+ | | ▶︎ 4: \\"key-4\\" → \\"value-4\\"
+ | | ▶︎ 5: \\"key-5\\" → \\"value-5\\"
+ | | ▶︎ 6: \\"key-6\\" → \\"value-6\\"
+ | | ▶︎ 7: \\"key-7\\" → \\"value-7\\"
+ | | ▶︎ 8: \\"key-8\\" → \\"value-8\\"
+ | | ▶︎ 9: \\"key-9\\" → \\"value-9\\"
+ | | ▶︎ 10: \\"key-10\\" → \\"value-10\\"
+"
+`;
+
+exports[`ObjectInspector - entries renders Object with entries as expected 1`] = `
+"
+▼ Map { Symbol(\\"a\\") → \\"value-a\\", Symbol(\\"b\\") → \\"value-b\\" }
+| size: 2
+| â–¼ <entries>
+| | ▼ 0: \\"key-0\\" → \\"value-0\\"
+| | | <key>: \\"key-0\\"
+| | | <value>: \\"value-0\\"
+| | ▼ 1: \\"key-1\\" → \\"value-1\\"
+| | | <key>: \\"key-1\\"
+| | | <value>: \\"value-1\\"
+| | ▼ 2: \\"key-2\\" → \\"value-2\\"
+| | | <key>: \\"key-2\\"
+| | | <value>: \\"value-2\\"
+| | ▼ 3: \\"key-3\\" → \\"value-3\\"
+| | | <key>: \\"key-3\\"
+| | | <value>: \\"value-3\\"
+| | ▼ 4: \\"key-4\\" → \\"value-4\\"
+| | | <key>: \\"key-4\\"
+| | | <value>: \\"value-4\\"
+| | ▼ 5: \\"key-5\\" → \\"value-5\\"
+| | | <key>: \\"key-5\\"
+| | | <value>: \\"value-5\\"
+| | ▼ 6: \\"key-6\\" → \\"value-6\\"
+| | | <key>: \\"key-6\\"
+| | | <value>: \\"value-6\\"
+| | ▼ 7: \\"key-7\\" → \\"value-7\\"
+| | | <key>: \\"key-7\\"
+| | | <value>: \\"value-7\\"
+| | ▼ 8: \\"key-8\\" → \\"value-8\\"
+| | | <key>: \\"key-8\\"
+| | | <value>: \\"value-8\\"
+| | ▼ 9: \\"key-9\\" → \\"value-9\\"
+| | | <key>: \\"key-9\\"
+| | | <value>: \\"value-9\\"
+| | ▼ 10: \\"key-10\\" → \\"value-10\\"
+| | | <key>: \\"key-10\\"
+| | | <value>: \\"value-10\\"
+| ▼ <prototype>: Object { … }
+| | <prototype>: Object { }
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/expand.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/expand.test.js.snap
new file mode 100644
index 0000000000..3cb8b39dee
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/expand.test.js.snap
@@ -0,0 +1,175 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - state does not expand if the user selected some text 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state does not expand if the user selected some text 2`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state does not handle actors when client does not have releaseActor function 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state does not handle actors when client does not have releaseActor function 2`] = `
+"
+[ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+ | ▶︎ <prototype>: Object { }
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state does not handle actors when client does not have releaseActor function 3`] = `
+"
+ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+[ | â–¼ <prototype>: Object { } ]
+ | | ▶︎ <prototype>: Object { }
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state does not throw when expanding a block node 1`] = `
+"
+▶︎ ☲ Block
+▶︎ Proxy: Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state does not throw when expanding a block node 2`] = `
+"
+[ ▼ ☲ Block ]
+ | a: 30
+ | b: 32
+ ▶︎ Proxy: Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state expanding a getter returning a longString does not throw 1`] = `
+"
+▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+| â–¼ baseVal: \\"data:image/png;base64,initial<<<<\\"
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state expands if user selected some text and clicked the arrow 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state expands if user selected some text and clicked the arrow 2`] = `
+"
+[ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+ | a: 1
+ | Symbol(): \\"hello\\"
+ | ▶︎ <prototype>: Object { … }
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected expandedPaths state when clicking nodes 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected expandedPaths state when clicking nodes 2`] = `
+"
+[ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+ | a: 1
+ | Symbol(): \\"hello\\"
+ | ▶︎ <prototype>: Object { … }
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected expandedPaths state when clicking nodes 3`] = `
+"
+[ ▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected expandedPaths state when clicking nodes 4`] = `
+"
+ ▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+[ ▼ Proxy { <target>: {…}, <handler>: (3) […] } ]
+ | ▶︎ <target>: Object { … }
+ | ▶︎ <handler>: Array(3) [ … ]
+"
+`;
+
+exports[`ObjectInspector - state has the expected expandedPaths state when clicking nodes 5`] = `
+"
+[ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+ | a: 1
+ | Symbol(): \\"hello\\"
+ | ▶︎ <prototype>: Object { … }
+ ▼ Proxy { <target>: {…}, <handler>: (3) […] }
+ | ▶︎ <target>: Object { … }
+ | ▶︎ <handler>: Array(3) [ … ]
+"
+`;
+
+exports[`ObjectInspector - state has the expected state when expanding a node 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected state when expanding a node 2`] = `
+"
+[ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … } ]
+ | ▶︎ <prototype>: Object { }
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected state when expanding a node 3`] = `
+"
+ ▼ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+[ | â–¼ <prototype>: Object { } ]
+ | | ▶︎ <prototype>: Object { }
+ ▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected state when expanding a proxy node 1`] = `
+"
+▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+▶︎ Proxy { <target>: {…}, <handler>: (3) […] }
+"
+`;
+
+exports[`ObjectInspector - state has the expected state when expanding a proxy node 2`] = `
+"
+ ▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+[ â–¼ Proxy ]
+ | ▶︎ <target>: Object { … }
+ | ▶︎ <handler>: Array(3) [ … ]
+"
+`;
+
+exports[`ObjectInspector - state has the expected state when expanding a proxy node 3`] = `
+"
+ ▶︎ Object { p0: \\"0\\", p1: \\"1\\", p2: \\"2\\", p3: \\"3\\", p4: \\"4\\", p5: \\"5\\", p6: \\"6\\", p7: \\"7\\", p8: \\"8\\", p9: \\"9\\", … }
+ â–¼ Proxy
+ | ▶︎ <target>: Object { … }
+[ | ▼ <handler>: (3) […] ]
+ | | <prototype>: Object { }
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/getter-setter.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/getter-setter.test.js.snap
new file mode 100644
index 0000000000..86ededb690
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/getter-setter.test.js.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - getters & setters onInvokeGetterButtonClick + getter & setter 1`] = `
+"
+â–¼ root
+| x: (>>)
+| ▶︎ <get x()>: function x()
+| ▶︎ <set x()>: function x()
+"
+`;
+
+exports[`ObjectInspector - getters & setters onInvokeGetterButtonClick + getter 1`] = `
+"
+â–¼ root
+| x: (>>)
+| ▶︎ <get x()>: function x()
+"
+`;
+
+exports[`ObjectInspector - getters & setters onInvokeGetterButtonClick + setter 1`] = `
+"
+â–¼ root
+| x: Setter
+| ▶︎ <set x()>: function x()
+"
+`;
+
+exports[`ObjectInspector - getters & setters renders getters and setters as expected 1`] = `
+"
+â–¼ root
+| x: Getter & Setter
+| ▶︎ <get x()>: function x()
+| ▶︎ <set x()>: function x()
+"
+`;
+
+exports[`ObjectInspector - getters & setters renders getters as expected 1`] = `
+"
+â–¼ root
+| x: Getter
+| ▶︎ <get x()>: function x()
+"
+`;
+
+exports[`ObjectInspector - getters & setters renders setters as expected 1`] = `
+"
+â–¼ root
+| x: Setter
+| ▶︎ <set x()>: function x()
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/keyboard-navigation.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/keyboard-navigation.test.js.snap
new file mode 100644
index 0000000000..8998ce3aff
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/keyboard-navigation.test.js.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - keyboard navigation works as expected 1`] = `
+"
+▶︎ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" }
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 2`] = `
+"
+[ ▶︎ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" } ]
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 3`] = `
+"
+[ â–¼ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" } ]
+ | <prototype>: Object { }
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 4`] = `
+"
+ â–¼ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" }
+[ | <prototype>: Object { } ]
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 5`] = `
+"
+[ â–¼ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" } ]
+ | <prototype>: Object { }
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 6`] = `
+"
+ â–¼ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" }
+[ | <prototype>: Object { } ]
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 7`] = `
+"
+[ â–¼ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" } ]
+ | <prototype>: Object { }
+"
+`;
+
+exports[`ObjectInspector - keyboard navigation works as expected 8`] = `
+"
+â–¼ Object { a: \\"a\\", b: \\"b\\", c: \\"c\\" }
+| <prototype>: Object { }
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/properties.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/properties.test.js.snap
new file mode 100644
index 0000000000..3bb9e517a8
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/properties.test.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - properties renders uninitialized bindings 1`] = `
+"
+ someFoo: (uninitialized)
+"
+`;
+
+exports[`ObjectInspector - properties renders unmapped bindings 1`] = `
+"
+ someFoo: (unmapped)
+"
+`;
+
+exports[`ObjectInspector - properties renders unscoped bindings 1`] = `
+"
+ someFoo: (unscoped)
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/proxy.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/proxy.test.js.snap
new file mode 100644
index 0000000000..d48a6e058d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/proxy.test.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - Proxy renders Proxy as expected 1`] = `
+"
+▼ Proxy { <target>: {…}, <handler>: (3) […] }
+| ▶︎ <target>: Object { … }
+| ▶︎ <handler>: Array(3) [ … ]
+"
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/window.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/window.test.js.snap
new file mode 100644
index 0000000000..2995d8de9b
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/__snapshots__/window.test.js.snap
@@ -0,0 +1,2114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ObjectInspector - dimTopLevelWindow renders collapsed top-level window when dimTopLevelWindow =false 1`] = `
+<Provider
+ store={
+ Object {
+ "dispatch": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ Symbol(observable): [Function],
+ }
+ }
+>
+ <Component
+ autoExpandDepth={0}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <Connect(ObjectInspector)
+ autoExpandDepth={0}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <ObjectInspector
+ addWatchpoint={[Function]}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ evaluations={Map {}}
+ expandedPaths={Set {}}
+ invokeGetter={[Function]}
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ removeWatchpoint={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ >
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={0}
+ className="object-inspector"
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getRoots={[Function]}
+ isExpandable={[Function]}
+ isExpanded={[Function]}
+ onActivate={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ className="tree object-inspector"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+ >
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={false}
+ focused={false}
+ id="window"
+ index={0}
+ isExpandable={true}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ key="window-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-expanded={false}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={true}
+ id="window"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={
+ <ArrowExpander
+ expanded={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ />
+ }
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={Set {}}
+ focused={false}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <ArrowExpander
+ expanded={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ >
+ <button
+ className="arrow"
+ />
+ </ArrowExpander>
+ <span
+ className="object-label"
+ >
+ window
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-Window"
+ data-link-actor-id="server0.conn0.windowGlobal2147483651/obj35"
+ title={null}
+ >
+ <span
+ className="objectTitle"
+ >
+ Window
+ </span>
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+ </div>
+ </Tree>
+ </ObjectInspector>
+ </Connect(ObjectInspector)>
+ </Component>
+</Provider>
+`;
+
+exports[`ObjectInspector - dimTopLevelWindow renders sub-level window 1`] = `
+<Provider
+ store={
+ Object {
+ "dispatch": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ Symbol(observable): [Function],
+ }
+ }
+>
+ <Component
+ autoExpandDepth={0}
+ dimTopLevelWindow={true}
+ injectWaitService={true}
+ roots={
+ Array [
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <Connect(ObjectInspector)
+ autoExpandDepth={0}
+ dimTopLevelWindow={true}
+ injectWaitService={true}
+ roots={
+ Array [
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <ObjectInspector
+ addWatchpoint={[Function]}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expandedPaths={
+ Set {
+ "root",
+ }
+ }
+ injectWaitService={true}
+ invokeGetter={[Function]}
+ loadedProperties={
+ Map {
+ "root" => Object {},
+ }
+ }
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ removeWatchpoint={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ >
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={0}
+ className="object-inspector"
+ focused={
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getRoots={[Function]}
+ isExpandable={[Function]}
+ isExpanded={[Function]}
+ onActivate={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-activedescendant="root"
+ className="tree object-inspector"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+ >
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={true}
+ id="root"
+ index={0}
+ isExpandable={true}
+ item={
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ key="root-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node focused"
+ data-expandable={true}
+ id="root"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={
+ <ArrowExpander
+ expanded={true}
+ item={
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ />
+ }
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expanded={true}
+ expandedPaths={
+ Set {
+ "root",
+ }
+ }
+ focused={true}
+ injectWaitService={true}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node focused"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <ArrowExpander
+ expanded={true}
+ item={
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ >
+ <button
+ className="arrow expanded"
+ />
+ </ArrowExpander>
+ <span
+ className="object-label"
+ >
+ root
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+ <TreeNode
+ active={false}
+ depth={1}
+ expanded={false}
+ focused={false}
+ id="window"
+ index={1}
+ isExpandable={true}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ key="window-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-expanded={false}
+ aria-level={2}
+ className="tree-node"
+ data-expandable={true}
+ id="window"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <span
+ className="tree-indent tree-last-indent"
+ >
+ ​
+ </span>
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={
+ <ArrowExpander
+ expanded={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ />
+ }
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={1}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={
+ Set {
+ "root",
+ }
+ }
+ focused={false}
+ injectWaitService={true}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ],
+ "meta": undefined,
+ "name": "root",
+ "parent": undefined,
+ "path": "root",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <ArrowExpander
+ expanded={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ >
+ <button
+ className="arrow"
+ />
+ </ArrowExpander>
+ <span
+ className="object-label"
+ >
+ window
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-Window"
+ data-link-actor-id="server0.conn0.windowGlobal2147483651/obj35"
+ title={null}
+ >
+ <span
+ className="objectTitle"
+ >
+ Window
+ </span>
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+ </div>
+ </Tree>
+ </ObjectInspector>
+ </Connect(ObjectInspector)>
+ </Component>
+</Provider>
+`;
+
+exports[`ObjectInspector - dimTopLevelWindow renders window as expected when dimTopLevelWindow is true 1`] = `
+<Provider
+ store={
+ Object {
+ "dispatch": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ Symbol(observable): [Function],
+ }
+ }
+>
+ <Component
+ autoExpandDepth={0}
+ dimTopLevelWindow={true}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <Connect(ObjectInspector)
+ autoExpandDepth={0}
+ dimTopLevelWindow={true}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <ObjectInspector
+ addWatchpoint={[Function]}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expandedPaths={Set {}}
+ invokeGetter={[Function]}
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ removeWatchpoint={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ >
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={0}
+ className="object-inspector"
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getRoots={[Function]}
+ isExpandable={[Function]}
+ isExpanded={[Function]}
+ onActivate={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ className="tree object-inspector"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+ >
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={false}
+ focused={false}
+ id="window"
+ index={0}
+ isExpandable={true}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ key="window-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-expanded={false}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={true}
+ id="window"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={
+ <ArrowExpander
+ expanded={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ />
+ }
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={Set {}}
+ focused={false}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node lessen"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <ArrowExpander
+ expanded={false}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ >
+ <button
+ className="arrow"
+ />
+ </ArrowExpander>
+ <span
+ className="object-label"
+ >
+ window
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-Window"
+ data-link-actor-id="server0.conn0.windowGlobal2147483651/obj35"
+ title={null}
+ >
+ <span
+ className="objectTitle"
+ >
+ Window
+ </span>
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+ </div>
+ </Tree>
+ </ObjectInspector>
+ </Connect(ObjectInspector)>
+ </Component>
+</Provider>
+`;
+
+exports[`ObjectInspector - dimTopLevelWindow renders window as expected when dimTopLevelWindow is true 2`] = `
+<Provider
+ store={
+ Object {
+ "dispatch": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ Symbol(observable): [Function],
+ }
+ }
+>
+ <Component
+ autoExpandDepth={0}
+ dimTopLevelWindow={true}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <Connect(ObjectInspector)
+ autoExpandDepth={0}
+ dimTopLevelWindow={true}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ >
+ <ObjectInspector
+ addWatchpoint={[Function]}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expandedPaths={
+ Set {
+ "window",
+ }
+ }
+ invokeGetter={[Function]}
+ loadedProperties={
+ Map {
+ "window" => Object {
+ "ownProperties": Object {},
+ "prototype": Object {},
+ },
+ }
+ }
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ removeWatchpoint={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ >
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={0}
+ className="object-inspector"
+ focused={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getRoots={[Function]}
+ isExpandable={[Function]}
+ isExpanded={[Function]}
+ onActivate={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-activedescendant="window"
+ className="tree object-inspector"
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+ >
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={true}
+ id="window"
+ index={0}
+ isExpandable={true}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ key="window-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node focused"
+ data-expandable={true}
+ id="window"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={
+ <ArrowExpander
+ expanded={true}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ />
+ }
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={0}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expanded={true}
+ expandedPaths={
+ Set {
+ "window",
+ }
+ }
+ focused={true}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ loadedProperties={Map {}}
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node focused"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <ArrowExpander
+ expanded={true}
+ item={
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ }
+ }
+ >
+ <button
+ className="arrow expanded"
+ />
+ </ArrowExpander>
+ <span
+ className="object-label"
+ >
+ window
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-Window"
+ data-link-actor-id="server0.conn0.windowGlobal2147483651/obj35"
+ title={null}
+ >
+ <span
+ className="objectTitle"
+ >
+ Window
+ </span>
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+ <TreeNode
+ active={false}
+ depth={1}
+ expanded={false}
+ focused={false}
+ id="windowâ—¦<prototype>"
+ index={1}
+ isExpandable={false}
+ item={
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {},
+ },
+ "meta": undefined,
+ "name": "<prototype>",
+ "parent": Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ "path": "windowâ—¦<prototype>",
+ "propertyName": undefined,
+ "type": Symbol(<prototype>),
+ }
+ }
+ key="windowâ—¦<prototype>-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ shouldItemUpdate={[Function]}
+ >
+ <div
+ aria-level={2}
+ className="tree-node"
+ data-expandable={false}
+ id="windowâ—¦<prototype>"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <span
+ className="tree-indent tree-last-indent"
+ >
+ ​
+ </span>
+ <ObjectInspectorItem
+ addWatchpoint={[Function]}
+ arrow={null}
+ autoExpandDepth={0}
+ autoReleaseObjectActors={true}
+ closeObjectInspector={[Function]}
+ depth={1}
+ dimTopLevelWindow={true}
+ evaluations={Map {}}
+ expanded={false}
+ expandedPaths={
+ Set {
+ "window",
+ }
+ }
+ focused={false}
+ invokeGetter={[Function]}
+ item={
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {},
+ },
+ "meta": undefined,
+ "name": "<prototype>",
+ "parent": Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ "path": "windowâ—¦<prototype>",
+ "propertyName": undefined,
+ "type": Symbol(<prototype>),
+ }
+ }
+ loadedProperties={
+ Map {
+ "window" => Object {
+ "ownProperties": Object {},
+ "prototype": Object {},
+ },
+ }
+ }
+ nodeCollapse={[Function]}
+ nodeExpand={[Function]}
+ nodeLoadProperties={[Function]}
+ nodePropertiesLoaded={[Function]}
+ onContextMenu={[Function]}
+ removeWatchpoint={[Function]}
+ renderItemActions={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "isError": false,
+ "ownPropertyLength": 806,
+ "preview": Object {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation",
+ },
+ "sealed": false,
+ "type": "object",
+ },
+ },
+ "meta": undefined,
+ "name": "window",
+ "parent": undefined,
+ "path": "window",
+ "propertyName": undefined,
+ "type": Symbol(GRIP),
+ },
+ ]
+ }
+ rootsChanged={[Function]}
+ setExpanded={[Function]}
+ >
+ <div
+ className="node object-node lessen"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ >
+ <span
+ className="object-label"
+ >
+ &lt;prototype&gt;
+ </span>
+ <span
+ className="object-delimiter"
+ >
+ :
+ </span>
+ <span
+ className="objectBox objectBox-object"
+ title={null}
+ >
+ <span
+ className="objectLeftBrace"
+ >
+ {
+ </span>
+ <span
+ className="objectRightBrace"
+ >
+ }
+ </span>
+ </span>
+ </div>
+ </ObjectInspectorItem>
+ </div>
+ </TreeNode>
+ </div>
+ </Tree>
+ </ObjectInspector>
+ </Connect(ObjectInspector)>
+ </Component>
+</Provider>
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/basic.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/basic.test.js
new file mode 100644
index 0000000000..47a919db02
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/basic.test.js
@@ -0,0 +1,439 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ mountObjectInspector,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const { mount } = require("enzyme");
+const {
+ createNode,
+ NODE_TYPES,
+} = require("devtools/client/shared/components/object-inspector/utils/node");
+
+const { Rep } = require(`devtools/client/shared/components/reps/reps/rep`);
+const {
+ MODE,
+} = require(`devtools/client/shared/components/reps/reps/constants`);
+const {
+ formatObjectInspector,
+ waitForDispatch,
+ waitForLoadedProperties,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const ObjectFront = require("devtools/client/shared/components/test/node/__mocks__/object-front");
+const gripRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip`);
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ ...overrides,
+ };
+}
+
+function mountOI(props, { initialState } = {}) {
+ const client = {
+ createObjectFront: grip => ObjectFront(grip),
+ };
+
+ const obj = mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ initialState: {
+ objectInspector: {
+ ...initialState,
+ evaluations: new Map(),
+ },
+ },
+ });
+
+ return obj;
+}
+
+function renderOI(props, opts) {
+ return mountOI(props, opts).wrapper;
+}
+
+describe("ObjectInspector - renders", () => {
+ it("renders as expected", () => {
+ const stub = gripRepStubs.get("testMoreThanMaxProps");
+
+ const renderObjectInspector = mode =>
+ renderOI({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ mode,
+ });
+
+ const renderRep = mode => Rep({ object: stub, mode });
+
+ const tinyOi = renderObjectInspector(MODE.TINY);
+ expect(tinyOi.find(".arrow").exists()).toBeTruthy();
+ expect(tinyOi.contains(renderRep(MODE.TINY))).toBeTruthy();
+ expect(formatObjectInspector(tinyOi)).toMatchSnapshot();
+
+ const shortOi = renderObjectInspector(MODE.SHORT);
+ expect(shortOi.find(".arrow").exists()).toBeTruthy();
+ expect(shortOi.contains(renderRep(MODE.SHORT))).toBeTruthy();
+ expect(formatObjectInspector(shortOi)).toMatchSnapshot();
+
+ const longOi = renderObjectInspector(MODE.LONG);
+ expect(longOi.find(".arrow").exists()).toBeTruthy();
+ expect(longOi.contains(renderRep(MODE.LONG))).toBeTruthy();
+ expect(formatObjectInspector(longOi)).toMatchSnapshot();
+
+ const oi = renderObjectInspector();
+ expect(oi.find(".arrow").exists()).toBeTruthy();
+ // When no mode is provided, it defaults to TINY mode to render the Rep.
+ expect(oi.contains(renderRep(MODE.TINY))).toBeTruthy();
+ expect(formatObjectInspector(oi)).toMatchSnapshot();
+ });
+
+ it("directly renders a Rep when the stub is not expandable", () => {
+ const object = 42;
+
+ const renderObjectInspector = mode =>
+ renderOI({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: object,
+ },
+ },
+ ],
+ mode,
+ });
+
+ const renderRep = mode => mount(Rep({ object, mode }));
+
+ const tinyOi = renderObjectInspector(MODE.TINY);
+ expect(tinyOi.find(".arrow").exists()).toBeFalsy();
+ expect(tinyOi.html()).toEqual(renderRep(MODE.TINY).html());
+
+ const shortOi = renderObjectInspector(MODE.SHORT);
+ expect(shortOi.find(".arrow").exists()).toBeFalsy();
+ expect(shortOi.html()).toEqual(renderRep(MODE.SHORT).html());
+
+ const longOi = renderObjectInspector(MODE.LONG);
+ expect(longOi.find(".arrow").exists()).toBeFalsy();
+ expect(longOi.html()).toEqual(renderRep(MODE.LONG).html());
+
+ const oi = renderObjectInspector();
+ expect(oi.find(".arrow").exists()).toBeFalsy();
+ // When no mode is provided, it defaults to TINY mode to render the Rep.
+ expect(oi.html()).toEqual(renderRep(MODE.TINY).html());
+ });
+
+ it("renders objects as expected when provided a name", () => {
+ const object = gripRepStubs.get("testMoreThanMaxProps");
+ const name = "myproperty";
+
+ const oi = renderOI({
+ roots: [
+ {
+ path: "root",
+ name,
+ contents: {
+ value: object,
+ },
+ },
+ ],
+ mode: MODE.SHORT,
+ });
+
+ expect(oi.find(".object-label").text()).toEqual(name);
+ expect(formatObjectInspector(oi)).toMatchSnapshot();
+ });
+
+ it("renders primitives as expected when provided a name", () => {
+ const value = 42;
+ const name = "myproperty";
+
+ const oi = renderOI({
+ roots: [
+ {
+ path: "root",
+ name,
+ contents: { value },
+ },
+ ],
+ mode: MODE.SHORT,
+ });
+
+ expect(oi.find(".object-label").text()).toEqual(name);
+ expect(formatObjectInspector(oi)).toMatchSnapshot();
+ });
+
+ it("renders as expected when not provided a name", () => {
+ const object = gripRepStubs.get("testMoreThanMaxProps");
+
+ const oi = renderOI({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: object,
+ },
+ },
+ ],
+ mode: MODE.SHORT,
+ });
+
+ expect(oi.find(".object-label").exists()).toBeFalsy();
+ expect(formatObjectInspector(oi)).toMatchSnapshot();
+ });
+
+ it("renders leaves with a shorter mode than the root", async () => {
+ const stub = gripRepStubs.get("testMaxProps");
+
+ const renderObjectInspector = mode =>
+ renderOI(
+ {
+ autoExpandDepth: 1,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ mode,
+ },
+ {
+ initialState: {
+ loadedProperties: new Map([
+ [
+ "root",
+ {
+ ownProperties: Object.keys(stub.preview.ownProperties).reduce(
+ (res, key) => ({
+ [key]: {
+ value: stub,
+ },
+ ...res,
+ }),
+ {}
+ ),
+ },
+ ],
+ ]),
+ },
+ }
+ );
+
+ const renderRep = mode => Rep({ object: stub, mode });
+
+ const tinyOi = renderObjectInspector(MODE.TINY);
+
+ expect(
+ tinyOi
+ .find(".node")
+ .at(1)
+ .contains(renderRep(MODE.TINY))
+ ).toBeTruthy();
+
+ const shortOi = renderObjectInspector(MODE.SHORT);
+ expect(
+ shortOi
+ .find(".node")
+ .at(1)
+ .contains(renderRep(MODE.TINY))
+ ).toBeTruthy();
+
+ const longOi = renderObjectInspector(MODE.LONG);
+ expect(
+ longOi
+ .find(".node")
+ .at(1)
+ .contains(renderRep(MODE.SHORT))
+ ).toBeTruthy();
+
+ const oi = renderObjectInspector();
+ // When no mode is provided, it defaults to TINY mode to render the Rep.
+ expect(
+ oi
+ .find(".node")
+ .at(1)
+ .contains(renderRep(MODE.TINY))
+ ).toBeTruthy();
+ });
+
+ it("renders less-important nodes as expected", async () => {
+ const defaultPropertiesNode = createNode({
+ name: "<default>",
+ contents: [],
+ type: NODE_TYPES.DEFAULT_PROPERTIES,
+ });
+
+ // The <default properties> node should have the "lessen" class only when
+ // collapsed.
+ let { store, wrapper } = mountOI({
+ roots: [defaultPropertiesNode],
+ });
+
+ let defaultPropertiesElementNode = wrapper.find(".node");
+ expect(defaultPropertiesElementNode.hasClass("lessen")).toBe(true);
+
+ let onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ defaultPropertiesElementNode.simulate("click");
+ await onPropertiesLoaded;
+ wrapper.update();
+ defaultPropertiesElementNode = wrapper.find(".node").first();
+ expect(
+ wrapper
+ .find(".node")
+ .first()
+ .hasClass("lessen")
+ ).toBe(false);
+
+ const prototypeNode = createNode({
+ name: "<prototype>",
+ contents: [],
+ type: NODE_TYPES.PROTOTYPE,
+ });
+
+ // The <prototype> node should have the "lessen" class only when collapsed.
+ ({ wrapper, store } = mountOI({
+ roots: [prototypeNode],
+ injectWaitService: true,
+ }));
+
+ let protoElementNode = wrapper.find(".node");
+ expect(protoElementNode.hasClass("lessen")).toBe(true);
+
+ onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ protoElementNode.simulate("click");
+ await onPropertiesLoaded;
+ wrapper.update();
+
+ protoElementNode = wrapper.find(".node").first();
+ expect(protoElementNode.hasClass("lessen")).toBe(false);
+ });
+
+ it("renders block nodes as expected", async () => {
+ const blockNode = createNode({
+ name: "Block",
+ contents: [
+ {
+ name: "a",
+ contents: {
+ value: 30,
+ },
+ },
+ {
+ name: "b",
+ contents: {
+ value: 32,
+ },
+ },
+ ],
+ type: NODE_TYPES.BLOCK,
+ });
+
+ const { wrapper, store } = mountOI({
+ roots: [blockNode],
+ autoExpandDepth: 1,
+ });
+
+ await waitForLoadedProperties(store, ["Block"]);
+ wrapper.update();
+
+ const blockElementNode = wrapper.find(".node").first();
+ expect(blockElementNode.hasClass("block")).toBe(true);
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it.skip("updates when the root changes", async () => {
+ let root = {
+ path: "root",
+ contents: {
+ value: gripRepStubs.get("testMoreThanMaxProps"),
+ },
+ };
+ const { wrapper } = mountOI({
+ roots: [root],
+ mode: MODE.LONG,
+ focusedItem: root,
+ });
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ root = {
+ path: "root-2",
+ contents: {
+ value: gripRepStubs.get("testMaxProps"),
+ },
+ };
+
+ wrapper.setProps({
+ roots: [root],
+ focusedItem: root,
+ });
+ wrapper.update();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it.skip("updates when the root changes but has same path", async () => {
+ const { wrapper, store } = mountOI({
+ injectWaitService: true,
+ roots: [
+ {
+ path: "root",
+ name: "root",
+ contents: [
+ {
+ name: "a",
+ contents: {
+ value: 30,
+ },
+ },
+ {
+ name: "b",
+ contents: {
+ value: 32,
+ },
+ },
+ ],
+ },
+ ],
+ mode: MODE.LONG,
+ });
+
+ wrapper
+ .find(".node")
+ .at(0)
+ .simulate("click");
+
+ const oldTree = formatObjectInspector(wrapper);
+
+ const onRootsChanged = waitForDispatch(store, "ROOTS_CHANGED");
+
+ wrapper.setProps({
+ roots: [
+ {
+ path: "root",
+ name: "root",
+ contents: [
+ {
+ name: "c",
+ contents: {
+ value: "i'm the new node",
+ },
+ },
+ ],
+ },
+ ],
+ });
+
+ await onRootsChanged;
+ wrapper.update();
+ expect(formatObjectInspector(wrapper)).not.toBe(oldTree);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/classnames.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/classnames.test.js
new file mode 100644
index 0000000000..9f93d8fb70
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/classnames.test.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+const {
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ roots: [
+ {
+ path: "root",
+ name: "root",
+ contents: { value: 42 },
+ },
+ ],
+ ...overrides,
+ };
+}
+
+function mount(props) {
+ const client = { createObjectFront: grip => ObjectFront(grip) };
+
+ return mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ });
+}
+
+describe("ObjectInspector - classnames", () => {
+ it("has the expected class", () => {
+ const { tree } = mount();
+ expect(tree.hasClass("tree")).toBeTruthy();
+ expect(tree.hasClass("inline")).toBeFalsy();
+ expect(tree.hasClass("nowrap")).toBeFalsy();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it("has the nowrap class when disableWrap prop is true", () => {
+ const { tree } = mount({ disableWrap: true });
+ expect(tree.hasClass("nowrap")).toBeTruthy();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it("has the inline class when inline prop is true", () => {
+ const { tree } = mount({ inline: true });
+ expect(tree.hasClass("inline")).toBeTruthy();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/create-long-string-front.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/create-long-string-front.test.js
new file mode 100644
index 0000000000..40e2fd772a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/create-long-string-front.test.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+
+const {
+ mountObjectInspector,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const ObjectFront = require("devtools/client/shared/components/test/node/__mocks__/object-front");
+const {
+ LongStringFront,
+} = require("devtools/client/shared/components/test/node/__mocks__/string-front");
+
+const longStringStubs = require(`devtools/client/shared/components/test/node/stubs/reps/long-string`);
+
+function mount(props) {
+ const substring = jest.fn(() => Promise.resolve(""));
+
+ const client = {
+ createObjectFront: grip => ObjectFront(grip),
+ createLongStringFront: jest.fn(grip =>
+ LongStringFront(grip, { substring })
+ ),
+ };
+
+ const obj = mountObjectInspector({
+ client,
+ props,
+ });
+
+ return { ...obj, substring };
+}
+
+describe("createLongStringFront", () => {
+ it("is called with the expected object for longString node", () => {
+ const stub = longStringStubs.get("testMultiline");
+
+ const { client } = mount({
+ autoExpandDepth: 1,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ });
+
+ expect(client.createLongStringFront.mock.calls[0][0]).toBe(stub);
+ });
+
+ describe("substring", () => {
+ it("is called for longStrings with unloaded full text", () => {
+ const stub = longStringStubs.get("testUnloadedFullText");
+
+ const { substring } = mount({
+ autoExpandDepth: 1,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ });
+
+ expect(substring.mock.calls[0]).toHaveLength(2);
+ const [start, length] = substring.mock.calls[0];
+ expect(start).toBe(stub.initial.length);
+ expect(length).toBe(stub.length);
+ });
+
+ it("is not called for longString node w/ loaded full text", () => {
+ const stub = longStringStubs.get("testLoadedFullText");
+
+ const { substring } = mount({
+ autoExpandDepth: 1,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ });
+
+ expect(substring.mock.calls).toHaveLength(0);
+ });
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/create-object-client.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/create-object-client.test.js
new file mode 100644
index 0000000000..f969debfb7
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/create-object-client.test.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+
+const {
+ mountObjectInspector,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const ObjectFront = require("devtools/client/shared/components/test/node/__mocks__/object-front");
+
+const {
+ createNode,
+ makeNodesForEntries,
+ makeNumericalBuckets,
+} = require("devtools/client/shared/components/object-inspector/utils/node");
+
+const gripRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip`);
+const gripArrayRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip-array`);
+
+function mount(props, overrides = {}) {
+ const client = {
+ createObjectFront:
+ overrides.createObjectFront || jest.fn(grip => ObjectFront(grip)),
+ getFrontByID: _id => null,
+ };
+
+ return mountObjectInspector({
+ client,
+ props,
+ });
+}
+
+describe("createObjectFront", () => {
+ it("is called with the expected object for regular node", () => {
+ const stub = gripRepStubs.get("testMoreThanMaxProps");
+ const { client } = mount({
+ autoExpandDepth: 1,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ });
+
+ expect(client.createObjectFront.mock.calls[0][0]).toBe(stub);
+ });
+
+ it("is called with the expected object for entries node", () => {
+ const grip = Symbol();
+ const mapStubNode = createNode({
+ name: "map",
+ contents: { value: grip },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+
+ const { client } = mount({
+ autoExpandDepth: 1,
+ roots: [entriesNode],
+ });
+
+ expect(client.createObjectFront.mock.calls[0][0]).toBe(grip);
+ });
+
+ it("is called with the expected object for bucket node", () => {
+ const grip = gripArrayRepStubs.get("testMaxProps");
+ const root = createNode({ name: "root", contents: { value: grip } });
+ const [bucket] = makeNumericalBuckets(root);
+
+ const { client } = mount({
+ autoExpandDepth: 1,
+ roots: [bucket],
+ });
+ expect(client.createObjectFront.mock.calls[0][0]).toBe(grip);
+ });
+
+ it("is called with the expected object for sub-bucket node", () => {
+ const grip = gripArrayRepStubs.get("testMaxProps");
+ const root = createNode({ name: "root", contents: { value: grip } });
+ const [bucket] = makeNumericalBuckets(root);
+ const [subBucket] = makeNumericalBuckets(bucket);
+
+ const { client } = mount({
+ autoExpandDepth: 1,
+ roots: [subBucket],
+ });
+
+ expect(client.createObjectFront.mock.calls[0][0]).toBe(grip);
+ });
+
+ it("doesn't fail when ObjectFront doesn't have expected methods", () => {
+ const stub = gripRepStubs.get("testMoreThanMaxProps");
+ const root = createNode({ name: "root", contents: { value: stub } });
+
+ // Override console.error so we don't spam test results.
+ const originalConsoleError = console.error;
+ console.error = () => {};
+
+ const createObjectFront = x => ({});
+ mount(
+ {
+ autoExpandDepth: 1,
+ roots: [root],
+ },
+ { createObjectFront }
+ );
+
+ // rollback console.error.
+ console.error = originalConsoleError;
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/entries.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/entries.test.js
new file mode 100644
index 0000000000..0cb896d3ae
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/entries.test.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+
+const {
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ formatObjectInspector,
+ waitForDispatch,
+ waitForLoadedProperties,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+const gripMapRepStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const mapStubs = require("resource://devtools/client/shared/components/test/node/stubs/object-inspector/map.js");
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ createObjectFront: grip => ObjectFront(grip),
+ ...overrides,
+ };
+}
+
+function getEnumEntriesMock() {
+ return jest.fn(() => ({
+ slice: () => mapStubs.get("11-entries"),
+ }));
+}
+
+function mount(props, { initialState }) {
+ const enumEntries = getEnumEntriesMock();
+
+ const client = {
+ createObjectFront: grip => ObjectFront(grip, { enumEntries }),
+ getFrontByID: _id => null,
+ };
+ const obj = mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ initialState: {
+ objectInspector: {
+ ...initialState,
+ evaluations: new Map(),
+ },
+ },
+ });
+
+ return { ...obj, enumEntries };
+}
+
+describe("ObjectInspector - entries", () => {
+ it("renders Object with entries as expected", async () => {
+ const stub = gripMapRepStubs.get("testSymbolKeyedMap");
+
+ const { store, wrapper, enumEntries } = mount(
+ {
+ autoExpandDepth: 3,
+ roots: [
+ {
+ path: "root",
+ contents: { value: stub },
+ },
+ ],
+ mode: MODE.LONG,
+ },
+ {
+ initialState: {
+ loadedProperties: new Map([["root", mapStubs.get("properties")]]),
+ },
+ }
+ );
+
+ await waitForLoadedProperties(store, [
+ "rootâ—¦<entries>â—¦0",
+ "rootâ—¦<entries>â—¦1",
+ ]);
+
+ wrapper.update();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("calls ObjectFront.enumEntries when expected", async () => {
+ const stub = gripMapRepStubs.get("testMoreThanMaxEntries");
+
+ const { wrapper, store, enumEntries } = mount(
+ {
+ autoExpandDepth: 1,
+ injectWaitService: true,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ },
+ {
+ initialState: {
+ loadedProperties: new Map([
+ ["root", { ownProperties: stub.preview.entries }],
+ ]),
+ },
+ }
+ );
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ const nodes = wrapper.find(".node");
+ const entriesNode = nodes.at(1);
+ expect(entriesNode.text()).toBe("<entries>");
+
+ const onEntrieLoad = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ entriesNode.simulate("click");
+ await onEntrieLoad;
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ expect(enumEntries.mock.calls).toHaveLength(1);
+
+ entriesNode.simulate("click");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ entriesNode.simulate("click");
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ // it does not call enumEntries if entries were already loaded.
+ expect(enumEntries.mock.calls).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/events.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/events.test.js
new file mode 100644
index 0000000000..e6483dbefb
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/events.test.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+const {
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+const gripRepStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ ...overrides,
+ };
+}
+
+function mount(props) {
+ const client = { createObjectFront: grip => ObjectFront(grip) };
+
+ return mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ });
+}
+
+describe("ObjectInspector - properties", () => {
+ it("calls the onFocus prop when provided one and given focus", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const onFocus = jest.fn();
+
+ const { wrapper } = mount({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ onFocus,
+ });
+
+ const node = wrapper.find(".node").first();
+ node.simulate("focus");
+
+ expect(onFocus.mock.calls).toHaveLength(1);
+ });
+
+ it("doesn't call the onFocus when given focus but focusable is false", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const onFocus = jest.fn();
+
+ const { wrapper } = mount({
+ focusable: false,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ onFocus,
+ });
+
+ const node = wrapper.find(".node").first();
+ node.simulate("focus");
+
+ expect(onFocus.mock.calls).toHaveLength(0);
+ });
+
+ it("calls onDoubleClick prop when provided one and double clicked", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const onDoubleClick = jest.fn();
+
+ const { wrapper } = mount({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ onDoubleClick,
+ });
+
+ const node = wrapper.find(".node").first();
+ node.simulate("doubleclick");
+
+ expect(onDoubleClick.mock.calls).toHaveLength(1);
+ });
+
+ it("calls the onCmdCtrlClick prop when provided and cmd/ctrl-clicked", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const onCmdCtrlClick = jest.fn();
+
+ const { wrapper } = mount({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ onCmdCtrlClick,
+ });
+
+ const node = wrapper.find(".node").first();
+ node.simulate("click", { ctrlKey: true });
+
+ expect(onCmdCtrlClick.mock.calls).toHaveLength(1);
+ });
+
+ it("calls the onLabel prop when provided one and label clicked", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const onLabelClick = jest.fn();
+
+ const { wrapper } = mount({
+ roots: [
+ {
+ path: "root",
+ name: "Label",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ onLabelClick,
+ });
+
+ const label = wrapper.find(".object-label").first();
+ label.simulate("click");
+
+ expect(onLabelClick.mock.calls).toHaveLength(1);
+ });
+
+ it("does not call the onLabel prop when the user selected text", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const onLabelClick = jest.fn();
+
+ const { wrapper } = mount({
+ roots: [
+ {
+ path: "root",
+ name: "Label",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ onLabelClick,
+ });
+
+ const label = wrapper.find(".object-label").first();
+
+ // Set a selection using the mock.
+ getSelection().setMockSelection("test");
+
+ label.simulate("click");
+
+ expect(onLabelClick.mock.calls).toHaveLength(0);
+
+ // Clear the selection for other tests.
+ getSelection().setMockSelection();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/expand.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/expand.test.js
new file mode 100644
index 0000000000..3243d8c259
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/expand.test.js
@@ -0,0 +1,435 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+const {
+ mountObjectInspector,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+
+const { MODE } = require(`devtools/client/shared/components/reps/index`);
+const ObjectFront = require("devtools/client/shared/components/test/node/__mocks__/object-front");
+const gripRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip`);
+const gripPropertiesStubs = require("devtools/client/shared/components/test/node/stubs/object-inspector/grip");
+const {
+ formatObjectInspector,
+ storeHasExactExpandedPaths,
+ storeHasExpandedPath,
+ storeHasLoadedProperty,
+ waitForDispatch,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const {
+ createNode,
+ NODE_TYPES,
+} = require("devtools/client/shared/components/object-inspector/utils/node");
+const {
+ getExpandedPaths,
+} = require("devtools/client/shared/components/object-inspector/reducer");
+
+const protoStub = {
+ prototype: {
+ type: "object",
+ actor: "server2.conn0.child1/obj628",
+ class: "Object",
+ },
+};
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ roots: [
+ {
+ path: "root-1",
+ contents: {
+ value: gripRepStubs.get("testMoreThanMaxProps"),
+ },
+ },
+ {
+ path: "root-2",
+ contents: {
+ value: gripRepStubs.get("testProxy"),
+ },
+ },
+ ],
+ createObjectFront: grip => ObjectFront(grip),
+ mode: MODE.LONG,
+ ...overrides,
+ };
+}
+const {
+ LongStringFront,
+} = require("devtools/client/shared/components/test/node/__mocks__/string-front");
+
+function getClient(overrides = {}) {
+ return {
+ releaseActor: () => {},
+
+ createObjectFront: grip =>
+ ObjectFront(grip, {
+ getPrototype: () => Promise.resolve(protoStub),
+ getProxySlots: () =>
+ Promise.resolve(gripRepStubs.get("testProxySlots")),
+ }),
+
+ createLongStringFront: grip =>
+ LongStringFront(grip, {
+ substring: async function(initiaLength, length) {
+ return "<<<<";
+ },
+ }),
+
+ getFrontByID: _id => null,
+
+ ...overrides,
+ };
+}
+
+function mount(props, { initialState, client = getClient() } = {}) {
+ return mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ initialState: {
+ objectInspector: {
+ ...initialState,
+ evaluations: new Map(),
+ },
+ },
+ });
+}
+
+describe("ObjectInspector - state", () => {
+ it("has the expected expandedPaths state when clicking nodes", async () => {
+ const { wrapper, store } = mount(
+ {},
+ {
+ initialState: {
+ loadedProperties: new Map([
+ ["root-1", gripPropertiesStubs.get("proto-properties-symbols")],
+ ]),
+ },
+ }
+ );
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ let nodes = wrapper.find(".node");
+
+ // Clicking on the root node adds it path to "expandedPaths".
+ const root1 = nodes.at(0);
+ const root2 = nodes.at(1);
+
+ root1.simulate("click");
+
+ expect(storeHasExactExpandedPaths(store, ["root-1"])).toBeTruthy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ //
+ // Clicking on the root node removes it path from "expandedPaths".
+ root1.simulate("click");
+ expect(storeHasExactExpandedPaths(store, [])).toBeTruthy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ root2.simulate("click");
+ await onPropertiesLoaded;
+ expect(storeHasExactExpandedPaths(store, ["root-2"])).toBeTruthy();
+
+ wrapper.update();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ root1.simulate("click");
+ expect(
+ storeHasExactExpandedPaths(store, ["root-1", "root-2"])
+ ).toBeTruthy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ nodes = wrapper.find(".node");
+ const propNode = nodes.at(1);
+ const symbolNode = nodes.at(2);
+ const protoNode = nodes.at(3);
+
+ propNode.simulate("click");
+ symbolNode.simulate("click");
+ protoNode.simulate("click");
+
+ expect(
+ storeHasExactExpandedPaths(store, [
+ "root-1",
+ "root-2",
+ "root-1â—¦<prototype>",
+ ])
+ ).toBeTruthy();
+
+ // The property and symbols have primitive values, and can't be expanded.
+ expect(getExpandedPaths(store.getState()).size).toBe(3);
+ });
+
+ it("has the expected state when expanding a node", async () => {
+ const { wrapper, store } = mount({}, {});
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ let nodes = wrapper.find(".node");
+ const root1 = nodes.at(0);
+
+ let onPropertiesLoad = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ root1.simulate("click");
+ await onPropertiesLoad;
+ wrapper.update();
+
+ expect(storeHasLoadedProperty(store, "root-1")).toBeTruthy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ nodes = wrapper.find(".node");
+ const protoNode = nodes.at(1);
+
+ onPropertiesLoad = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ protoNode.simulate("click");
+ await onPropertiesLoad;
+ wrapper.update();
+
+ // Once all the loading promises are resolved, actors and loadedProperties
+ // should have the expected values.
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ expect(storeHasLoadedProperty(store, "root-1â—¦<prototype>")).toBeTruthy();
+ });
+
+ it("does not handle actors when client does not have releaseActor function", async () => {
+ const { wrapper, store } = mount(
+ {},
+ { client: getClient({ releaseActor: null }) }
+ );
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ let nodes = wrapper.find(".node");
+ const root1 = nodes.at(0);
+
+ let onPropertiesLoad = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ root1.simulate("click");
+ await onPropertiesLoad;
+ wrapper.update();
+
+ expect(storeHasLoadedProperty(store, "root-1")).toBeTruthy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ nodes = wrapper.find(".node");
+ const protoNode = nodes.at(1);
+
+ onPropertiesLoad = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ protoNode.simulate("click");
+ await onPropertiesLoad;
+ wrapper.update();
+
+ // Once all the loading promises are resolved, actors and loadedProperties
+ // should have the expected values.
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ expect(storeHasLoadedProperty(store, "root-1â—¦<prototype>")).toBeTruthy();
+ });
+
+ it.skip("has the expected state when expanding a proxy node", async () => {
+ const { wrapper, store } = mount({});
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ let nodes = wrapper.find(".node");
+
+ const proxyNode = nodes.at(1);
+
+ let onLoadProperties = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ proxyNode.simulate("click");
+ await onLoadProperties;
+ wrapper.update();
+
+ // Once the properties are loaded, actors and loadedProperties should have
+ // the expected values.
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ nodes = wrapper.find(".node");
+ const handlerNode = nodes.at(3);
+ onLoadProperties = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ handlerNode.simulate("click");
+ await onLoadProperties;
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ expect(storeHasLoadedProperty(store, "root-2â—¦<handler>")).toBeTruthy();
+ });
+
+ it("does not expand if the user selected some text", async () => {
+ const { wrapper, store } = mount(
+ {},
+ {
+ initialSate: {
+ loadedProperties: new Map([
+ ["root-1", gripPropertiesStubs.get("proto-properties-symbols")],
+ ]),
+ },
+ }
+ );
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ const nodes = wrapper.find(".node");
+
+ // Set a selection using the mock.
+ getSelection().setMockSelection("test");
+
+ const root1 = nodes.at(0);
+ root1.simulate("click");
+ expect(storeHasExpandedPath(store, "root-1")).toBeFalsy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // Clear the selection for other tests.
+ getSelection().setMockSelection();
+ });
+
+ it("expands if user selected some text and clicked the arrow", async () => {
+ const { wrapper, store } = mount(
+ {},
+ {
+ initialState: {
+ loadedProperties: new Map([
+ ["root-1", gripPropertiesStubs.get("proto-properties-symbols")],
+ ]),
+ },
+ }
+ );
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ const nodes = wrapper.find(".node");
+
+ // Set a selection using the mock.
+ getSelection().setMockSelection("test");
+
+ const root1 = nodes.at(0);
+ root1.find(".arrow").simulate("click");
+ expect(getExpandedPaths(store.getState()).has("root-1")).toBeTruthy();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // Clear the selection for other tests.
+ getSelection().setMockSelection();
+ });
+
+ it("does not throw when expanding a block node", async () => {
+ const blockNode = createNode({
+ name: "Block",
+ contents: [
+ {
+ name: "a",
+ contents: {
+ value: 30,
+ },
+ },
+ {
+ name: "b",
+ contents: {
+ value: 32,
+ },
+ },
+ ],
+ type: NODE_TYPES.BLOCK,
+ });
+
+ const proxyNode = createNode({
+ name: "Proxy",
+ contents: {
+ value: gripRepStubs.get("testProxy"),
+ },
+ });
+
+ const { wrapper, store } = mount({
+ roots: [blockNode, proxyNode],
+ });
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ const nodes = wrapper.find(".node");
+ const root = nodes.at(0);
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ root.simulate("click");
+ await onPropertiesLoaded;
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("calls recordTelemetryEvent when expanding a node", async () => {
+ const recordTelemetryEvent = jest.fn();
+ const { wrapper, store } = mount(
+ {
+ recordTelemetryEvent,
+ },
+ {
+ initialState: {
+ loadedProperties: new Map([
+ ["root-1", gripPropertiesStubs.get("proto-properties-symbols")],
+ ]),
+ },
+ }
+ );
+
+ let nodes = wrapper.find(".node");
+ const root1 = nodes.at(0);
+ const root2 = nodes.at(1);
+
+ // Expanding a node calls recordTelemetryEvent.
+ root1.simulate("click");
+ expect(recordTelemetryEvent.mock.calls).toHaveLength(1);
+ expect(recordTelemetryEvent.mock.calls[0][0]).toEqual("object_expanded");
+
+ // Collapsing a node does not call recordTelemetryEvent.
+ root1.simulate("click");
+ expect(recordTelemetryEvent.mock.calls).toHaveLength(1);
+
+ // Expanding another node calls recordTelemetryEvent.
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ root2.simulate("click");
+ await onPropertiesLoaded;
+ expect(recordTelemetryEvent.mock.calls).toHaveLength(2);
+ expect(recordTelemetryEvent.mock.calls[1][0]).toEqual("object_expanded");
+
+ wrapper.update();
+
+ // Re-expanding a node calls recordTelemetryEvent.
+ root1.simulate("click");
+ expect(recordTelemetryEvent.mock.calls).toHaveLength(3);
+ expect(recordTelemetryEvent.mock.calls[2][0]).toEqual("object_expanded");
+
+ nodes = wrapper.find(".node");
+ const propNode = nodes.at(1);
+ const symbolNode = nodes.at(2);
+ const protoNode = nodes.at(3);
+
+ propNode.simulate("click");
+ symbolNode.simulate("click");
+ protoNode.simulate("click");
+
+ // The property and symbols have primitive values, and can't be expanded.
+ expect(recordTelemetryEvent.mock.calls).toHaveLength(4);
+ expect(recordTelemetryEvent.mock.calls[3][0]).toEqual("object_expanded");
+ });
+
+ it("expanding a getter returning a longString does not throw", async () => {
+ const { wrapper, store } = mount(
+ {
+ focusable: false,
+ },
+ {
+ initialState: {
+ loadedProperties: new Map([
+ ["root-1", gripPropertiesStubs.get("longs-string-safe-getter")],
+ ]),
+ },
+ }
+ );
+
+ wrapper
+ .find(".node")
+ .at(0)
+ .simulate("click");
+ wrapper.update();
+
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ wrapper
+ .find(".node")
+ .at(1)
+ .simulate("click");
+ await onPropertiesLoaded;
+
+ wrapper.update();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/function.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/function.test.js
new file mode 100644
index 0000000000..0e5bb9573a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/function.test.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ createNode,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+const functionStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/function.js");
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 1,
+ ...overrides,
+ };
+}
+
+function mount(props) {
+ const client = {
+ createObjectFront: grip => ObjectFront(grip),
+ getFrontByID: _id => null,
+ };
+
+ return mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ });
+}
+
+describe("ObjectInspector - functions", () => {
+ it("renders named function properties as expected", () => {
+ const stub = functionStubs.get("Named");
+ const { wrapper } = mount({
+ roots: [
+ createNode({
+ name: "fn",
+ contents: { value: stub },
+ }),
+ ],
+ });
+
+ const nodes = wrapper.find(".node");
+ const functionNode = nodes.first();
+ expect(functionNode.text()).toBe("fn:testName()");
+ });
+
+ it("renders anon function properties as expected", () => {
+ const stub = functionStubs.get("Anon");
+ const { wrapper } = mount({
+ roots: [
+ createNode({
+ name: "fn",
+ contents: { value: stub },
+ }),
+ ],
+ });
+
+ const nodes = wrapper.find(".node");
+ const functionNode = nodes.first();
+ // It should have the name of the property.
+ expect(functionNode.text()).toBe("fn()");
+ });
+
+ it("renders non-TINY mode functions as expected", () => {
+ const stub = functionStubs.get("Named");
+ const { wrapper } = mount({
+ autoExpandDepth: 0,
+ roots: [
+ {
+ path: "root",
+ name: "x",
+ contents: { value: stub },
+ },
+ ],
+ mode: MODE.LONG,
+ });
+
+ const nodes = wrapper.find(".node");
+ const functionNode = nodes.first();
+ // It should have the name of the property.
+ expect(functionNode.text()).toBe("x: function testName()");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/getter-setter.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/getter-setter.test.js
new file mode 100644
index 0000000000..e5fbbff7de
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/getter-setter.test.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ formatObjectInspector,
+ waitForLoadedProperties,
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+const {
+ makeNodesForProperties,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const accessorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 1,
+ createObjectFront: grip => ObjectFront(grip),
+ mode: MODE.LONG,
+ ...overrides,
+ };
+}
+
+function mount(stub, propsOverride = {}) {
+ const client = { createObjectFront: grip => ObjectFront(grip) };
+
+ const root = { path: "root", name: "root" };
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ x: stub,
+ },
+ },
+ root
+ );
+ root.contents = nodes;
+
+ return mountObjectInspector({
+ client,
+ props: generateDefaults({ roots: [root], ...propsOverride }),
+ });
+}
+
+describe("ObjectInspector - getters & setters", () => {
+ it("renders getters as expected", async () => {
+ const { store, wrapper } = mount(accessorStubs.get("getter"));
+ await waitForLoadedProperties(store, ["root"]);
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("renders setters as expected", async () => {
+ const { store, wrapper } = mount(accessorStubs.get("setter"));
+ await waitForLoadedProperties(store, ["root"]);
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("renders getters and setters as expected", async () => {
+ const { store, wrapper } = mount(accessorStubs.get("getter setter"));
+ await waitForLoadedProperties(store, ["root"]);
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("onInvokeGetterButtonClick + getter", async () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const { store, wrapper } = mount(accessorStubs.get("getter"), {
+ onInvokeGetterButtonClick,
+ });
+ await waitForLoadedProperties(store, ["root"]);
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("onInvokeGetterButtonClick + setter", async () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const { store, wrapper } = mount(accessorStubs.get("setter"), {
+ onInvokeGetterButtonClick,
+ });
+ await waitForLoadedProperties(store, ["root"]);
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("onInvokeGetterButtonClick + getter & setter", async () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const { store, wrapper } = mount(accessorStubs.get("getter setter"), {
+ onInvokeGetterButtonClick,
+ });
+ await waitForLoadedProperties(store, ["root"]);
+ wrapper.update();
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/keyboard-navigation.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/keyboard-navigation.test.js
new file mode 100644
index 0000000000..c36c611a53
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/keyboard-navigation.test.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ mountObjectInspector,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const { MODE } = require("devtools/client/shared/components/reps/index");
+
+const {
+ formatObjectInspector,
+ waitForDispatch,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const ObjectFront = require("devtools/client/shared/components/test/node/__mocks__/object-front");
+const gripRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip`);
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ mode: MODE.LONG,
+ ...overrides,
+ };
+}
+
+function mount(props) {
+ const client = {
+ createObjectFront: grip => ObjectFront(grip),
+ getFrontByID: _id => null,
+ };
+
+ return mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ });
+}
+
+describe("ObjectInspector - keyboard navigation", () => {
+ it("works as expected", async () => {
+ const stub = gripRepStubs.get("testMaxProps");
+
+ const { wrapper, store } = mount({
+ roots: [{ path: "root", contents: { value: stub } }],
+ });
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ wrapper.simulate("focus");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // Pressing right arrow key should expand the node and lod its properties.
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ simulateKeyDown(wrapper, "ArrowRight");
+ await onPropertiesLoaded;
+ wrapper.update();
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // The child node should be focused.
+ keyNavigate(wrapper, store, "ArrowDown");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // The root node should be focused again.
+ keyNavigate(wrapper, store, "ArrowLeft");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // The child node should be focused again.
+ keyNavigate(wrapper, store, "ArrowRight");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // The root node should be focused again.
+ keyNavigate(wrapper, store, "ArrowUp");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ wrapper.simulate("blur");
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+});
+
+function keyNavigate(wrapper, store, key) {
+ simulateKeyDown(wrapper, key);
+ wrapper.update();
+}
+
+function simulateKeyDown(wrapper, key) {
+ wrapper.simulate("keydown", {
+ key,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+}
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/properties.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/properties.test.js
new file mode 100644
index 0000000000..8773cfd49f
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/properties.test.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+
+const {
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+const gripRepStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+
+const {
+ formatObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ createObjectFront: grip => ObjectFront(grip),
+ ...overrides,
+ };
+}
+
+function getEnumPropertiesMock() {
+ return jest.fn(() => ({
+ slice: () => ({}),
+ }));
+}
+
+function mount(props, { initialState } = {}) {
+ const enumProperties = getEnumPropertiesMock();
+
+ const client = {
+ createObjectFront: grip => ObjectFront(grip, { enumProperties }),
+ getFrontByID: _id => null,
+ };
+
+ const obj = mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ initialState,
+ });
+
+ return { ...obj, enumProperties };
+}
+describe("ObjectInspector - properties", () => {
+ it("does not load properties if properties are already loaded", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+
+ const { enumProperties } = mount(
+ {
+ autoExpandDepth: 1,
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ },
+ {
+ initialState: {
+ objectInspector: {
+ loadedProperties: new Map([
+ ["root", { ownProperties: stub.preview.ownProperties }],
+ ]),
+ evaluations: new Map(),
+ },
+ },
+ }
+ );
+
+ expect(enumProperties.mock.calls).toHaveLength(0);
+ });
+
+ it("calls enumProperties when expandable leaf is clicked", () => {
+ const stub = gripRepStubs.get("testMaxProps");
+ const { enumProperties, wrapper } = mount({
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ createObjectFront: grip => ObjectFront(grip, { enumProperties }),
+ });
+
+ const node = wrapper.find(".node");
+ node.simulate("click");
+
+ // The function is called twice, to get both non-indexed and indexed props.
+ expect(enumProperties.mock.calls).toHaveLength(2);
+ expect(enumProperties.mock.calls[0][0]).toEqual({
+ ignoreNonIndexedProperties: true,
+ });
+ expect(enumProperties.mock.calls[1][0]).toEqual({
+ ignoreIndexedProperties: true,
+ });
+ });
+
+ it("renders uninitialized bindings", () => {
+ const { wrapper } = mount({
+ roots: [
+ {
+ name: "someFoo",
+ path: "root/someFoo",
+ contents: {
+ value: {
+ uninitialized: true,
+ },
+ },
+ },
+ ],
+ });
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("renders unmapped bindings", () => {
+ const { wrapper } = mount({
+ roots: [
+ {
+ name: "someFoo",
+ path: "root/someFoo",
+ contents: {
+ value: {
+ unmapped: true,
+ },
+ },
+ },
+ ],
+ });
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+
+ it("renders unscoped bindings", () => {
+ const { wrapper } = mount({
+ roots: [
+ {
+ name: "someFoo",
+ path: "root/someFoo",
+ contents: {
+ value: {
+ unscoped: true,
+ },
+ },
+ },
+ ],
+ });
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/proxy.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/proxy.test.js
new file mode 100644
index 0000000000..0bf716ccff
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/proxy.test.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+const {
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const stub = gripStubs.get("testProxy");
+const proxySlots = gripStubs.get("testProxySlots");
+const {
+ formatObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+function generateDefaults(overrides) {
+ return {
+ roots: [
+ {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ },
+ ],
+ autoExpandDepth: 1,
+ mode: MODE.LONG,
+ ...overrides,
+ };
+}
+
+function getEnumPropertiesMock() {
+ return jest.fn(() => ({
+ slice: () => ({}),
+ }));
+}
+
+function getProxySlotsMock() {
+ return jest.fn(() => proxySlots);
+}
+
+function mount(props, { initialState } = {}) {
+ const enumProperties = getEnumPropertiesMock();
+ const getProxySlots = getProxySlotsMock();
+
+ const client = {
+ createObjectFront: grip =>
+ ObjectFront(grip, { enumProperties, getProxySlots }),
+ getFrontByID: _id => null,
+ };
+
+ const obj = mountObjectInspector({
+ client,
+ props: generateDefaults(props),
+ initialState,
+ });
+
+ return { ...obj, enumProperties, getProxySlots };
+}
+
+describe("ObjectInspector - Proxy", () => {
+ it("renders Proxy as expected", () => {
+ const { wrapper, enumProperties, getProxySlots } = mount(
+ {},
+ {
+ initialState: {
+ objectInspector: {
+ // Have the prototype already loaded so the component does not call
+ // enumProperties for the root's properties.
+ loadedProperties: new Map([["root", proxySlots]]),
+ evaluations: new Map(),
+ },
+ },
+ }
+ );
+
+ expect(formatObjectInspector(wrapper)).toMatchSnapshot();
+
+ // enumProperties should not have been called.
+ expect(enumProperties.mock.calls).toHaveLength(0);
+
+ // getProxySlots should not have been called.
+ expect(getProxySlots.mock.calls).toHaveLength(0);
+ });
+
+ it("calls enumProperties on <target> and <handler> clicks", () => {
+ const { wrapper, enumProperties } = mount(
+ {},
+ {
+ initialState: {
+ objectInspector: {
+ // Have the prototype already loaded so the component does not call
+ // enumProperties for the root's properties.
+ loadedProperties: new Map([["root", proxySlots]]),
+ evaluations: new Map(),
+ },
+ },
+ }
+ );
+
+ const nodes = wrapper.find(".node");
+
+ const targetNode = nodes.at(1);
+ const handlerNode = nodes.at(2);
+
+ targetNode.simulate("click");
+ // The function is called twice,
+ // to get both non-indexed and indexed properties.
+ expect(enumProperties.mock.calls).toHaveLength(2);
+ expect(enumProperties.mock.calls[0][0]).toEqual({
+ ignoreNonIndexedProperties: true,
+ });
+ expect(enumProperties.mock.calls[1][0]).toEqual({
+ ignoreIndexedProperties: true,
+ });
+
+ handlerNode.simulate("click");
+ // The function is called twice,
+ // to get both non-indexed and indexed properties.
+ expect(enumProperties.mock.calls).toHaveLength(4);
+ expect(enumProperties.mock.calls[2][0]).toEqual({
+ ignoreNonIndexedProperties: true,
+ });
+ expect(enumProperties.mock.calls[3][0]).toEqual({
+ ignoreIndexedProperties: true,
+ });
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/should-item-update.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/should-item-update.test.js
new file mode 100644
index 0000000000..645b4ede6c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/should-item-update.test.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global jest */
+
+const {
+ mountObjectInspector,
+} = require("devtools/client/shared/components/test/node/components/object-inspector/test-utils");
+const ObjectFront = require("devtools/client/shared/components/test/node/__mocks__/object-front");
+const {
+ LongStringFront,
+} = require("devtools/client/shared/components/test/node/__mocks__/string-front");
+
+const longStringStubs = require(`devtools/client/shared/components/test/node/stubs/reps/long-string`);
+const gripStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip`);
+
+function mount(stub) {
+ const root = {
+ path: "root",
+ contents: {
+ value: stub,
+ },
+ };
+
+ const { wrapper } = mountObjectInspector({
+ client: {
+ createObjectFront: grip => ObjectFront(grip),
+ createLongStringFront: grip => LongStringFront(grip),
+ getFrontByID: _id => null,
+ },
+ props: {
+ roots: [root],
+ },
+ });
+
+ return { wrapper, root };
+}
+
+describe("shouldItemUpdate", () => {
+ it("for longStrings", () => {
+ shouldItemUpdateCheck(longStringStubs.get("testUnloadedFullText"), true, 2);
+ });
+
+ it("for basic object", () => {
+ shouldItemUpdateCheck(gripStubs.get("testBasic"), false, 1);
+ });
+});
+
+function shouldItemUpdateCheck(
+ stub,
+ shouldItemUpdateResult,
+ renderCallsLength
+) {
+ const { root, wrapper } = mount(stub);
+
+ const shouldItemUpdateSpy = getShouldItemUpdateSpy(wrapper);
+ const treeNodeRenderSpy = getTreeNodeRenderSpy(wrapper);
+
+ updateObjectInspectorTree(wrapper);
+
+ checkShouldItemUpdate(shouldItemUpdateSpy, root, shouldItemUpdateResult);
+ expect(treeNodeRenderSpy.mock.calls).toHaveLength(renderCallsLength);
+}
+
+function checkShouldItemUpdate(spy, item, result) {
+ expect(spy.mock.calls).toHaveLength(1);
+ expect(spy.mock.calls[0][0]).toBe(item);
+ expect(spy.mock.calls[0][1]).toBe(item);
+ expect(spy.mock.results[0].value).toBe(result);
+}
+
+function getInstance(wrapper, selector) {
+ return wrapper
+ .find(selector)
+ .first()
+ .instance();
+}
+
+function getShouldItemUpdateSpy(wrapper) {
+ return jest.spyOn(
+ getInstance(wrapper, "ObjectInspector"),
+ "shouldItemUpdate"
+ );
+}
+
+function getTreeNodeRenderSpy(wrapper) {
+ return jest.spyOn(getInstance(wrapper, "TreeNode"), "render");
+}
+
+function updateObjectInspectorTree(wrapper) {
+ // Update the ObjectInspector first to propagate its updated options to the
+ // Tree component.
+ getInstance(wrapper, "ObjectInspector").forceUpdate();
+ getInstance(wrapper, "Tree").forceUpdate();
+}
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/component/window.test.js b/devtools/client/shared/components/test/node/components/object-inspector/component/window.test.js
new file mode 100644
index 0000000000..c0b1036385
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/component/window.test.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ createNode,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const {
+ waitForDispatch,
+ mountObjectInspector,
+} = require("resource://devtools/client/shared/components/test/node/components/object-inspector/test-utils.js");
+
+const gripWindowStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+const ObjectFront = require("resource://devtools/client/shared/components/test/node/__mocks__/object-front.js");
+const windowNode = createNode({
+ name: "window",
+ contents: { value: gripWindowStubs.get("Window")._grip },
+});
+
+const client = {
+ createObjectFront: grip => ObjectFront(grip),
+ getFrontByID: _id => null,
+};
+
+function generateDefaults(overrides) {
+ return {
+ autoExpandDepth: 0,
+ roots: [windowNode],
+ ...overrides,
+ };
+}
+
+describe("ObjectInspector - dimTopLevelWindow", () => {
+ it("renders window as expected when dimTopLevelWindow is true", async () => {
+ const props = generateDefaults({
+ dimTopLevelWindow: true,
+ });
+
+ const { wrapper, store } = mountObjectInspector({ client, props });
+ let nodes = wrapper.find(".node");
+ const node = nodes.at(0);
+
+ expect(nodes.at(0).hasClass("lessen")).toBeTruthy();
+ expect(wrapper).toMatchSnapshot();
+
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ node.simulate("click");
+ await onPropertiesLoaded;
+ wrapper.update();
+
+ nodes = wrapper.find(".node");
+ expect(nodes.at(0).hasClass("lessen")).toBeFalsy();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders collapsed top-level window when dimTopLevelWindow =false", () => {
+ // The window node should not have the "lessen" class when
+ // dimTopLevelWindow is falsy.
+ const props = generateDefaults();
+ const { wrapper } = mountObjectInspector({ client, props });
+
+ expect(wrapper.find(".node.lessen").exists()).toBeFalsy();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders sub-level window", async () => {
+ // The window node should not have the "lessen" class when it is not at
+ // top level.
+ const root = createNode({
+ name: "root",
+ contents: [windowNode],
+ });
+
+ const props = generateDefaults({
+ roots: [root],
+ dimTopLevelWindow: true,
+ injectWaitService: true,
+ });
+ const { wrapper, store } = mountObjectInspector({ client, props });
+
+ let nodes = wrapper.find(".node");
+ const node = nodes.at(0);
+ const onPropertiesLoaded = waitForDispatch(store, "NODE_PROPERTIES_LOADED");
+ node.simulate("click");
+ await onPropertiesLoaded;
+ wrapper.update();
+
+ nodes = wrapper.find(".node");
+ const win = nodes.at(1);
+
+ // Make sure we target the window object.
+ expect(win.find(".objectBox-Window").exists()).toBeTruthy();
+ expect(win.hasClass("lessen")).toBeFalsy();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/test-utils.js b/devtools/client/shared/components/test/node/components/object-inspector/test-utils.js
new file mode 100644
index 0000000000..79d3e41161
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/test-utils.js
@@ -0,0 +1,231 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const { mount } = require("enzyme");
+const { createFactory } = require("resource://devtools/client/shared/vendor/react.js");
+
+const { Provider } = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ combineReducers,
+ createStore,
+ applyMiddleware,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+
+const { thunk } = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const {
+ waitUntilService,
+} = require("resource://devtools/client/shared/redux/middleware/wait-service.js");
+
+/**
+ * Redux store utils
+ * @module utils/create-store
+ */
+const objectInspector = require("resource://devtools/client/shared/components/object-inspector/index.js");
+const {
+ getLoadedProperties,
+ getLoadedPropertyKeys,
+ getExpandedPaths,
+ getExpandedPathKeys,
+} = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+
+const ObjectInspector = createFactory(objectInspector.ObjectInspector);
+
+const {
+ NAME: WAIT_UNTIL_TYPE,
+} = require("resource://devtools/client/shared/redux/middleware/wait-service.js");
+
+/*
+ * Takes an Enzyme wrapper (obtained with mount/shallow/…) and
+ * returns a stringified version of the ObjectInspector, e.g.
+ *
+ * ▼ Map { "a" → "value-a", "b" → "value-b" }
+ * | size : 2
+ * | â–¼ <entries>
+ * | | ▼ 0 : "a" → "value-a"
+ * | | | <key> : "a"
+ * | | | <value> : "value-a"
+ * | | ▼ 1 : "b" → "value-b"
+ * | | | <key> : "b"
+ * | | | <value> : "value-b"
+ * | ▼ <prototype> : Object { … }
+ *
+ */
+function formatObjectInspector(wrapper) {
+ const hasFocusedNode = wrapper.find(".tree-node.focused").length > 0;
+ const textTree = wrapper
+ .find(".tree-node")
+ .map(node => {
+ const indentStr = "| ".repeat((node.prop("aria-level") || 1) - 1);
+ // Need to target .arrow or Enzyme will also match the ArrowExpander
+ // component.
+ const arrow = node.find(".arrow");
+ let arrowStr = " ";
+ if (arrow.exists()) {
+ arrowStr = arrow.hasClass("expanded") ? "▼ " : "▶︎ ";
+ } else {
+ arrowStr = " ";
+ }
+
+ const icon = node
+ .find(".node")
+ .first()
+ .hasClass("block")
+ ? "☲ "
+ : "";
+ let text = `${indentStr}${arrowStr}${icon}${getSanitizedNodeText(node)}`;
+
+ if (node.find("button.invoke-getter").exists()) {
+ text = `${text}(>>)`;
+ }
+
+ if (!hasFocusedNode) {
+ return text;
+ }
+ return node.hasClass("focused") ? `[ ${text} ]` : ` ${text}`;
+ })
+ .join("\n");
+ // Wrap the text representation in new lines so it keeps alignment between
+ // tree nodes.
+ return `\n${textTree}\n`;
+}
+
+function getSanitizedNodeText(node) {
+ // Stripping off the invisible space used in the indent.
+ return node.text().replace(/^\u200B+/, "");
+}
+
+/**
+ * Wait for a specific action type to be dispatched.
+ *
+ * @param {Object} store: Redux store
+ * @param {String} type: type of the actin to wait for
+ * @return {Promise}
+ */
+function waitForDispatch(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ type: WAIT_UNTIL_TYPE,
+ predicate: action => action.type === type,
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ },
+ });
+ });
+}
+
+/**
+ * Wait until the condition evaluates to something truthy
+ * @param {function} condition: function that we need for returning something
+ * truthy.
+ * @param {int} interval: Time to wait before trying to evaluate condition again
+ * @param {int} maxTries: Number of evaluation to try.
+ */
+async function waitFor(condition, interval = 50, maxTries = 100) {
+ let res = condition();
+ while (!res) {
+ await new Promise(done => setTimeout(done, interval));
+ maxTries--;
+
+ if (maxTries <= 0) {
+ throw new Error("waitFor - maxTries limit hit");
+ }
+
+ res = condition();
+ }
+ return res;
+}
+
+/**
+ * Wait until the state has all the expected keys for the loadedProperties
+ * state prop.
+ * @param {Redux Store} store: function that we need for returning something
+ * truthy.
+ * @param {Array} expectedKeys: Array of stringified keys.
+ * @param {int} interval: Time to wait before trying to evaluate condition again
+ * @param {int} maxTries: Number of evaluation to try.
+ */
+function waitForLoadedProperties(store, expectedKeys, interval, maxTries) {
+ return waitFor(
+ () => storeHasLoadedPropertiesKeys(store, expectedKeys),
+ interval,
+ maxTries
+ );
+}
+
+function storeHasLoadedPropertiesKeys(store, expectedKeys) {
+ return expectedKeys.every(key => storeHasLoadedProperty(store, key));
+}
+
+function storeHasLoadedProperty(store, key) {
+ return getLoadedPropertyKeys(store.getState()).some(
+ k => k.toString() === key
+ );
+}
+
+function storeHasExactLoadedProperties(store, expectedKeys) {
+ return (
+ expectedKeys.length === getLoadedProperties(store.getState()).size &&
+ expectedKeys.every(key => storeHasLoadedProperty(store, key))
+ );
+}
+
+function storeHasExpandedPaths(store, expectedKeys) {
+ return expectedKeys.every(key => storeHasExpandedPath(store, key));
+}
+
+function storeHasExpandedPath(store, key) {
+ return getExpandedPathKeys(store.getState()).some(k => k.toString() === key);
+}
+
+function storeHasExactExpandedPaths(store, expectedKeys) {
+ return (
+ expectedKeys.length === getExpandedPaths(store.getState()).size &&
+ expectedKeys.every(key => storeHasExpandedPath(store, key))
+ );
+}
+
+function createOiStore(client, initialState = {}) {
+ const reducers = { objectInspector: objectInspector.reducer.default };
+ return configureStore({
+ thunkArgs: { client },
+ })(combineReducers(reducers), initialState);
+}
+
+const configureStore = (opts = {}) => {
+ const middleware = [thunk(opts.thunkArgs), waitUntilService];
+ return applyMiddleware(...middleware)(createStore);
+};
+
+function mountObjectInspector({ props, client, initialState = {} }) {
+ if (initialState.objectInspector) {
+ initialState.objectInspector = {
+ expandedPaths: new Set(),
+ loadedProperties: new Map(),
+ ...initialState.objectInspector,
+ };
+ }
+ const store = createOiStore(client, initialState);
+ const wrapper = mount(
+ createFactory(Provider)({ store }, ObjectInspector(props))
+ );
+
+ const tree = wrapper.find(".tree");
+
+ return { store, tree, wrapper, client };
+}
+
+module.exports = {
+ formatObjectInspector,
+ storeHasExpandedPaths,
+ storeHasExpandedPath,
+ storeHasExactExpandedPaths,
+ storeHasLoadedPropertiesKeys,
+ storeHasLoadedProperty,
+ storeHasExactLoadedProperties,
+ waitFor,
+ waitForDispatch,
+ waitForLoadedProperties,
+ mountObjectInspector,
+ createStore: createOiStore,
+};
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/__snapshots__/promises.test.js.snap b/devtools/client/shared/components/test/node/components/object-inspector/utils/__snapshots__/promises.test.js.snap
new file mode 100644
index 0000000000..75903c0ff1
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/__snapshots__/promises.test.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`promises utils function makeNodesForPromiseProperties 1`] = `
+Array [
+ Object {
+ "contents": Object {
+ "value": "rejected",
+ },
+ "meta": undefined,
+ "name": "<state>",
+ "parent": Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server2.conn2.child1/obj36",
+ "class": "Promise",
+ "type": "object",
+ },
+ },
+ "path": "root",
+ },
+ "path": "rootâ—¦<state>",
+ "propertyName": undefined,
+ "type": Symbol(<state>),
+ },
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "type": "3",
+ },
+ },
+ "meta": undefined,
+ "name": "<reason>",
+ "parent": Object {
+ "contents": Object {
+ "value": Object {
+ "actor": "server2.conn2.child1/obj36",
+ "class": "Promise",
+ "type": "object",
+ },
+ },
+ "path": "root",
+ },
+ "path": "rootâ—¦<reason>",
+ "propertyName": undefined,
+ "type": Symbol(<reason>),
+ },
+]
+`;
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/create-node.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/create-node.test.js
new file mode 100644
index 0000000000..792ad2dfb0
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/create-node.test.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const { createNode, NODE_TYPES } = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+describe("createNode", () => {
+ it("returns null when contents is undefined", () => {
+ expect(createNode({ name: "name" })).toBeNull();
+ });
+
+ it("does not return null when contents is null", () => {
+ expect(
+ createNode({
+ name: "name",
+ path: "path",
+ contents: null,
+ })
+ ).not.toBe(null);
+ });
+
+ it("returns the expected object when parent is undefined", () => {
+ const node = createNode({
+ name: "name",
+ path: "path",
+ contents: "contents",
+ });
+ expect(node).toEqual({
+ name: "name",
+ path: node.path,
+ contents: "contents",
+ type: NODE_TYPES.GRIP,
+ });
+ });
+
+ it("returns the expected object when parent is not null", () => {
+ const root = createNode({ name: "name", contents: null });
+ const child = createNode({
+ parent: root,
+ name: "name",
+ path: "path",
+ contents: "contents",
+ });
+ expect(child.parent).toEqual(root);
+ });
+
+ it("returns the expected object when type is not undefined", () => {
+ const root = createNode({ name: "name", contents: null });
+ const child = createNode({
+ parent: root,
+ name: "name",
+ path: "path",
+ contents: "contents",
+ type: NODE_TYPES.BUCKET,
+ });
+
+ expect(child.type).toEqual(NODE_TYPES.BUCKET);
+ });
+
+ it("uses the name property for the path when path is not provided", () => {
+ expect(
+ createNode({ name: "name", contents: "contents" }).path.toString()
+ ).toBe("name");
+ });
+
+ it("wraps the path in a Symbol when provided", () => {
+ expect(
+ createNode({
+ name: "name",
+ path: "path",
+ contents: "contents",
+ }).path.toString()
+ ).toBe("path");
+ });
+
+ it("uses parent path to compute its path", () => {
+ const root = createNode({ name: "root", contents: null });
+ expect(
+ createNode({
+ parent: root,
+ name: "name",
+ path: "path",
+ contents: "contents",
+ }).path.toString()
+ ).toBe("rootâ—¦path");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/get-children.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/get-children.test.js
new file mode 100644
index 0000000000..f57f82073d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/get-children.test.js
@@ -0,0 +1,278 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const accessorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+const performanceStubs = require("resource://devtools/client/shared/components/test/node/stubs/object-inspector/performance.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripEntryStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-entry.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+
+const {
+ createNode,
+ getChildren,
+ getValue,
+ makeNodesForProperties,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+function createRootNodeWithAccessorProperty(accessorStub) {
+ const node = { name: "root", path: "rootpath" };
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ x: accessorStub,
+ },
+ },
+ node
+ );
+ node.contents = nodes;
+
+ return createNode(node);
+}
+
+describe("getChildren", () => {
+ it("accessors - getter", () => {
+ const children = getChildren({
+ item: createRootNodeWithAccessorProperty(accessorStubs.get("getter")),
+ });
+
+ const names = children.map(n => n.name);
+ const paths = children.map(n => n.path.toString());
+
+ expect(names).toEqual(["x", "<get x()>"]);
+ expect(paths).toEqual(["rootpathâ—¦x", "rootpathâ—¦<get x()>"]);
+ });
+
+ it("accessors - setter", () => {
+ const children = getChildren({
+ item: createRootNodeWithAccessorProperty(accessorStubs.get("setter")),
+ });
+
+ const names = children.map(n => n.name);
+ const paths = children.map(n => n.path.toString());
+
+ expect(names).toEqual(["x", "<set x()>"]);
+ expect(paths).toEqual(["rootpathâ—¦x", "rootpathâ—¦<set x()>"]);
+ });
+
+ it("accessors - getter & setter", () => {
+ const children = getChildren({
+ item: createRootNodeWithAccessorProperty(
+ accessorStubs.get("getter setter")
+ ),
+ });
+
+ const names = children.map(n => n.name);
+ const paths = children.map(n => n.path.toString());
+
+ expect(names).toEqual(["x", "<get x()>", "<set x()>"]);
+ expect(paths).toEqual([
+ "rootpathâ—¦x",
+ "rootpathâ—¦<get x()>",
+ "rootpathâ—¦<set x()>",
+ ]);
+ });
+
+ it("returns the expected nodes for Proxy", () => {
+ const proxyNode = createNode({
+ name: "root",
+ path: "rootpath",
+ contents: { value: gripStubs.get("testProxy") },
+ });
+ const loadedProperties = new Map([
+ [proxyNode.path, gripStubs.get("testProxySlots")],
+ ]);
+ const nodes = getChildren({ item: proxyNode, loadedProperties });
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["<target>", "<handler>"]);
+ expect(paths).toEqual(["rootpathâ—¦<target>", "rootpathâ—¦<handler>"]);
+ });
+
+ it("safeGetterValues", () => {
+ const stub = performanceStubs.get("timing");
+ const root = createNode({
+ name: "root",
+ path: "rootpath",
+ contents: {
+ value: {
+ actor: "rootactor",
+ type: "object",
+ },
+ },
+ });
+ const nodes = getChildren({
+ item: root,
+ loadedProperties: new Map([[root.path, stub]]),
+ });
+
+ const nodeEntries = nodes.map(n => [n.name, getValue(n)]);
+ const nodePaths = nodes.map(n => n.path.toString());
+
+ const childrenEntries = [
+ ["connectEnd", 1500967716401],
+ ["connectStart", 1500967716401],
+ ["domComplete", 1500967716719],
+ ["domContentLoadedEventEnd", 1500967716715],
+ ["domContentLoadedEventStart", 1500967716696],
+ ["domInteractive", 1500967716552],
+ ["domLoading", 1500967716426],
+ ["domainLookupEnd", 1500967716401],
+ ["domainLookupStart", 1500967716401],
+ ["fetchStart", 1500967716401],
+ ["loadEventEnd", 1500967716720],
+ ["loadEventStart", 1500967716719],
+ ["navigationStart", 1500967716401],
+ ["redirectEnd", 0],
+ ["redirectStart", 0],
+ ["requestStart", 1500967716401],
+ ["responseEnd", 1500967716401],
+ ["responseStart", 1500967716401],
+ ["secureConnectionStart", 1500967716401],
+ ["unloadEventEnd", 0],
+ ["unloadEventStart", 0],
+ ["<prototype>", stub.prototype],
+ ];
+ const childrenPaths = childrenEntries.map(([name]) => `rootpathâ—¦${name}`);
+
+ expect(nodeEntries).toEqual(childrenEntries);
+ expect(nodePaths).toEqual(childrenPaths);
+ });
+
+ it("gets data from the cache when it exists", () => {
+ const mapNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("testSymbolKeyedMap"),
+ },
+ });
+ const cachedData = "";
+ const children = getChildren({
+ cachedNodes: new Map([[mapNode.path, cachedData]]),
+ item: mapNode,
+ });
+ expect(children).toBe(cachedData);
+ });
+
+ it("returns an empty array if the node does not represent an object", () => {
+ const node = createNode({ name: "root", contents: { value: 42 } });
+ expect(
+ getChildren({
+ item: node,
+ })
+ ).toEqual([]);
+ });
+
+ it("returns an empty array if a grip node has no loaded properties", () => {
+ const node = createNode({
+ name: "root",
+ contents: { value: gripMapStubs.get("testMaxProps") },
+ });
+ expect(
+ getChildren({
+ item: node,
+ })
+ ).toEqual([]);
+ });
+
+ it("adds children to cache when a grip node has loaded properties", () => {
+ const stub = performanceStubs.get("timing");
+ const cachedNodes = new Map();
+
+ const rootNode = createNode({
+ name: "root",
+ contents: {
+ value: {
+ actor: "rootactor",
+ type: "object",
+ },
+ },
+ });
+ const children = getChildren({
+ cachedNodes,
+ item: rootNode,
+ loadedProperties: new Map([[rootNode.path, stub]]),
+ });
+ expect(cachedNodes.get(rootNode.path)).toBe(children);
+ });
+
+ it("adds children to cache when it already has some", () => {
+ const cachedNodes = new Map();
+ const children = [""];
+ const rootNode = createNode({ name: "root", contents: children });
+ getChildren({
+ cachedNodes,
+ item: rootNode,
+ });
+ expect(cachedNodes.get(rootNode.path)).toBe(children);
+ });
+
+ it("adds children to cache on a node with accessors", () => {
+ const cachedNodes = new Map();
+ const node = createRootNodeWithAccessorProperty(
+ accessorStubs.get("getter setter")
+ );
+
+ const children = getChildren({
+ cachedNodes,
+ item: node,
+ });
+ expect(cachedNodes.get(node.path)).toBe(children);
+ });
+
+ it("adds children to cache on a map entry node", () => {
+ const cachedNodes = new Map();
+ const node = createNode({
+ name: "root",
+ contents: { value: gripEntryStubs.get("A → 0") },
+ });
+ const children = getChildren({
+ cachedNodes,
+ item: node,
+ });
+ expect(cachedNodes.get(node.path)).toBe(children);
+ });
+
+ it("adds children to cache on a proxy node having loaded props", () => {
+ const cachedNodes = new Map();
+ const node = createNode({
+ name: "root",
+ contents: { value: gripStubs.get("testProxy") },
+ });
+ const children = getChildren({
+ cachedNodes,
+ item: node,
+ loadedProperties: new Map([[node.path, gripStubs.get("testProxySlots")]]),
+ });
+ expect(cachedNodes.get(node.path)).toBe(children);
+ });
+
+ it("doesn't cache children on node with buckets and no loaded props", () => {
+ const cachedNodes = new Map();
+ const node = createNode({
+ name: "root",
+ contents: { value: gripArrayStubs.get("Array(234)") },
+ });
+ getChildren({
+ cachedNodes,
+ item: node,
+ });
+ expect(cachedNodes.has(node.path)).toBeFalsy();
+ });
+
+ it("caches children on a node with buckets having loaded props", () => {
+ const cachedNodes = new Map();
+ const node = createNode({
+ name: "root",
+ contents: { value: gripArrayStubs.get("Array(234)") },
+ });
+ const children = getChildren({
+ cachedNodes,
+ item: node,
+ loadedProperties: new Map([[node.path, { prototype: {} }]]),
+ });
+ expect(cachedNodes.get(node.path)).toBe(children);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/get-closest-grip-node.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/get-closest-grip-node.test.js
new file mode 100644
index 0000000000..9aa7e127a8
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/get-closest-grip-node.test.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ createNode,
+ getClosestGripNode,
+ makeNodesForEntries,
+ makeNumericalBuckets,
+} = require("devtools/client/shared/components/object-inspector/utils/node");
+
+const gripRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip`);
+const gripArrayRepStubs = require(`devtools/client/shared/components/test/node/stubs/reps/grip-array`);
+
+describe("getClosestGripNode", () => {
+ it("returns grip node itself", () => {
+ const stub = gripRepStubs.get("testMoreThanMaxProps");
+ const node = createNode({ name: "root", contents: { value: stub } });
+ expect(getClosestGripNode(node)).toBe(node);
+ });
+
+ it("returns the expected node for entries node", () => {
+ const mapStubNode = createNode({ name: "map", contents: { value: {} } });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(getClosestGripNode(entriesNode)).toBe(mapStubNode);
+ });
+
+ it("returns the expected node for bucket node", () => {
+ const grip = gripArrayRepStubs.get("testMaxProps");
+ const root = createNode({ name: "root", contents: { value: grip } });
+ const [bucket] = makeNumericalBuckets(root);
+ expect(getClosestGripNode(bucket)).toBe(root);
+ });
+
+ it("returns the expected node for sub-bucket node", () => {
+ const grip = gripArrayRepStubs.get("testMaxProps");
+ const root = createNode({ name: "root", contents: { value: grip } });
+ const [bucket] = makeNumericalBuckets(root);
+ const [subBucket] = makeNumericalBuckets(bucket);
+ expect(getClosestGripNode(subBucket)).toBe(root);
+ });
+
+ it("returns the expected node for deep sub-bucket node", () => {
+ const grip = gripArrayRepStubs.get("testMaxProps");
+ const root = createNode({ name: "root", contents: { value: grip } });
+ let [bucket] = makeNumericalBuckets(root);
+ for (let i = 0; i < 10; i++) {
+ bucket = makeNumericalBuckets({ ...bucket })[0];
+ }
+ expect(getClosestGripNode(bucket)).toBe(root);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/get-value.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/get-value.test.js
new file mode 100644
index 0000000000..29f0c0ffce
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/get-value.test.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const { getValue } = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+describe("getValue", () => {
+ it("get the value from contents.value", () => {
+ let item = {
+ contents: {
+ value: "my value",
+ },
+ };
+ expect(getValue(item)).toBe("my value");
+
+ item = {
+ contents: {
+ value: 0,
+ },
+ };
+ expect(getValue(item)).toBe(0);
+
+ item = {
+ contents: {
+ value: false,
+ },
+ };
+ expect(getValue(item)).toBe(false);
+
+ item = {
+ contents: {
+ value: null,
+ },
+ };
+ expect(getValue(item)).toBe(null);
+ });
+
+ it("get the value from contents.getterValue", () => {
+ let item = {
+ contents: {
+ getterValue: "my getter value",
+ },
+ };
+ expect(getValue(item)).toBe("my getter value");
+
+ item = {
+ contents: {
+ getterValue: 0,
+ },
+ };
+ expect(getValue(item)).toBe(0);
+
+ item = {
+ contents: {
+ getterValue: false,
+ },
+ };
+ expect(getValue(item)).toBe(false);
+
+ item = {
+ contents: {
+ getterValue: null,
+ },
+ };
+ expect(getValue(item)).toBe(null);
+ });
+
+ it("get the value from getter and setter", () => {
+ let item = {
+ contents: {
+ get: "get",
+ },
+ };
+ expect(getValue(item)).toEqual({ get: "get" });
+
+ item = {
+ contents: {
+ set: "set",
+ },
+ };
+ expect(getValue(item)).toEqual({ set: "set" });
+
+ item = {
+ contents: {
+ get: "get",
+ set: "set",
+ },
+ };
+ expect(getValue(item)).toEqual({ get: "get", set: "set" });
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/make-node-for-properties.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/make-node-for-properties.test.js
new file mode 100644
index 0000000000..da0a221531
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/make-node-for-properties.test.js
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ createNode,
+ makeNodesForProperties,
+ nodeIsDefaultProperties,
+ nodeIsEntries,
+ nodeIsMapEntry,
+ nodeIsPrototype,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+
+const root = {
+ path: "root",
+ contents: {
+ value: gripArrayStubs.get("testBasic"),
+ },
+};
+
+const objProperties = {
+ ownProperties: {
+ "0": {
+ value: {},
+ },
+ "2": {},
+ length: {
+ value: 3,
+ },
+ },
+ prototype: {
+ type: "object",
+ actor: "server2.conn1.child1/obj618",
+ class: "bla",
+ },
+};
+
+describe("makeNodesForProperties", () => {
+ it("kitchen sink", () => {
+ const nodes = makeNodesForProperties(objProperties, root);
+
+ const names = nodes.map(n => n.name);
+ expect(names).toEqual(["0", "length", "<prototype>"]);
+
+ const paths = nodes.map(n => n.path.toString());
+ expect(paths).toEqual(["rootâ—¦0", "rootâ—¦length", "rootâ—¦<prototype>"]);
+ });
+
+ it("includes getters and setters", () => {
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ foo: { value: "foo" },
+ bar: {
+ get: {
+ type: "object",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ baz: {
+ get: {
+ type: "undefined",
+ },
+ set: {
+ type: "object",
+ },
+ },
+ },
+ prototype: {
+ class: "bla",
+ },
+ },
+ root
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual([
+ "bar",
+ "baz",
+ "foo",
+ "<get bar()>",
+ "<set baz()>",
+ "<prototype>",
+ ]);
+
+ expect(paths).toEqual([
+ "rootâ—¦bar",
+ "rootâ—¦baz",
+ "rootâ—¦foo",
+ "rootâ—¦<get bar()>",
+ "rootâ—¦<set baz()>",
+ "rootâ—¦<prototype>",
+ ]);
+ });
+
+ it("does not include unrelevant properties", () => {
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ foo: undefined,
+ bar: null,
+ baz: {},
+ },
+ },
+ root
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path);
+
+ expect(names).toEqual([]);
+ expect(paths).toEqual([]);
+ });
+
+ it("sorts keys", () => {
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ bar: { value: {} },
+ 1: { value: {} },
+ 11: { value: {} },
+ 2: { value: {} },
+ _bar: { value: {} },
+ },
+ prototype: {
+ class: "bla",
+ },
+ },
+ root
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["1", "2", "11", "_bar", "bar", "<prototype>"]);
+ expect(paths).toEqual([
+ "rootâ—¦1",
+ "rootâ—¦2",
+ "rootâ—¦11",
+ "rootâ—¦_bar",
+ "rootâ—¦bar",
+ "rootâ—¦<prototype>",
+ ]);
+ });
+
+ it("prototype is included", () => {
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ bar: { value: {} },
+ },
+ prototype: { value: {}, class: "bla" },
+ },
+ root
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["bar", "<prototype>"]);
+ expect(paths).toEqual(["rootâ—¦bar", "rootâ—¦<prototype>"]);
+
+ expect(nodeIsPrototype(nodes[1])).toBe(true);
+ });
+
+ it("window object", () => {
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ bar: {
+ value: {},
+ get: { type: "function" },
+ set: { type: "function" },
+ },
+ location: { value: {} },
+ onload: {
+ get: { type: "function" },
+ set: { type: "function" },
+ },
+ },
+ class: "Window",
+ },
+ {
+ path: "root",
+ contents: { value: { class: "Window" } },
+ }
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path);
+
+ expect(names).toEqual([
+ "bar",
+ "<default properties>",
+ "<get bar()>",
+ "<set bar()>",
+ ]);
+ expect(paths).toEqual([
+ "rootâ—¦bar",
+ "rootâ—¦<default properties>",
+ "rootâ—¦<get bar()>",
+ "rootâ—¦<set bar()>",
+ ]);
+
+ const defaultPropertyNode = nodes[1];
+ expect(nodeIsDefaultProperties(defaultPropertyNode)).toBe(true);
+
+ const defaultPropNames = defaultPropertyNode.contents.map(n => n.name);
+ const defaultPropPath = defaultPropertyNode.contents.map(n => n.path);
+ expect(defaultPropNames).toEqual([
+ "location",
+ "onload",
+ "<get onload()>",
+ "<set onload()>",
+ ]);
+ expect(defaultPropPath).toEqual([
+ "rootâ—¦<default properties>â—¦location",
+ "rootâ—¦<default properties>â—¦onload",
+ "rootâ—¦<default properties>â—¦<get onload()>",
+ "rootâ—¦<default properties>â—¦<set onload()>",
+ ]);
+ });
+
+ it("object with entries", () => {
+ const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+
+ const mapNode = createNode({
+ name: "map",
+ path: "root",
+ contents: {
+ value: gripMapStubs.get("testSymbolKeyedMap"),
+ },
+ });
+
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ size: { value: 1 },
+ custom: { value: "customValue" },
+ },
+ },
+ mapNode
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["custom", "size", "<entries>"]);
+ expect(paths).toEqual(["rootâ—¦custom", "rootâ—¦size", "rootâ—¦<entries>"]);
+
+ const entriesNode = nodes[2];
+ expect(nodeIsEntries(entriesNode)).toBe(true);
+ });
+
+ it("quotes property names", () => {
+ const nodes = makeNodesForProperties(
+ {
+ ownProperties: {
+ // Numbers are ok.
+ 332217: { value: {} },
+ "needs-quotes": { value: {} },
+ unquoted: { value: {} },
+ "": { value: {} },
+ },
+ prototype: {
+ class: "WindowPrototype",
+ },
+ },
+ root
+ );
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual([
+ '""',
+ "332217",
+ '"needs-quotes"',
+ "unquoted",
+ "<prototype>",
+ ]);
+ expect(paths).toEqual([
+ 'rootâ—¦""',
+ "rootâ—¦332217",
+ 'rootâ—¦"needs-quotes"',
+ "rootâ—¦unquoted",
+ "rootâ—¦<prototype>",
+ ]);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/make-numerical-buckets.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/make-numerical-buckets.test.js
new file mode 100644
index 0000000000..02fb1a3bc5
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/make-numerical-buckets.test.js
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ createNode,
+ makeNumericalBuckets,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+
+describe("makeNumericalBuckets", () => {
+ it("handles simple numerical buckets", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ const nodes = makeNumericalBuckets(node);
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["[0…99]", "[100…199]", "[200…233]"]);
+
+ expect(paths).toEqual(["root◦[0…99]", "root◦[100…199]", "root◦[200…233]"]);
+ });
+
+ // TODO: Re-enable when we have support for lonely node.
+ it.skip("does not create a numerical bucket for a single node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(101)"),
+ },
+ });
+ const nodes = makeNumericalBuckets(node);
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["[0…99]", "100"]);
+
+ expect(paths).toEqual(["rootâ—¦bucket_0-99", "rootâ—¦100"]);
+ });
+
+ // TODO: Re-enable when we have support for lonely node.
+ it.skip("does create a numerical bucket for two node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ const nodes = makeNumericalBuckets(node);
+
+ const names = nodes.map(n => n.name);
+ const paths = nodes.map(n => n.path.toString());
+
+ expect(names).toEqual(["[0…99]", "[100…101]"]);
+
+ expect(paths).toEqual(["rootâ—¦bucket_0-99", "rootâ—¦bucket_100-101"]);
+ });
+
+ it("creates sub-buckets when needed", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(23456)"),
+ },
+ });
+ const nodes = makeNumericalBuckets(node);
+ const names = nodes.map(n => n.name);
+
+ expect(names).toEqual([
+ "[0…999]",
+ "[1000…1999]",
+ "[2000…2999]",
+ "[3000…3999]",
+ "[4000…4999]",
+ "[5000…5999]",
+ "[6000…6999]",
+ "[7000…7999]",
+ "[8000…8999]",
+ "[9000…9999]",
+ "[10000…10999]",
+ "[11000…11999]",
+ "[12000…12999]",
+ "[13000…13999]",
+ "[14000…14999]",
+ "[15000…15999]",
+ "[16000…16999]",
+ "[17000…17999]",
+ "[18000…18999]",
+ "[19000…19999]",
+ "[20000…20999]",
+ "[21000…21999]",
+ "[22000…22999]",
+ "[23000…23455]",
+ ]);
+
+ const firstBucketNodes = makeNumericalBuckets(nodes[0]);
+ const firstBucketNames = firstBucketNodes.map(n => n.name);
+ const firstBucketPaths = firstBucketNodes.map(n => n.path.toString());
+
+ expect(firstBucketNames).toEqual([
+ "[0…99]",
+ "[100…199]",
+ "[200…299]",
+ "[300…399]",
+ "[400…499]",
+ "[500…599]",
+ "[600…699]",
+ "[700…799]",
+ "[800…899]",
+ "[900…999]",
+ ]);
+ expect(firstBucketPaths[0]).toEqual("root◦[0…999]◦[0…99]");
+ expect(firstBucketPaths[firstBucketPaths.length - 1]).toEqual(
+ "root◦[0…999]◦[900…999]"
+ );
+
+ const lastBucketNodes = makeNumericalBuckets(nodes[nodes.length - 1]);
+ const lastBucketNames = lastBucketNodes.map(n => n.name);
+ const lastBucketPaths = lastBucketNodes.map(n => n.path.toString());
+ expect(lastBucketNames).toEqual([
+ "[23000…23099]",
+ "[23100…23199]",
+ "[23200…23299]",
+ "[23300…23399]",
+ "[23400…23455]",
+ ]);
+ expect(lastBucketPaths[0]).toEqual("root◦[23000…23455]◦[23000…23099]");
+ expect(lastBucketPaths[lastBucketPaths.length - 1]).toEqual(
+ "root◦[23000…23455]◦[23400…23455]"
+ );
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/node-has-entries.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/node-has-entries.test.js
new file mode 100644
index 0000000000..4a7aaa971d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/node-has-entries.test.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+
+const {
+ createNode,
+ nodeHasEntries,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+const createRootNode = value =>
+ createNode({ name: "root", contents: { value } });
+describe("nodeHasEntries", () => {
+ it("returns true for Maps", () => {
+ expect(
+ nodeHasEntries(createRootNode(gripMapStubs.get("testSymbolKeyedMap")))
+ ).toBe(true);
+ });
+
+ it("returns true for WeakMaps", () => {
+ expect(
+ nodeHasEntries(createRootNode(gripMapStubs.get("testWeakMap")))
+ ).toBe(true);
+ });
+
+ it("returns true for Sets", () => {
+ expect(
+ nodeHasEntries(createRootNode(gripArrayStubs.get("new Set([1,2,3,4])")))
+ ).toBe(true);
+ });
+
+ it("returns true for WeakSets", () => {
+ expect(
+ nodeHasEntries(
+ createRootNode(
+ gripArrayStubs.get(
+ "new WeakSet(document.querySelectorAll('div, button'))"
+ )
+ )
+ )
+ ).toBe(true);
+ });
+
+ it("returns false for Arrays", () => {
+ expect(
+ nodeHasEntries(createRootNode(gripMapStubs.get("testMaxProps")))
+ ).toBe(false);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/node-is-window.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/node-is-window.test.js
new file mode 100644
index 0000000000..8fe920ed17
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/node-is-window.test.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const gripWindowStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+
+const {
+ createNode,
+ nodeIsWindow,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+const createRootNode = value =>
+ createNode({ name: "root", contents: { value } });
+describe("nodeIsWindow", () => {
+ it("returns true for Window", () => {
+ expect(
+ nodeIsWindow(createRootNode(gripWindowStubs.get("Window")._grip))
+ ).toBe(true);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/node-supports-numerical-bucketing.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/node-supports-numerical-bucketing.test.js
new file mode 100644
index 0000000000..51199146e0
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/node-supports-numerical-bucketing.test.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ createNode,
+ makeNodesForEntries,
+ nodeSupportsNumericalBucketing,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+const createRootNode = stub =>
+ createNode({
+ name: "root",
+ contents: { value: stub },
+ });
+
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+
+describe("nodeSupportsNumericalBucketing", () => {
+ it("returns true for Arrays", () => {
+ expect(
+ nodeSupportsNumericalBucketing(
+ createRootNode(gripArrayStubs.get("testBasic"))
+ )
+ ).toBe(true);
+ });
+
+ it("returns true for NodeMap", () => {
+ expect(
+ nodeSupportsNumericalBucketing(
+ createRootNode(gripArrayStubs.get("testNamedNodeMap"))
+ )
+ ).toBe(true);
+ });
+
+ it("returns true for NodeList", () => {
+ expect(
+ nodeSupportsNumericalBucketing(
+ createRootNode(gripArrayStubs.get("testNodeList"))
+ )
+ ).toBe(true);
+ });
+
+ it("returns true for DocumentFragment", () => {
+ expect(
+ nodeSupportsNumericalBucketing(
+ createRootNode(gripArrayStubs.get("testDocumentFragment"))
+ )
+ ).toBe(true);
+ });
+
+ it("returns true for <entries> node", () => {
+ expect(
+ nodeSupportsNumericalBucketing(
+ makeNodesForEntries(
+ createRootNode(gripMapStubs.get("testSymbolKeyedMap"))
+ )
+ )
+ ).toBe(true);
+ });
+
+ it("returns true for buckets node", () => {
+ expect(
+ nodeSupportsNumericalBucketing(
+ makeNodesForEntries(
+ createRootNode(gripMapStubs.get("testSymbolKeyedMap"))
+ )
+ )
+ ).toBe(true);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/promises.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/promises.test.js
new file mode 100644
index 0000000000..229f717b56
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/promises.test.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const {
+ makeNodesForPromiseProperties,
+ nodeIsPromise,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+describe("promises utils function", () => {
+ it("is promise", () => {
+ const promise = {
+ contents: {
+ enumerable: true,
+ configurable: false,
+ value: {
+ actor: "server2.conn2.child1/obj36",
+ promiseState: {
+ state: "rejected",
+ reason: {
+ type: "undefined",
+ },
+ },
+ class: "Promise",
+ type: "object",
+ },
+ },
+ };
+
+ expect(nodeIsPromise(promise)).toEqual(true);
+ });
+
+ it("makeNodesForPromiseProperties", () => {
+ const item = {
+ path: "root",
+ contents: {
+ value: {
+ actor: "server2.conn2.child1/obj36",
+ class: "Promise",
+ type: "object",
+ },
+ },
+ };
+ const promiseState = {
+ state: "rejected",
+ reason: {
+ type: "3",
+ },
+ };
+
+ const properties = makeNodesForPromiseProperties({promiseState}, item);
+ expect(properties).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-entries.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-entries.test.js
new file mode 100644
index 0000000000..e4672f2a92
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-entries.test.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const { createNode, getChildren, makeNodesForEntries } = Utils.node;
+
+const { shouldLoadItemEntries } = Utils.loadProperties;
+
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+
+describe("shouldLoadItemEntries", () => {
+ it("returns true for an entries node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemEntries(entriesNode)).toBeTruthy();
+ });
+
+ it("returns false for an already loaded entries node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ const loadedProperties = new Map([[entriesNode.path, true]]);
+ expect(shouldLoadItemEntries(entriesNode, loadedProperties)).toBeFalsy();
+ });
+
+ it("returns true for entries on a Map with everything in preview", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("testSymbolKeyedMap"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemEntries(entriesNode)).toBeTruthy();
+ });
+
+ it("returns true for entries on a Set with everything in preview", () => {
+ const setStubNode = createNode({
+ name: "set",
+ contents: {
+ value: gripArrayStubs.get("new Set([1,2,3,4])"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(setStubNode);
+ expect(shouldLoadItemEntries(entriesNode)).toBeTruthy();
+ });
+
+ it("returns false for a Set node", () => {
+ const setStubNode = createNode({
+ name: "set",
+ contents: {
+ value: gripArrayStubs.get("new Set([1,2,3,4])"),
+ },
+ });
+ expect(shouldLoadItemEntries(setStubNode)).toBeFalsy();
+ });
+
+ it("returns false for a Map node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ expect(shouldLoadItemEntries(mapStubNode)).toBeFalsy();
+ });
+
+ it("returns false for an array", () => {
+ const node = createNode({
+ name: "array",
+ contents: {
+ value: gripMapStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemEntries(node)).toBeFalsy();
+ });
+
+ it("returns false for an object", () => {
+ const node = createNode({
+ name: "array",
+ contents: {
+ value: gripStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemEntries(node)).toBeFalsy();
+ });
+
+ it("returns false for an entries node with buckets", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("234-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemEntries(entriesNode)).toBeFalsy();
+ });
+
+ it("returns true for an entries bucket node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("234-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ const bucketNodes = getChildren({
+ item: entriesNode,
+ loadedProperties: new Map([[entriesNode.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemEntries(bucketNodes[0])).toBeTruthy();
+ });
+
+ it("returns false for an entries bucket node with sub-buckets", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("23456-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ const bucketNodes = getChildren({
+ item: entriesNode,
+ loadedProperties: new Map([[entriesNode.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…999]");
+ expect(shouldLoadItemEntries(bucketNodes[0])).toBeFalsy();
+ });
+
+ it("returns true for an entries sub-bucket node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("23456-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ const bucketNodes = getChildren({
+ item: entriesNode,
+ loadedProperties: new Map([[entriesNode.path, true]]),
+ });
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…999]");
+
+ // Get the sub-buckets
+ const subBucketNodes = getChildren({
+ item: bucketNodes[0],
+ loadedProperties: new Map([[bucketNodes[0].path, true]]),
+ });
+ // Make sure we do have a bucket.
+ expect(subBucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemEntries(subBucketNodes[0])).toBeTruthy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-full-text.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-full-text.test.js
new file mode 100644
index 0000000000..9e696a028c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-full-text.test.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const { createNode } = Utils.node;
+const { shouldLoadItemFullText } = Utils.loadProperties;
+
+const longStringStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/long-string.js");
+const symbolStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/symbol.js");
+
+describe("shouldLoadItemFullText", () => {
+ it("returns true for a longString node with unloaded full text", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: longStringStubs.get("testUnloadedFullText"),
+ },
+ });
+ expect(shouldLoadItemFullText(node)).toBeTruthy();
+ });
+
+ it("returns false for a longString node with loaded full text", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: longStringStubs.get("testLoadedFullText"),
+ },
+ });
+ const loadedProperties = new Map([[node.path, true]]);
+ expect(shouldLoadItemFullText(node, loadedProperties)).toBeFalsy();
+ });
+
+ it("returns false for non longString primitive nodes", () => {
+ const values = [
+ "primitive string",
+ 1,
+ -1,
+ 0,
+ true,
+ false,
+ null,
+ undefined,
+ symbolStubs.get("Symbol"),
+ ];
+
+ const nodes = values.map((value, i) =>
+ createNode({
+ name: `root${i}`,
+ contents: { value },
+ })
+ );
+
+ nodes.forEach(node => expect(shouldLoadItemFullText(node)).toBeFalsy());
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-indexed-properties.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-indexed-properties.test.js
new file mode 100644
index 0000000000..63e505b947
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-indexed-properties.test.js
@@ -0,0 +1,259 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const {
+ createNode,
+ createGetterNode,
+ createSetterNode,
+ getChildren,
+ makeNodesForEntries,
+ nodeIsDefaultProperties,
+} = Utils.node;
+
+const { shouldLoadItemIndexedProperties } = Utils.loadProperties;
+
+const {
+ createGripMapEntry,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const accessorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const windowStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+
+describe("shouldLoadItemIndexedProperties", () => {
+ it("returns true for an array", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns false for an already loaded item", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ const loadedProperties = new Map([[node.path, true]]);
+ expect(shouldLoadItemIndexedProperties(node, loadedProperties)).toBeFalsy();
+ });
+
+ it("returns false for an array node with buckets", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeFalsy();
+ });
+
+ it("returns true for an array bucket node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ const bucketNodes = getChildren({
+ item: node,
+ loadedProperties: new Map([[node.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemIndexedProperties(bucketNodes[0])).toBeTruthy();
+ });
+
+ it("returns false for an array bucket node with sub-buckets", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(23456)"),
+ },
+ });
+ const bucketNodes = getChildren({
+ item: node,
+ loadedProperties: new Map([[node.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…999]");
+ expect(shouldLoadItemIndexedProperties(bucketNodes[0])).toBeFalsy();
+ });
+
+ it("returns true for an array sub-bucket node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(23456)"),
+ },
+ });
+ const bucketNodes = getChildren({
+ item: node,
+ loadedProperties: new Map([[node.path, true]]),
+ });
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…999]");
+
+ // Get the sub-buckets
+ const subBucketNodes = getChildren({
+ item: bucketNodes[0],
+ loadedProperties: new Map([[bucketNodes[0].path, true]]),
+ });
+ // Make sure we do have a bucket.
+ expect(subBucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemIndexedProperties(subBucketNodes[0])).toBeTruthy();
+ });
+
+ it("returns false for an entries node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemIndexedProperties(entriesNode)).toBeFalsy();
+ });
+
+ it("returns true for an Object", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns true for a Map", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns true for a Set", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("new Set([1,2,3,4])"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns true for a Window", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns false for a <default properties> node", () => {
+ const windowNode = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ const loadedProperties = new Map([
+ [
+ windowNode.path,
+ {
+ ownProperties: {
+ foo: { value: "bar" },
+ location: { value: "a" },
+ },
+ },
+ ],
+ ]);
+ const [, defaultPropertiesNode] = getChildren({
+ item: windowNode,
+ loadedProperties,
+ });
+ expect(nodeIsDefaultProperties(defaultPropertiesNode)).toBe(true);
+ expect(shouldLoadItemIndexedProperties(defaultPropertiesNode)).toBeFalsy();
+ });
+
+ it("returns false for a MapEntry node", () => {
+ const node = createGripMapEntry("key", "value");
+ expect(shouldLoadItemIndexedProperties(node)).toBeFalsy();
+ });
+
+ it("returns false for a Proxy node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeFalsy();
+ });
+
+ it("returns true for a Proxy target node", () => {
+ const proxyNode = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ const loadedProperties = new Map([
+ [proxyNode.path, gripStubs.get("testProxySlots")],
+ ]);
+ const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
+ // Make sure we have the target node.
+ expect(targetNode.name).toBe("<target>");
+ expect(shouldLoadItemIndexedProperties(targetNode)).toBeTruthy();
+ });
+
+ it("returns false for an accessor node", () => {
+ const accessorNode = createNode({
+ name: "root",
+ contents: {
+ value: accessorStubs.get("getter"),
+ },
+ });
+ expect(shouldLoadItemIndexedProperties(accessorNode)).toBeFalsy();
+ });
+
+ it("returns true for an accessor <get> node", () => {
+ const getNode = createGetterNode({
+ name: "root",
+ property: accessorStubs.get("getter"),
+ });
+ expect(getNode.name).toBe("<get root()>");
+ expect(shouldLoadItemIndexedProperties(getNode)).toBeTruthy();
+ });
+
+ it("returns true for an accessor <set> node", () => {
+ const setNode = createSetterNode({
+ name: "root",
+ property: accessorStubs.get("setter"),
+ });
+ expect(setNode.name).toBe("<set root()>");
+ expect(shouldLoadItemIndexedProperties(setNode)).toBeTruthy();
+ });
+
+ it("returns false for a primitive node", () => {
+ const node = createNode({
+ name: "root",
+ contents: { value: 42 },
+ });
+ expect(shouldLoadItemIndexedProperties(node)).toBeFalsy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-non-indexed-properties.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-non-indexed-properties.test.js
new file mode 100644
index 0000000000..425540eee2
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-non-indexed-properties.test.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const {
+ createNode,
+ createGetterNode,
+ createSetterNode,
+ getChildren,
+ makeNodesForEntries,
+ nodeIsDefaultProperties,
+} = Utils.node;
+
+const { shouldLoadItemNonIndexedProperties } = Utils.loadProperties;
+
+const {
+ createGripMapEntry,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const accessorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const windowStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+
+describe("shouldLoadItemNonIndexedProperties", () => {
+ it("returns true for an array", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns false for an already loaded item", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ const loadedProperties = new Map([[node.path, true]]);
+ expect(
+ shouldLoadItemNonIndexedProperties(node, loadedProperties)
+ ).toBeFalsy();
+ });
+
+ it("returns true for an array node with buckets", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns false for an array bucket node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ const bucketNodes = getChildren({
+ item: node,
+ loadedProperties: new Map([[node.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemNonIndexedProperties(bucketNodes[0])).toBeFalsy();
+ });
+
+ it("returns false for an entries node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemNonIndexedProperties(entriesNode)).toBeFalsy();
+ });
+
+ it("returns true for an Object", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns true for a Map", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns true for a Set", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("new Set([1,2,3,4])"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns true for a Window", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeTruthy();
+ });
+
+ it("returns false for a <default properties> node", () => {
+ const windowNode = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ const loadedProperties = new Map([
+ [
+ windowNode.path,
+ {
+ ownProperties: {
+ foo: { value: "bar" },
+ location: { value: "a" },
+ },
+ },
+ ],
+ ]);
+ const [, defaultPropertiesNode] = getChildren({
+ item: windowNode,
+ loadedProperties,
+ });
+ expect(nodeIsDefaultProperties(defaultPropertiesNode)).toBe(true);
+ expect(
+ shouldLoadItemNonIndexedProperties(defaultPropertiesNode)
+ ).toBeFalsy();
+ });
+
+ it("returns false for a MapEntry node", () => {
+ const node = createGripMapEntry("key", "value");
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeFalsy();
+ });
+
+ it("returns false for a Proxy node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeFalsy();
+ });
+
+ it("returns true for a Proxy target node", () => {
+ const proxyNode = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ const loadedProperties = new Map([
+ [proxyNode.path, gripStubs.get("testProxySlots")],
+ ]);
+ const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
+ // Make sure we have the target node.
+ expect(targetNode.name).toBe("<target>");
+ expect(shouldLoadItemNonIndexedProperties(targetNode)).toBeTruthy();
+ });
+
+ it("returns false for an accessor node", () => {
+ const accessorNode = createNode({
+ name: "root",
+ contents: {
+ value: accessorStubs.get("getter"),
+ },
+ });
+ expect(shouldLoadItemNonIndexedProperties(accessorNode)).toBeFalsy();
+ });
+
+ it("returns true for an accessor <get> node", () => {
+ const getNode = createGetterNode({
+ name: "root",
+ property: accessorStubs.get("getter"),
+ });
+ expect(getNode.name).toBe("<get root()>");
+ expect(shouldLoadItemNonIndexedProperties(getNode)).toBeTruthy();
+ });
+
+ it("returns true for an accessor <set> node", () => {
+ const setNode = createSetterNode({
+ name: "root",
+ property: accessorStubs.get("setter"),
+ });
+ expect(setNode.name).toBe("<set root()>");
+ expect(shouldLoadItemNonIndexedProperties(setNode)).toBeTruthy();
+ });
+
+ it("returns false for a primitive node", () => {
+ const node = createNode({
+ name: "root",
+ contents: { value: 42 },
+ });
+ expect(shouldLoadItemNonIndexedProperties(node)).toBeFalsy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-prototype.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-prototype.test.js
new file mode 100644
index 0000000000..83d45df70c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-prototype.test.js
@@ -0,0 +1,218 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const {
+ createNode,
+ createGetterNode,
+ createSetterNode,
+ getChildren,
+ makeNodesForEntries,
+ nodeIsDefaultProperties,
+} = Utils.node;
+
+const { shouldLoadItemPrototype } = Utils.loadProperties;
+
+const {
+ createGripMapEntry,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const accessorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const windowStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+
+describe("shouldLoadItemPrototype", () => {
+ it("returns true for an array", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeTruthy();
+ });
+
+ it("returns false for an already loaded item", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ const loadedProperties = new Map([[node.path, true]]);
+ expect(shouldLoadItemPrototype(node, loadedProperties)).toBeFalsy();
+ });
+
+ it("returns true for an array node with buckets", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeTruthy();
+ });
+
+ it("returns false for an array bucket node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ const bucketNodes = getChildren({
+ item: node,
+ loadedProperties: new Map([[node.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemPrototype(bucketNodes[0])).toBeFalsy();
+ });
+
+ it("returns false for an entries node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemPrototype(entriesNode)).toBeFalsy();
+ });
+
+ it("returns true for an Object", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeTruthy();
+ });
+
+ it("returns true for a Map", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeTruthy();
+ });
+
+ it("returns true for a Set", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("new Set([1,2,3,4])"),
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeTruthy();
+ });
+
+ it("returns true for a Window", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeTruthy();
+ });
+
+ it("returns false for a <default properties> node", () => {
+ const windowNode = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ const loadedProperties = new Map([
+ [
+ windowNode.path,
+ {
+ ownProperties: {
+ foo: { value: "bar" },
+ location: { value: "a" },
+ },
+ },
+ ],
+ ]);
+ const [, defaultPropertiesNode] = getChildren({
+ item: windowNode,
+ loadedProperties,
+ });
+ expect(nodeIsDefaultProperties(defaultPropertiesNode)).toBe(true);
+ expect(shouldLoadItemPrototype(defaultPropertiesNode)).toBeFalsy();
+ });
+
+ it("returns false for a MapEntry node", () => {
+ const node = createGripMapEntry("key", "value");
+ expect(shouldLoadItemPrototype(node)).toBeFalsy();
+ });
+
+ it("returns false for a Proxy node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeFalsy();
+ });
+
+ it("returns true for a Proxy target node", () => {
+ const proxyNode = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ const loadedProperties = new Map([
+ [proxyNode.path, gripStubs.get("testProxySlots")],
+ ]);
+ const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
+ // Make sure we have the target node.
+ expect(targetNode.name).toBe("<target>");
+ expect(shouldLoadItemPrototype(targetNode)).toBeTruthy();
+ });
+
+ it("returns false for an accessor node", () => {
+ const accessorNode = createNode({
+ name: "root",
+ contents: {
+ value: accessorStubs.get("getter"),
+ },
+ });
+ expect(shouldLoadItemPrototype(accessorNode)).toBeFalsy();
+ });
+
+ it("returns true for an accessor <get> node", () => {
+ const getNode = createGetterNode({
+ name: "root",
+ property: accessorStubs.get("getter"),
+ });
+ expect(getNode.name).toBe("<get root()>");
+ expect(shouldLoadItemPrototype(getNode)).toBeTruthy();
+ });
+
+ it("returns true for an accessor <set> node", () => {
+ const setNode = createSetterNode({
+ name: "root",
+ property: accessorStubs.get("setter"),
+ });
+ expect(setNode.name).toBe("<set root()>");
+ expect(shouldLoadItemPrototype(setNode)).toBeTruthy();
+ });
+
+ it("returns false for a primitive node", () => {
+ const node = createNode({
+ name: "root",
+ contents: { value: 42 },
+ });
+ expect(shouldLoadItemPrototype(node)).toBeFalsy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-symbols.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-symbols.test.js
new file mode 100644
index 0000000000..a937b9fcab
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-load-item-symbols.test.js
@@ -0,0 +1,218 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const {
+ createNode,
+ createGetterNode,
+ createSetterNode,
+ getChildren,
+ makeNodesForEntries,
+ nodeIsDefaultProperties,
+} = Utils.node;
+
+const { shouldLoadItemSymbols } = Utils.loadProperties;
+
+const {
+ createGripMapEntry,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const accessorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+const gripMapStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const windowStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+
+describe("shouldLoadItemSymbols", () => {
+ it("returns true for an array", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeTruthy();
+ });
+
+ it("returns false for an already loaded item", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("testMaxProps"),
+ },
+ });
+ const loadedProperties = new Map([[node.path, true]]);
+ expect(shouldLoadItemSymbols(node, loadedProperties)).toBeFalsy();
+ });
+
+ it("returns true for an array node with buckets", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeTruthy();
+ });
+
+ it("returns false for an array bucket node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("Array(234)"),
+ },
+ });
+ const bucketNodes = getChildren({
+ item: node,
+ loadedProperties: new Map([[node.path, true]]),
+ });
+
+ // Make sure we do have a bucket.
+ expect(bucketNodes[0].name).toBe("[0…99]");
+ expect(shouldLoadItemSymbols(bucketNodes[0])).toBeFalsy();
+ });
+
+ it("returns false for an entries node", () => {
+ const mapStubNode = createNode({
+ name: "map",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ const entriesNode = makeNodesForEntries(mapStubNode);
+ expect(shouldLoadItemSymbols(entriesNode)).toBeFalsy();
+ });
+
+ it("returns true for an Object", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testMaxProps"),
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeTruthy();
+ });
+
+ it("returns true for a Map", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripMapStubs.get("20-entries Map"),
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeTruthy();
+ });
+
+ it("returns true for a Set", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripArrayStubs.get("new Set([1,2,3,4])"),
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeTruthy();
+ });
+
+ it("returns true for a Window", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeTruthy();
+ });
+
+ it("returns false for a <default properties> node", () => {
+ const windowNode = createNode({
+ name: "root",
+ contents: {
+ value: windowStubs.get("Window")._grip,
+ },
+ });
+ const loadedProperties = new Map([
+ [
+ windowNode.path,
+ {
+ ownProperties: {
+ foo: { value: "bar" },
+ location: { value: "a" },
+ },
+ },
+ ],
+ ]);
+ const [, defaultPropertiesNode] = getChildren({
+ item: windowNode,
+ loadedProperties,
+ });
+ expect(nodeIsDefaultProperties(defaultPropertiesNode)).toBe(true);
+ expect(shouldLoadItemSymbols(defaultPropertiesNode)).toBeFalsy();
+ });
+
+ it("returns false for a MapEntry node", () => {
+ const node = createGripMapEntry("key", "value");
+ expect(shouldLoadItemSymbols(node)).toBeFalsy();
+ });
+
+ it("returns false for a Proxy node", () => {
+ const node = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeFalsy();
+ });
+
+ it("returns true for a Proxy target node", () => {
+ const proxyNode = createNode({
+ name: "root",
+ contents: {
+ value: gripStubs.get("testProxy"),
+ },
+ });
+ const loadedProperties = new Map([
+ [proxyNode.path, gripStubs.get("testProxySlots")],
+ ]);
+ const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
+ // Make sure we have the target node.
+ expect(targetNode.name).toBe("<target>");
+ expect(shouldLoadItemSymbols(targetNode)).toBeTruthy();
+ });
+
+ it("returns false for an accessor node", () => {
+ const accessorNode = createNode({
+ name: "root",
+ contents: {
+ value: accessorStubs.get("getter"),
+ },
+ });
+ expect(shouldLoadItemSymbols(accessorNode)).toBeFalsy();
+ });
+
+ it("returns true for an accessor <get> node", () => {
+ const getNode = createGetterNode({
+ name: "root",
+ property: accessorStubs.get("getter"),
+ });
+ expect(getNode.name).toBe("<get root()>");
+ expect(shouldLoadItemSymbols(getNode)).toBeTruthy();
+ });
+
+ it("returns true for an accessor <set> node", () => {
+ const setNode = createSetterNode({
+ name: "root",
+ property: accessorStubs.get("setter"),
+ });
+ expect(setNode.name).toBe("<set root()>");
+ expect(shouldLoadItemSymbols(setNode)).toBeTruthy();
+ });
+
+ it("returns false for a primitive node", () => {
+ const node = createNode({
+ name: "root",
+ contents: { value: 42 },
+ });
+ expect(shouldLoadItemSymbols(node)).toBeFalsy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/object-inspector/utils/should-render-roots-in-reps.test.js b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-render-roots-in-reps.test.js
new file mode 100644
index 0000000000..456326545a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/object-inspector/utils/should-render-roots-in-reps.test.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const { shouldRenderRootsInReps } = Utils;
+
+const nullStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/null.js");
+const numberStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/number.js");
+const undefinedStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/undefined.js");
+const gripStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const symbolStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/symbol.js");
+const errorStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/error.js");
+const bigIntStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/big-int.js");
+
+describe("shouldRenderRootsInReps", () => {
+ it("returns true for a string", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: "Hello" },
+ },
+ ])
+ ).toBeTruthy();
+ });
+
+ it("returns true for an integer", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: numberStubs.get("Int") },
+ },
+ ])
+ ).toBeTruthy();
+ });
+
+ it("returns false for empty roots", () => {
+ expect(shouldRenderRootsInReps([])).toBeFalsy();
+ });
+
+ it("returns true for a big int", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: bigIntStubs.get("1n") },
+ },
+ ])
+ ).toBeTruthy();
+ });
+
+ it("returns true for undefined", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: undefinedStubs.get("Undefined") },
+ },
+ ])
+ ).toBeTruthy();
+ });
+
+ it("returns true for null", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: nullStubs.get("Null") },
+ },
+ ])
+ ).toBeTruthy();
+ });
+
+ it("returns true for Symbols", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: symbolStubs.get("Symbol") },
+ },
+ ])
+ ).toBeTruthy();
+ });
+
+ it("returns true for Errors when customFormat prop is true", () => {
+ expect(
+ shouldRenderRootsInReps(
+ [
+ {
+ contents: { value: errorStubs.get("MultilineStackError") },
+ },
+ ],
+ { customFormat: true }
+ )
+ ).toBeTruthy();
+ });
+
+ it("returns false for Errors when customFormat prop is false", () => {
+ expect(
+ shouldRenderRootsInReps(
+ [
+ {
+ contents: { value: errorStubs.get("MultilineStackError") },
+ },
+ ],
+ { customFormat: false }
+ )
+ ).toBeFalsy();
+ });
+
+ it("returns false when there are multiple primitive roots", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ contents: { value: "Hello" },
+ },
+ {
+ contents: { value: 42 },
+ },
+ ])
+ ).toBeFalsy();
+ });
+
+ it("returns false for primitive when the root specifies a name", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ name: "label",
+ contents: { value: 42 },
+ },
+ ])
+ ).toBeFalsy();
+ });
+
+ it("returns false for Grips", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ name: "label",
+ contents: { value: gripStubs.get("testMaxProps") },
+ },
+ ])
+ ).toBeFalsy();
+ });
+
+ it("returns false for Arrays", () => {
+ expect(
+ shouldRenderRootsInReps([
+ {
+ name: "label",
+ contents: { value: gripArrayStubs.get("testMaxProps") },
+ },
+ ])
+ ).toBeFalsy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/__snapshots__/accessor.test.js.snap b/devtools/client/shared/components/test/node/components/reps/__snapshots__/accessor.test.js.snap
new file mode 100644
index 0000000000..d8e298956a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/__snapshots__/accessor.test.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Accessor - Invoke getter does not render an icon when the object has an evaluation 1`] = `"\\"hello\\""`;
diff --git a/devtools/client/shared/components/test/node/components/reps/__snapshots__/element-node.test.js.snap b/devtools/client/shared/components/test/node/components/reps/__snapshots__/element-node.test.js.snap
new file mode 100644
index 0000000000..077bc6cc71
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/__snapshots__/element-node.test.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ElementNode - Node with spaces in the class name renders with expected text content 1`] = `
+<span
+ className="objectBox objectBox-node"
+ data-link-actor-id="server1.conn3.child1/obj59"
+>
+ <span
+ className="angleBracket"
+ >
+ &lt;
+ </span>
+ <span
+ className="tag-name"
+ >
+ body
+ </span>
+
+ <span>
+ <span
+ className="attrName"
+ >
+ class
+ </span>
+ <span
+ className="attrEqual"
+ >
+ =
+ </span>
+ <span
+ className="objectBox objectBox-string attrValue"
+ >
+ "a b c"
+ </span>
+ </span>
+ <span
+ className="angleBracket"
+ >
+ &gt;
+ </span>
+</span>
+`;
diff --git a/devtools/client/shared/components/test/node/components/reps/__snapshots__/error.test.js.snap b/devtools/client/shared/components/test/node/components/reps/__snapshots__/error.test.js.snap
new file mode 100644
index 0000000000..8cf04ea9c9
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/__snapshots__/error.test.js.snap
@@ -0,0 +1,1210 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Error - Error with V8-like stack renders with expected text 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1020"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ BOOM
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ getAccount
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ http://moz.com/script.js:1:2
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Error with invalid stack renders with expected text 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1020"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ bad stack
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ />
+</span>
+`;
+
+exports[`Error - Error with stack having frames with multiple @ renders with expected text for Error object 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1021"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ bar
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ errorBar
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ https://example.com/turbo/from-npm.js@0.8.26/dist/from-npm.js:814:31
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn1"
+ >
+ errorFoo
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location1"
+ >
+ https://example.com/turbo/from-npm.js@0.8.26/dist/from-npm.js:815:31
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn2"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location2"
+ >
+ https://example.com/turbo/from-npm.js@0.8.26/dist/from-npm.js:816:31
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Error with undefined-grip message renders with expected text 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server0.conn0.child1/obj88"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-undefined"
+ title={null}
+ >
+ undefined
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:16:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Error with undefined-grip message renders with expected text 2`] = `
+<span
+ className="objectBox-stackTrace "
+ data-link-actor-id="server0.conn0.child1/obj88"
+ title={null}
+>
+ <span
+ className="objectTitle"
+ key="title"
+ >
+ Error
+ </span>
+</span>
+`;
+
+exports[`Error - Error with undefined-grip name renders with expected text 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server0.conn0.child1/obj88"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ too much recursion
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:16:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Error with undefined-grip name renders with expected text 2`] = `
+<span
+ className="objectBox-stackTrace "
+ data-link-actor-id="server0.conn0.child1/obj88"
+ title={null}
+>
+ <span
+ className="objectTitle"
+ key="title"
+ >
+ Error
+ </span>
+</span>
+`;
+
+exports[`Error - Error with undefined-grip stack renders with expected text 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server0.conn0.child1/obj88"
+ title={null}
+>
+ InternalError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ too much recursion
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ />
+</span>
+`;
+
+exports[`Error - Eval error renders with expected text for an EvalError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1022"
+ title={null}
+>
+ EvalError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ EvalError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:10:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Internal error renders with expected text for an InternalError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1023"
+ title={null}
+>
+ InternalError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ InternalError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:11:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Multi line stack error renders with expected text for Error object 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1021"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ bar
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ errorBar
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:6:15
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn1"
+ >
+ errorFoo
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location1"
+ >
+ debugger eval code:3:3
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn2"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location2"
+ >
+ debugger eval code:8:1
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Range error renders with expected text for RangeError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1024"
+ title={null}
+>
+ RangeError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ RangeError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:12:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Reference error renders with expected text for ReferenceError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1025"
+ title={null}
+>
+ ReferenceError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ ReferenceError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:13:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Simple error renders with error type and preview message when in short mode 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1021"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ bar
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ errorBar
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:6:15
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn1"
+ >
+ errorFoo
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location1"
+ >
+ debugger eval code:3:3
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn2"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location2"
+ >
+ debugger eval code:8:1
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Simple error renders with error type only when customFormat prop isn't set 1`] = `
+<span
+ className="objectBox-stackTrace "
+ data-link-actor-id="server1.conn1.child1/obj1021"
+ title={null}
+>
+ <span
+ className="objectTitle"
+ key="title"
+ >
+ Error:
+ </span>
+ <span
+ className="objectBox objectBox-string"
+ >
+ bar
+ </span>
+</span>
+`;
+
+exports[`Error - Simple error renders with error type only when depth is > 0 1`] = `
+<span
+ className="objectBox-stackTrace "
+ data-link-actor-id="server1.conn1.child1/obj1021"
+ title={null}
+>
+ <span
+ className="objectTitle"
+ key="title"
+ >
+ Error:
+ </span>
+ <span
+ className="objectBox objectBox-string"
+ >
+ bar
+ </span>
+</span>
+`;
+
+exports[`Error - Simple error renders with expected text for simple error 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1020"
+ title="Error: \\"Error message\\""
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ title="Error message"
+ >
+ Error message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:1:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Syntax error renders with expected text for SyntaxError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1026"
+ title={null}
+>
+ SyntaxError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ SyntaxError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:14:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - Type error renders with expected text for TypeError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1027"
+ title={null}
+>
+ TypeError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ TypeError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:15:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - URI error renders with expected text for URIError 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1028"
+ title={null}
+>
+ URIError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ URIError message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ &lt;anonymous&gt;
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ debugger eval code:16:13
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - base-loader.sys.mjs renders as expected in tiny mode 1`] = `
+<span
+ className="objectBox-stackTrace "
+ data-link-actor-id="server1.conn1.child1/obj1020"
+ title={null}
+>
+ <span
+ className="objectTitle"
+ key="title"
+ >
+ Error
+ </span>
+</span>
+`;
+
+exports[`Error - base-loader.sys.mjs renders as expected without mode 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1020"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ Error message
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ onPacket
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ resource://devtools/client/debugger-client.js:856:9
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn1"
+ >
+ send
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location1"
+ >
+ resource://devtools/shared/transport/transport.js:569:13
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn2"
+ >
+ makeInfallible
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location2"
+ >
+ resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn3"
+ >
+ makeInfallible
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location3"
+ >
+ resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - longString stacktrace - cut-off location renders as expected 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj33"
+ title={null}
+>
+ InternalError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ too much recursion
+ </span>
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:32:1
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn1"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location1"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn2"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location2"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn3"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location3"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn4"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location4"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn5"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location5"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn6"
+ >
+ doStuff
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location6"
+ >
+ https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - longString stacktrace renders as expected 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn2.child1/obj33"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ />
+ <span
+ className="objectBox-stackTrace-grid"
+ key="stack"
+ >
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn0"
+ >
+ ngOnChanges
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location0"
+ >
+ webpack-internal:///./node_modules/@angular/common/esm5/common.js:2656:27
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn1"
+ >
+ checkAndUpdateDirectiveInline
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location1"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:12581:9
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn2"
+ >
+ checkAndUpdateNodeInline
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location2"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14109:20
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn3"
+ >
+ checkAndUpdateNode
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location3"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14052:16
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn4"
+ >
+ debugCheckAndUpdateNode
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location4"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14945:55
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn5"
+ >
+ debugCheckDirectivesFn
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location5"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14886:13
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn6"
+ >
+ View_MetaTableComponent_6
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location6"
+ >
+ ng:///AppModule/MetaTableComponent.ngfactory.js:98:5
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn7"
+ >
+ debugUpdateDirectives
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location7"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14871:12
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn8"
+ >
+ checkAndUpdateView
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location8"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14018:5
+ </span>
+
+
+
+ <span
+ className="objectBox-stackTrace-fn"
+ key="fn9"
+ >
+ callViewAction
+ </span>
+
+ <span
+ className="objectBox-stackTrace-location"
+ key="location9"
+ >
+ webpack-internal:///./node_modules/@angular/core/esm5/core.js:14369:21
+ </span>
+
+
+ </span>
+</span>
+`;
+
+exports[`Error - renderStacktrace prop uses renderStacktrace prop when provided 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj1021"
+ title={null}
+>
+ Error:
+ <span
+ className="objectBox objectBox-string"
+ >
+ bar
+ </span>
+ <li
+ className="frame"
+ >
+ Function errorBar called from debugger eval code:6:15
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function errorFoo called from debugger eval code:3:3
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function &lt;anonymous&gt; called from debugger eval code:8:1
+
+ </li>
+</span>
+`;
+
+exports[`Error - renderStacktrace prop uses renderStacktrace with longString errors too 1`] = `
+<span
+ className="objectBox-stackTrace reps-custom-format"
+ data-link-actor-id="server1.conn1.child1/obj33"
+ title={null}
+>
+ InternalError:
+ <span
+ className="objectBox objectBox-string"
+ >
+ too much recursion
+ </span>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:32:1
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+
+ </li>
+ <li
+ className="frame"
+ >
+ Function execute/AppComponent&lt;/AppComponent.prototype.doStuff called from https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21
+
+ </li>
+</span>
+`;
diff --git a/devtools/client/shared/components/test/node/components/reps/__snapshots__/nan.test.js.snap b/devtools/client/shared/components/test/node/components/reps/__snapshots__/nan.test.js.snap
new file mode 100644
index 0000000000..c80b14a2fb
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/__snapshots__/nan.test.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NaN renders NaN Rep as expected 1`] = `
+<span
+ className="objectBox objectBox-nan"
+ title={null}
+>
+ NaN
+</span>
+`;
diff --git a/devtools/client/shared/components/test/node/components/reps/accessible.test.js b/devtools/client/shared/components/test/node/components/reps/accessible.test.js
new file mode 100644
index 0000000000..8df9650c36
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/accessible.test.js
@@ -0,0 +1,321 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest, __dirname */
+const { mount, shallow } = require("enzyme");
+const { JSDOM } = require("jsdom");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Accessible } = REPS;
+const {
+ ELLIPSIS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessible.js");
+
+describe("Accessible - Document", () => {
+ const stub = stubs.get("Document");
+
+ it("selects Accessible Rep", () => {
+ expect(getRep(stub)).toBe(Accessible.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual('"New Tab": document');
+ expect(renderedComponent.prop("title")).toEqual('"New Tab": document');
+ });
+});
+
+describe("Accessible - ButtonMenu", () => {
+ const stub = stubs.get("ButtonMenu");
+
+ it("selects Accessible Rep", () => {
+ expect(getRep(stub)).toBe(Accessible.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '"New to Nightly? Let’s get started.": buttonmenu'
+ );
+ });
+
+ it("renders an inspect icon", () => {
+ const onInspectIconClick = jest.fn();
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ onInspectIconClick,
+ })
+ );
+
+ const node = renderedComponent.find(".open-accessibility-inspector");
+ node.simulate("click", { type: "click" });
+
+ expect(node.exists()).toBeTruthy();
+ expect(onInspectIconClick.mock.calls).toHaveLength(1);
+ expect(onInspectIconClick.mock.calls[0][0]).toEqual(stub);
+ expect(onInspectIconClick.mock.calls[0][1].type).toEqual("click");
+ });
+
+ it("calls the expected function when click is fired on Rep", () => {
+ const onAccessibleClick = jest.fn();
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ onAccessibleClick,
+ })
+ );
+
+ renderedComponent.simulate("click");
+
+ expect(onAccessibleClick.mock.calls).toHaveLength(1);
+ });
+
+ it("calls the expected function when mouseout is fired on Rep", () => {
+ const onAccessibleMouseOut = jest.fn();
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ onAccessibleMouseOut,
+ })
+ );
+
+ renderedComponent.simulate("mouseout");
+
+ expect(onAccessibleMouseOut.mock.calls).toHaveLength(1);
+ });
+
+ it("calls the expected function when mouseover is fired on Rep", () => {
+ const onAccessibleMouseOver = jest.fn();
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ onAccessibleMouseOver,
+ })
+ );
+
+ renderedComponent.simulate("mouseover");
+
+ expect(onAccessibleMouseOver.mock.calls).toHaveLength(1);
+ expect(onAccessibleMouseOver.mock.calls[0][0]).toEqual(stub);
+ });
+});
+
+describe("Accessible - No Name Accessible", () => {
+ const stub = stubs.get("NoName");
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("text container");
+ expect(renderedComponent.prop("title")).toEqual("text container");
+ expect(renderedComponent.find(".separator").exists()).toBeFalsy();
+ expect(renderedComponent.find(".accessible-namer").exists()).toBeFalsy();
+ });
+});
+
+describe("Accessible - Disconnected accessible", () => {
+ const stub = stubs.get("DisconnectedAccessible");
+
+ it(
+ "renders no inspect icon when the accessible is not in the Accessible " +
+ "tree",
+ () => {
+ const onInspectIconClick = jest.fn();
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ onInspectIconClick,
+ })
+ );
+
+ expect(
+ renderedComponent.find(".open-accessibility-inspector").exists()
+ ).toBeFalsy();
+ }
+ );
+});
+
+describe("Accessible - No Preview (not a valid grip)", () => {
+ const stub = stubs.get("NoPreview");
+
+ it("does not select Accessible Rep", () => {
+ expect(getRep(stub)).not.toBe(Accessible.rep);
+ });
+});
+
+describe("Accessible - Accessible with long name", () => {
+ const stub = stubs.get("AccessibleWithLongName");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(Accessible.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ `"${"a".repeat(1000)}": text leaf`
+ );
+ });
+
+ it("renders with expected text content with name max length", () => {
+ const renderedComponent = shallow(
+ Accessible.rep({
+ object: stub,
+ nameMaxLength: 20,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ `"${"a".repeat(9)}${ELLIPSIS}${"a".repeat(8)}": text leaf`
+ );
+ });
+});
+
+describe("Accessible - Inspect icon title", () => {
+ const stub = stubs.get("PushButton");
+
+ it("renders with expected title", () => {
+ const inspectIconTitle = "inspect icon title";
+
+ const renderedComponent = shallow(
+ Accessible.rep({
+ inspectIconTitle,
+ object: stub,
+ onInspectIconClick: jest.fn(),
+ })
+ );
+
+ const iconNode = renderedComponent.find(".open-accessibility-inspector");
+ expect(iconNode.prop("title")).toEqual(inspectIconTitle);
+ });
+});
+
+describe("Accessible - Separator text", () => {
+ const stub = stubs.get("PushButton");
+
+ it("renders with expected title", () => {
+ const separatorText = " - ";
+
+ const renderedComponent = shallow(
+ Accessible.rep({
+ separatorText,
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual('"Search" - pushbutton');
+ });
+});
+
+describe("Accessible - Role first", () => {
+ const stub = stubs.get("PushButton");
+
+ it("renders with expected title", () => {
+ const renderedComponent = shallow(
+ Accessible.rep({
+ roleFirst: true,
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual('pushbutton: "Search"');
+ });
+});
+
+describe("Accessible - Cursor style", () => {
+ const stub = stubs.get("PushButton");
+
+ it("renders with styled cursor", async () => {
+ const window = await createWindowForCursorTest();
+ const attachTo = window.document.querySelector("#attach-to");
+ const renderedComponent = mount(
+ Accessible.rep({
+ object: stub,
+ onAccessibleClick: jest.fn(),
+ onInspectIconClick: jest.fn(),
+ }),
+ {
+ attachTo,
+ }
+ );
+
+ const objectNode = renderedComponent.getDOMNode();
+ const iconNode = objectNode.querySelector(".open-accessibility-inspector");
+ expect(renderedComponent.hasClass("clickable")).toBeTruthy();
+ expect(window.getComputedStyle(objectNode).cursor).toEqual("pointer");
+ expect(window.getComputedStyle(iconNode).cursor).toEqual("pointer");
+ });
+
+ it("renders with unstyled cursor", async () => {
+ const window = await createWindowForCursorTest();
+ const attachTo = window.document.querySelector("#attach-to");
+ const renderedComponent = mount(
+ Accessible.rep({
+ object: stub,
+ }),
+ {
+ attachTo,
+ }
+ );
+
+ const objectNode = renderedComponent.getDOMNode();
+ expect(renderedComponent.hasClass("clickable")).toBeFalsy();
+ expect(window.getComputedStyle(objectNode).cursor).toEqual("");
+ });
+});
+
+async function createWindowForCursorTest() {
+ const path = require("path");
+ const css = await readTextFile(
+ path.resolve(__dirname, "../../../../reps/", "reps.css")
+ );
+ const html = `
+ <body>
+ <style>${css}</style>
+ <div id="attach-to"></div>
+ </body>
+ `;
+
+ return new JSDOM(html).window;
+}
+
+async function readTextFile(fileName) {
+ return new Promise((resolve, reject) => {
+ const fs = require("fs");
+ fs.readFile(fileName, "utf8", (error, text) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(text);
+ }
+ });
+ });
+}
diff --git a/devtools/client/shared/components/test/node/components/reps/accessor.test.js b/devtools/client/shared/components/test/node/components/reps/accessor.test.js
new file mode 100644
index 0000000000..31499fea52
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/accessor.test.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { Accessor, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/accessor.js");
+
+describe("Accessor - getter", () => {
+ const object = stubs.get("getter");
+
+ it("Rep correctly selects Accessor Rep", () => {
+ expect(getRep(object)).toBe(Accessor.rep);
+ });
+
+ it("Accessor rep has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({ object, shouldRenderTooltip: true })
+ );
+ expect(renderedComponent.text()).toEqual("Getter");
+ expect(renderedComponent.prop("title")).toEqual("Getter");
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+});
+
+describe("Accessor - setter", () => {
+ const object = stubs.get("setter");
+
+ it("Rep correctly selects Accessor Rep", () => {
+ expect(getRep(object)).toBe(Accessor.rep);
+ });
+
+ it("Accessor rep has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({ object, shouldRenderTooltip: true })
+ );
+ expect(renderedComponent.text()).toEqual("Setter");
+ expect(renderedComponent.prop("title")).toEqual("Setter");
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+});
+
+describe("Accessor - getter & setter", () => {
+ const object = stubs.get("getter setter");
+
+ it("Rep correctly selects Accessor Rep", () => {
+ expect(getRep(object)).toBe(Accessor.rep);
+ });
+
+ it("Accessor rep has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({ object, shouldRenderTooltip: true })
+ );
+ expect(renderedComponent.text()).toEqual("Getter & Setter");
+ expect(renderedComponent.prop("title")).toEqual("Getter & Setter");
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+});
+
+describe("Accessor - Invoke getter", () => {
+ it("renders an icon for getter with onInvokeGetterButtonClick", () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const object = stubs.get("getter");
+ const renderedComponent = shallow(
+ Rep({ object, onInvokeGetterButtonClick })
+ );
+
+ const node = renderedComponent.find(".invoke-getter");
+ node.simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+ expect(node.prop("title")).toEqual("Invoke getter");
+ expect(node.exists()).toBeTruthy();
+ expect(onInvokeGetterButtonClick.mock.calls).toHaveLength(1);
+ });
+
+ it("does not render an icon for a setter only", () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const object = stubs.get("setter");
+ const renderedComponent = shallow(
+ Rep({ object, onInvokeGetterButtonClick })
+ );
+ expect(renderedComponent.text()).toEqual("Setter");
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+
+ it("renders an icon for getter/setter with onInvokeGetterButtonClick", () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const object = stubs.get("getter setter");
+ const renderedComponent = shallow(
+ Rep({ object, onInvokeGetterButtonClick })
+ );
+
+ const node = renderedComponent.find(".invoke-getter");
+ node.simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+
+ expect(node.exists()).toBeTruthy();
+ expect(onInvokeGetterButtonClick.mock.calls).toHaveLength(1);
+ });
+
+ it("does not render an icon when the object has an evaluation", () => {
+ const onInvokeGetterButtonClick = jest.fn();
+ const object = stubs.get("getter");
+ const renderedComponent = shallow(
+ Rep({
+ object,
+ onInvokeGetterButtonClick,
+ evaluation: { getterValue: "hello" },
+ })
+ );
+ expect(renderedComponent.text()).toMatchSnapshot();
+
+ const node = renderedComponent.find(".invoke-getter");
+ expect(node.exists()).toBeFalsy();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/array.test.js b/devtools/client/shared/components/test/node/components/reps/array.test.js
new file mode 100644
index 0000000000..cd8cfb9d97
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/array.test.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const { ArrayRep, Rep } = REPS;
+const { maxLengthMap } = ArrayRep;
+
+describe("Array", () => {
+ it("selects Array Rep as expected", () => {
+ const stub = [];
+ expect(getRep(stub, undefined, true)).toBe(ArrayRep.rep);
+ });
+
+ it("renders empty array as expected", () => {
+ const object = [];
+ const renderRep = props => shallow(Rep({ object, noGrip: true, ...props }));
+
+ const defaultOutput = "[]";
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: undefined }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+
+ it("renders basic array as expected", () => {
+ const object = [1, "foo", {}];
+ const renderRep = props => shallow(Rep({ object, noGrip: true, ...props }));
+
+ const defaultOutput = '[ 1, "foo", {} ]';
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: undefined }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("[…]");
+ expect(renderRep({ mode: MODE.TINY }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+
+ it("renders array with more than SHORT mode max props as expected", () => {
+ const object = Array(maxLengthMap.get(MODE.SHORT) + 1).fill("foo");
+ const renderRep = props => shallow(Rep({ object, noGrip: true, ...props }));
+
+ const defaultShortOutput = `[ ${Array(maxLengthMap.get(MODE.SHORT))
+ .fill('"foo"')
+ .join(", ")}, … ]`;
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultShortOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("[…]");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultShortOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ `[ ${Array(maxLengthMap.get(MODE.SHORT) + 1)
+ .fill('"foo"')
+ .join(", ")} ]`
+ );
+ });
+
+ it("renders array with more than LONG mode maximum props as expected", () => {
+ const object = Array(maxLengthMap.get(MODE.LONG) + 1).fill("foo");
+ const renderRep = props => shallow(Rep({ object, noGrip: true, ...props }));
+
+ const defaultShortOutput = `[ ${Array(maxLengthMap.get(MODE.SHORT))
+ .fill('"foo"')
+ .join(", ")}, … ]`;
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultShortOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("[…]");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultShortOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ `[ ${Array(maxLengthMap.get(MODE.LONG)).fill('"foo"').join(", ")}, … ]`
+ );
+ });
+
+ it("renders recursive array as expected", () => {
+ const object = [1];
+ object.push(object);
+ const renderRep = props => shallow(Rep({ object, noGrip: true, ...props }));
+
+ const defaultOutput = "[ 1, […] ]";
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: undefined }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("[…]");
+ expect(renderRep({ mode: MODE.TINY }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+
+ it("renders array containing an object as expected", () => {
+ const object = [
+ {
+ p1: "s1",
+ p2: ["a1", "a2", "a3"],
+ p3: "s3",
+ p4: "s4",
+ },
+ ];
+ const renderRep = props => shallow(Rep({ object, noGrip: true, ...props }));
+
+ const defaultOutput = "[ {…} ]";
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: undefined }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("[…]");
+ expect(renderRep({ mode: MODE.TINY }).prop("title")).toBe("Array");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/attribute.test.js b/devtools/client/shared/components/test/node/components/reps/attribute.test.js
new file mode 100644
index 0000000000..c13ab59857
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/attribute.test.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const { Attribute, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/attribute.js");
+
+describe("Attribute", () => {
+ const stub = stubs.get("Attribute")._grip;
+
+ it("Rep correctly selects Attribute Rep", () => {
+ expect(getRep(stub)).toBe(Attribute.rep);
+ });
+
+ it("Attribute rep has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.text()).toEqual(
+ 'class="autocomplete-suggestions"'
+ );
+ expect(renderedComponent.prop("title")).toBe(
+ 'class="autocomplete-suggestions"'
+ );
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/big-int.test.js b/devtools/client/shared/components/test/node/components/reps/big-int.test.js
new file mode 100644
index 0000000000..1fa6cd0ee1
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/big-int.test.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { BigInt, Rep } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/big-int.js");
+
+describe("BigInt", () => {
+ describe("1n", () => {
+ const stub = stubs.get("1n");
+
+ it("correctly selects BigInt Rep for BigInt value", () => {
+ expect(getRep(stub)).toBe(BigInt.rep);
+ });
+
+ it("renders with expected text content for BigInt", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("1n");
+ expect(renderedComponent.prop("title")).toBe("1n");
+ });
+ });
+
+ describe("-2n", () => {
+ const stub = stubs.get("-2n");
+
+ it("correctly selects BigInt Rep for negative BigInt value", () => {
+ expect(getRep(stub)).toBe(BigInt.rep);
+ });
+
+ it("renders with expected text content for negative BigInt", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("-2n");
+ expect(renderedComponent.prop("title")).toBe("-2n");
+ });
+ });
+
+ describe("0n", () => {
+ const stub = stubs.get("0n");
+
+ it("correctly selects BigInt Rep for zero BigInt value", () => {
+ expect(getRep(stub)).toBe(BigInt.rep);
+ });
+
+ it("renders with expected text content for zero BigInt", () => {
+ const renderedComponent = shallow(Rep({ object: stub }));
+ expect(renderedComponent.text()).toEqual("0n");
+ });
+ });
+
+ describe("in objects", () => {
+ it("renders with expected text content in Array", () => {
+ const stub = stubs.get("[1n,-2n,0n]");
+ const renderedComponent = shallow(Rep({ object: stub }));
+ expect(renderedComponent.text()).toEqual("Array(3) [ 1n, -2n, 0n ]");
+ });
+
+ it("renders with expected text content in Set", () => {
+ const stub = stubs.get("new Set([1n,-2n,0n])");
+ const renderedComponent = shallow(Rep({ object: stub }));
+ expect(renderedComponent.text()).toEqual("Set(3) [ 1n, -2n, 0n ]");
+ });
+
+ it("renders with expected text content in Map", () => {
+ const stub = stubs.get("new Map([ [1n, -1n], [-2n, 0n], [0n, -2n]])");
+ const renderedComponent = shallow(Rep({ object: stub }));
+ expect(renderedComponent.text()).toEqual(
+ "Map(3) { 1n → -1n, -2n → 0n, 0n → -2n }"
+ );
+ });
+
+ it("renders with expected text content in Object", () => {
+ const stub = stubs.get("({simple: 1n, negative: -2n, zero: 0n})");
+ const renderedComponent = shallow(Rep({ object: stub }));
+ expect(renderedComponent.text()).toEqual(
+ "Object { simple: 1n, negative: -2n, zero: 0n }"
+ );
+ });
+
+ it("renders with expected text content in Promise", () => {
+ const stub = stubs.get("Promise.resolve(1n)");
+ const renderedComponent = shallow(Rep({ object: stub }));
+ expect(renderedComponent.text()).toEqual(
+ 'Promise { <state>: "fulfilled", <value>: 1n }'
+ );
+ });
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/comment-node.test.js b/devtools/client/shared/components/test/node/components/reps/comment-node.test.js
new file mode 100644
index 0000000000..df420f8b1b
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/comment-node.test.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const { Rep, CommentNode } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/comment-node.js");
+
+describe("CommentNode", () => {
+ const stub = stubs.get("Comment")._grip;
+
+ it("selects CommentNode Rep correctly", () => {
+ expect(getRep(stub)).toEqual(CommentNode.rep);
+ });
+
+ it("renders with correct class names", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.hasClass("objectBox theme-comment")).toBe(true);
+ });
+
+ it("renders with correct title tooltip", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.prop("title")).toBe(
+ "<!-- test\nand test\nand test\nand test\nand test\nand test\nand test -->"
+ );
+ });
+
+ it("renders as expected", () => {
+ const object = stubs.get("Comment")._grip;
+ const renderRep = props => shallow(CommentNode.rep({ object, ...props }));
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toEqual(
+ "<!-- test\nand test\nand test\nan…d test\nand test\nand test -->"
+ );
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toEqual(
+ "<!-- test\\nand test\\na… test\\nand test -->"
+ );
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toEqual(`<!-- ${stub.preview.textContent} -->`);
+ expectActorAttribute(component, object.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/date-time.test.js b/devtools/client/shared/components/test/node/components/reps/date-time.test.js
new file mode 100644
index 0000000000..62a8557c3a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/date-time.test.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const { DateTime, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/date-time.js");
+
+describe("test DateTime", () => {
+ const stub = stubs.get("DateTime")._grip;
+
+ it("selects DateTime as expected", () => {
+ expect(getRep(stub)).toBe(DateTime.rep);
+ });
+
+ it("renders DateTime as expected", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ const expectedDate = new Date(
+ "Date Thu Mar 31 2016 00:17:24 GMT+0300 (EAT)"
+ ).toString();
+
+ expect(renderedComponent.text()).toEqual(`Date ${expectedDate}`);
+ expect(renderedComponent.prop("title")).toEqual(`Date ${expectedDate}`);
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("test invalid DateTime", () => {
+ const stub = stubs.get("InvalidDateTime")._grip;
+
+ it("renders expected text for invalid date", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Invalid Date");
+ expect(renderedComponent.prop("title")).toEqual("Invalid Date");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/document-type.test.js b/devtools/client/shared/components/test/node/components/reps/document-type.test.js
new file mode 100644
index 0000000000..f4709c6d5d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/document-type.test.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const { DocumentType } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/document-type.js");
+
+describe("DocumentType", () => {
+ const stub = stubs.get("html");
+ it("correctly selects DocumentType Rep", () => {
+ expect(getRep(stub)).toBe(DocumentType.rep);
+ });
+
+ it("renders with expected text content on html doctype", () => {
+ const renderedComponent = shallow(
+ DocumentType.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("<!DOCTYPE html>");
+ expect(renderedComponent.prop("title")).toEqual("<!DOCTYPE html>");
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it("renders with expected text content on empty doctype", () => {
+ const unnamedStub = stubs.get("unnamed");
+ const renderedComponent = shallow(
+ DocumentType.rep({
+ object: unnamedStub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.text()).toEqual("<!DOCTYPE>");
+ expect(renderedComponent.prop("title")).toEqual("<!DOCTYPE>");
+ expectActorAttribute(renderedComponent, unnamedStub.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/document.test.js b/devtools/client/shared/components/test/node/components/reps/document.test.js
new file mode 100644
index 0000000000..36d2eced11
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/document.test.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const { Document } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/document.js");
+
+describe("Document", () => {
+ const stub = stubs.get("Document");
+ it("correctly selects Document Rep", () => {
+ expect(getRep(stub)).toBe(Document.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ Document.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "HTMLDocument https://www.mozilla.org/en-US/firefox/new/"
+ );
+ expect(renderedComponent.prop("title")).toEqual(
+ "HTMLDocument https://www.mozilla.org/en-US/firefox/new/"
+ );
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it("renders location-less document with expected text content", () => {
+ const renderedComponent = shallow(
+ Document.rep({
+ object: stubs.get("Location-less Document"),
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("HTMLDocument");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/element-node.test.js b/devtools/client/shared/components/test/node/components/reps/element-node.test.js
new file mode 100644
index 0000000000..f8cb2e401b
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/element-node.test.js
@@ -0,0 +1,654 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest, __dirname */
+const { mount, shallow } = require("enzyme");
+const { JSDOM } = require("jsdom");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ MAX_ATTRIBUTE_LENGTH,
+} = require("resource://devtools/client/shared/components/reps/reps/element-node.js");
+const { ElementNode } = REPS;
+const {
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const {
+ ELLIPSIS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/element-node.js");
+
+describe("ElementNode - BodyNode", () => {
+ const stub = stubs.get("BodyNode");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<body id="body-id" class="body-class">'
+ );
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it("renders with expected text content on tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("body#body-id.body-class");
+ expect(renderedComponent.prop("title")).toEqual("body#body-id.body-class");
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("ElementNode - DocumentElement", () => {
+ const stub = stubs.get("DocumentElement");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual('<html dir="ltr" lang="en-US">');
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("html");
+ });
+});
+
+describe("ElementNode - Node", () => {
+ const stub = stubs.get("Node");
+ const grips = getSelectableInInspectorGrips(stub);
+
+ it("has one node grip", () => {
+ expect(grips).toHaveLength(1);
+ });
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<input id="newtab-customize-button" class="bar baz" dir="ltr" ' +
+ 'title="Customize your New Tab page" value="foo" type="button">'
+ );
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "input#newtab-customize-button.bar.baz"
+ );
+ });
+
+ it("renders an inspect icon", () => {
+ const onInspectIconClick = jest.fn();
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stubs.get("Node"),
+ onInspectIconClick,
+ })
+ );
+
+ const node = renderedComponent.find(".open-inspector");
+ node.simulate("click", { type: "click" });
+
+ expect(node.exists()).toBeTruthy();
+ expect(onInspectIconClick.mock.calls).toHaveLength(1);
+ expect(onInspectIconClick.mock.calls[0][0]).toEqual(stub);
+ expect(onInspectIconClick.mock.calls[0][1].type).toEqual("click");
+ });
+
+ it("calls the expected function when click is fired on Rep", () => {
+ const onDOMNodeClick = jest.fn();
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ onDOMNodeClick,
+ })
+ );
+
+ renderedComponent.simulate("click");
+
+ expect(onDOMNodeClick.mock.calls).toHaveLength(1);
+ });
+
+ it("calls the expected function when mouseout is fired on Rep", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ onDOMNodeMouseOut,
+ })
+ );
+
+ renderedComponent.simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toEqual(stub);
+ });
+
+ it("calls the expected function when mouseover is fired on Rep", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ onDOMNodeMouseOver,
+ })
+ );
+
+ renderedComponent.simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toEqual(stub);
+ });
+});
+
+describe("ElementNode - Leading and trailing spaces class name", () => {
+ const stub = stubs.get("NodeWithLeadingAndTrailingSpacesClassName");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<body id="nightly-whatsnew" class=" html-ltr ">'
+ );
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("body#nightly-whatsnew.html-ltr");
+ });
+});
+
+describe("ElementNode - Node with spaces in the class name", () => {
+ const stub = stubs.get("NodeWithSpacesInClassName");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("body.a.b.c");
+ });
+});
+
+describe("ElementNode - Node without attributes", () => {
+ const stub = stubs.get("NodeWithoutAttributes");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("<p>");
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("p");
+ });
+});
+
+describe("ElementNode - Node with many attributes", () => {
+ const stub = stubs.get("LotsOfAttributes");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+ 'h="" i="" j="" k="" l="" m="" n="">'
+ );
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("p#lots-of-attributes");
+ });
+});
+
+describe("ElementNode - SVG Node", () => {
+ const stub = stubs.get("SvgNode");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<clipPath id="clip" class="svg-element">'
+ );
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("clipPath#clip.svg-element");
+ });
+});
+
+describe("ElementNode - SVG Node in XHTML", () => {
+ const stub = stubs.get("SvgNodeInXHTML");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<svg:circle class="svg-element" cx="0" cy="0" r="5">'
+ );
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("svg:circle.svg-element");
+ });
+});
+
+describe("ElementNode - Disconnected node", () => {
+ const stub = stubs.get("DisconnectedNode");
+
+ it("renders no inspect icon when the node is not in the DOM tree", () => {
+ const onInspectIconClick = jest.fn();
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ onInspectIconClick,
+ })
+ );
+
+ expect(renderedComponent.find(".open-inspector").exists()).toBeFalsy();
+ });
+});
+
+describe("ElementNode - Element with longString attribute", () => {
+ const stub = stubs.get("NodeWithLongStringAttribute");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ `<div data-test="${"a".repeat(MAX_ATTRIBUTE_LENGTH)}${ELLIPSIS}">`
+ );
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("div");
+ });
+});
+
+describe("ElementNode - Element attribute cropping", () => {
+ it("renders no title attribute for short attribute", () => {
+ const stub = stubs.get("NodeWithSpacesInClassName");
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.first().find("span.attrValue").prop("title")).toBe(
+ undefined
+ );
+ });
+
+ it("renders partial value for long attribute", () => {
+ const stub = stubs.get("NodeWithLongAttribute");
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<p data-test="aaaaaaaaaaaaaaaaaaaaaaaa…aaaaaaaaaaaaaaaaaaaaaaa">'
+ );
+ expect(renderedComponent.first().find("span.attrValue").prop("title")).toBe(
+ "a".repeat(100)
+ );
+ });
+
+ it("renders partial attribute for LongString", () => {
+ const stub = stubs.get("NodeWithLongStringAttribute");
+
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ '<div data-test="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…">'
+ );
+ expect(renderedComponent.first().find("span.attrValue").prop("title")).toBe(
+ "a".repeat(1000)
+ );
+ });
+});
+
+describe("ElementNode - : Marker pseudo element", () => {
+ const stub = stubs.get("MarkerPseudoElement");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("::marker");
+ expect(renderedComponent.prop("title")).toEqual("::marker");
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("::marker");
+ expect(renderedComponent.prop("title")).toEqual("::marker");
+ });
+});
+
+describe("ElementNode - : Before pseudo element", () => {
+ const stub = stubs.get("BeforePseudoElement");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("::before");
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("::before");
+ });
+});
+
+describe("ElementNode - After pseudo element", () => {
+ const stub = stubs.get("AfterPseudoElement");
+
+ it("selects ElementNode Rep", () => {
+ expect(getRep(stub)).toBe(ElementNode.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("::after");
+ });
+
+ it("renders with expected text content in tiny mode", () => {
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("::after");
+ });
+});
+
+describe("ElementNode - Inspect icon title", () => {
+ const stub = stubs.get("Node");
+
+ it("renders with expected title", () => {
+ const inspectIconTitle = "inspect icon title";
+
+ const renderedComponent = shallow(
+ ElementNode.rep({
+ inspectIconTitle,
+ object: stub,
+ shouldRenderTooltip: true,
+ onInspectIconClick: jest.fn(),
+ })
+ );
+
+ const iconNode = renderedComponent.find(".open-inspector");
+ expect(iconNode.prop("title")).toEqual(inspectIconTitle);
+ });
+});
+
+describe("ElementNode - Cursor style", () => {
+ const stub = stubs.get("Node");
+
+ it("renders with styled cursor", async () => {
+ const window = await createWindowForCursorTest();
+ const attachTo = window.document.querySelector("#attach-to");
+ const renderedComponent = mount(
+ ElementNode.rep({
+ object: stub,
+ onDOMNodeClick: jest.fn(),
+ onInspectIconClick: jest.fn(),
+ }),
+ {
+ attachTo,
+ }
+ );
+
+ const objectNode = renderedComponent.getDOMNode();
+ const iconNode = objectNode.querySelector(".open-inspector");
+ expect(renderedComponent.hasClass("clickable")).toBeTruthy();
+ expect(window.getComputedStyle(objectNode).cursor).toEqual("pointer");
+ expect(window.getComputedStyle(iconNode).cursor).toEqual("pointer");
+ });
+
+ it("renders with unstyled cursor", async () => {
+ const window = await createWindowForCursorTest();
+ const attachTo = window.document.querySelector("#attach-to");
+ const renderedComponent = mount(
+ ElementNode.rep({
+ object: stub,
+ }),
+ {
+ attachTo,
+ }
+ );
+
+ const objectNode = renderedComponent.getDOMNode();
+ expect(renderedComponent.hasClass("clickable")).toBeFalsy();
+ expect(window.getComputedStyle(objectNode).cursor).toEqual("");
+ });
+});
+
+async function createWindowForCursorTest() {
+ const path = require("path");
+ const css = await readTextFile(
+ path.resolve(__dirname, "../../../../reps/", "reps.css")
+ );
+ const html = `
+ <body>
+ <style>${css}</style>
+ <div id="attach-to"></div>
+ </body>
+ `;
+
+ return new JSDOM(html).window;
+}
+
+async function readTextFile(fileName) {
+ return new Promise((resolve, reject) => {
+ const fs = require("fs");
+ fs.readFile(fileName, "utf8", (error, text) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(text);
+ }
+ });
+ });
+}
diff --git a/devtools/client/shared/components/test/node/components/reps/error.test.js b/devtools/client/shared/components/test/node/components/reps/error.test.js
new file mode 100644
index 0000000000..ebbbd8be48
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/error.test.js
@@ -0,0 +1,748 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const { ErrorRep } = REPS;
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/error.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+describe("Error - Simple error", () => {
+ // Test object = `new Error("Error message")`
+ const stub = stubs.get("SimpleError");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for simple error", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ expect(renderedComponent.prop("title")).toBe('Error: "Error message"');
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it("renders with expected text for simple error in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Error");
+ });
+
+ it("renders with error type and preview message when in short mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stubs.get("MultilineStackError"),
+ mode: MODE.SHORT,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with error type only when customFormat prop isn't set", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stubs.get("MultilineStackError"),
+ mode: MODE.SHORT,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with error type only when depth is > 0", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stubs.get("MultilineStackError"),
+ customFormat: true,
+ depth: 1,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - Multi line stack error", () => {
+ /*
+ * Test object = `
+ * function errorFoo() {
+ * errorBar();
+ * }
+ * function errorBar() {
+ * console.log(new Error("bar"));
+ * }
+ * errorFoo();`
+ */
+ const stub = stubs.get("MultilineStackError");
+
+ it("correctly selects the Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for Error object", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders expected text for simple multiline error in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Error");
+ });
+});
+
+describe("Error - Error without stacktrace", () => {
+ const stub = stubs.get("ErrorWithoutStacktrace");
+
+ it("correctly selects the Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for Error object", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Error: Error message");
+ });
+
+ it("renders expected text for error without stacktrace in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Error");
+ });
+});
+
+describe("Error - Eval error", () => {
+ // Test object = `new EvalError("EvalError message")`
+ const stub = stubs.get("EvalError");
+
+ it("correctly selects the Error Rep for EvalError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for an EvalError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for an EvalError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("EvalError");
+ });
+});
+
+describe("Error - Internal error", () => {
+ // Test object = `new InternalError("InternalError message")`
+ const stub = stubs.get("InternalError");
+
+ it("correctly selects the Error Rep for InternalError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for an InternalError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for an InternalError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("InternalError");
+ });
+});
+
+describe("Error - Range error", () => {
+ // Test object = `new RangeError("RangeError message")`
+ const stub = stubs.get("RangeError");
+
+ it("correctly selects the Error Rep for RangeError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for RangeError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for RangeError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("RangeError");
+ });
+});
+
+describe("Error - Reference error", () => {
+ // Test object = `new ReferenceError("ReferenceError message"`
+ const stub = stubs.get("ReferenceError");
+
+ it("correctly selects the Error Rep for ReferenceError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for ReferenceError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for ReferenceError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("ReferenceError");
+ });
+});
+
+describe("Error - Syntax error", () => {
+ // Test object = `new SyntaxError("SyntaxError message"`
+ const stub = stubs.get("SyntaxError");
+
+ it("correctly selects the Error Rep for SyntaxError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for SyntaxError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for SyntaxError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("SyntaxError");
+ });
+});
+
+describe("Error - Type error", () => {
+ // Test object = `new TypeError("TypeError message"`
+ const stub = stubs.get("TypeError");
+
+ it("correctly selects the Error Rep for TypeError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for TypeError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for TypeError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("TypeError");
+ });
+});
+
+describe("Error - URI error", () => {
+ // Test object = `new URIError("URIError message")`
+ const stub = stubs.get("URIError");
+
+ it("correctly selects the Error Rep for URIError object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for URIError", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders with expected text for URIError in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("URIError");
+ });
+});
+
+describe("Error - DOMException", () => {
+ const stub = stubs.get("DOMException");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text for DOMException", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "DOMException: 'foo;()bar!' is not a valid selector"
+ );
+ });
+
+ it("renders with expected text for DOMException in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("DOMException");
+ });
+});
+
+describe("Error - base-loader.sys.mjs", () => {
+ const stub = stubs.get("base-loader Error");
+
+ it("renders as expected without mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("renders as expected in tiny mode", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - longString stacktrace", () => {
+ const stub = stubs.get("longString stack Error");
+
+ it("renders as expected", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - longString stacktrace - cut-off location", () => {
+ const stub = stubs.get("longString stack Error - cut-off location");
+
+ it("renders as expected", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - stacktrace location click", () => {
+ it("Calls onViewSourceInDebugger with the expected arguments", () => {
+ const onViewSourceInDebugger = jest.fn();
+ const object = stubs.get("base-loader Error");
+
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object,
+ onViewSourceInDebugger,
+ customFormat: true,
+ })
+ );
+
+ const locations = renderedComponent.find(".objectBox-stackTrace-location");
+ expect(locations.exists()).toBeTruthy();
+
+ expect(locations.first().prop("title")).toBe(
+ "View source in debugger → " +
+ "resource://devtools/client/debugger-client.js:856:9"
+ );
+ locations.first().simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+
+ expect(onViewSourceInDebugger.mock.calls).toHaveLength(1);
+ let mockCall = onViewSourceInDebugger.mock.calls[0][0];
+ expect(mockCall.url).toEqual(
+ "resource://devtools/client/debugger-client.js"
+ );
+ expect(mockCall.line).toEqual(856);
+ expect(mockCall.column).toEqual(9);
+
+ expect(locations.last().prop("title")).toBe(
+ "View source in debugger → " +
+ "resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14"
+ );
+ locations.last().simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+
+ expect(onViewSourceInDebugger.mock.calls).toHaveLength(2);
+ mockCall = onViewSourceInDebugger.mock.calls[1][0];
+ expect(mockCall.url).toEqual(
+ "resource://devtools/shared/ThreadSafeDevToolsUtils.js"
+ );
+ expect(mockCall.line).toEqual(109);
+ expect(mockCall.column).toEqual(14);
+ });
+
+ it("Does not call onViewSourceInDebugger on excluded urls", () => {
+ const onViewSourceInDebugger = jest.fn();
+ const object = stubs.get("URIError");
+
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object,
+ onViewSourceInDebugger,
+ customFormat: true,
+ })
+ );
+
+ const locations = renderedComponent.find(".objectBox-stackTrace-location");
+ expect(locations.exists()).toBeTruthy();
+ expect(locations.first().prop("title")).toBe(undefined);
+
+ locations.first().simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+
+ expect(onViewSourceInDebugger.mock.calls).toHaveLength(0);
+ });
+
+ it("Does not throw when onViewSourceInDebugger props is not provided", () => {
+ const object = stubs.get("base-loader Error");
+
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object,
+ customFormat: true,
+ })
+ );
+
+ const locations = renderedComponent.find(".objectBox-stackTrace-location");
+ expect(locations.exists()).toBeTruthy();
+ expect(locations.first().prop("title")).toBe(undefined);
+
+ locations.first().simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+ });
+});
+
+describe("Error - renderStacktrace prop", () => {
+ it("uses renderStacktrace prop when provided", () => {
+ const stub = stubs.get("MultilineStackError");
+
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ renderStacktrace: frames => {
+ return frames.map(frame =>
+ dom.li(
+ { className: "frame" },
+ `Function ${frame.functionName} called from ${frame.filename}:${frame.lineNumber}:${frame.columnNumber}\n`
+ )
+ );
+ },
+ customFormat: true,
+ })
+ );
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("uses renderStacktrace with longString errors too", () => {
+ const stub = stubs.get("longString stack Error - cut-off location");
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ renderStacktrace: frames => {
+ return frames.map(frame =>
+ dom.li(
+ { className: "frame" },
+ `Function ${frame.functionName} called from ${frame.filename}:${frame.lineNumber}:${frame.columnNumber}\n`
+ )
+ );
+ },
+ customFormat: true,
+ })
+ );
+ expect(renderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - Error with V8-like stack", () => {
+ // Test object:
+ // x = new Error("BOOM");
+ // x.stack = "Error: BOOM\ngetAccount@http://moz.com/script.js:1:2";
+ const stub = stubs.get("Error with V8-like stack");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("Error - Error with invalid stack", () => {
+ // Test object:
+ // x = new Error("bad stack");
+ // x.stack = "bar\nbaz\nfoo\n\n\n\n\n\n\n";
+ const stub = stubs.get("Error with invalid stack");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("Error - Error with undefined-grip stack", () => {
+ // Test object:
+ // x = new Error("sd");
+ // x.stack = undefined;
+ const stub = stubs.get("Error with undefined-grip stack");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("Error - Error with undefined-grip name", () => {
+ // Test object:
+ // x = new Error("");
+ // x.name = undefined;
+ const stub = stubs.get("Error with undefined-grip name");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+ expect(renderedComponent).toMatchSnapshot();
+
+ const tinyRenderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(tinyRenderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - Error with undefined-grip message", () => {
+ // Test object:
+ // x = new Error("");
+ // x.message = undefined;
+ const stub = stubs.get("Error with undefined-grip message");
+
+ it("correctly selects Error Rep for Error object", () => {
+ expect(getRep(stub)).toBe(ErrorRep.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+ expect(renderedComponent).toMatchSnapshot();
+
+ const tinyRenderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(tinyRenderedComponent).toMatchSnapshot();
+ });
+});
+
+describe("Error - Error with stack having frames with multiple @", () => {
+ const stub = stubs.get("Error with stack having frames with multiple @");
+
+ it("renders with expected text for Error object", () => {
+ const renderedComponent = shallow(
+ ErrorRep.rep({
+ object: stub,
+ customFormat: true,
+ })
+ );
+
+ expect(renderedComponent).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/event.test.js b/devtools/client/shared/components/test/node/components/reps/event.test.js
new file mode 100644
index 0000000000..fe0f6b5601
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/event.test.js
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Event } = REPS;
+const {
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/event.js");
+
+describe("Event - beforeprint", () => {
+ const object = stubs.get("testEvent");
+
+ it("correctly selects Event Rep", () => {
+ expect(getRep(object)).toBe(Event.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderedComponent = shallow(Event.rep({ object }));
+ expect(renderedComponent.text()).toEqual(
+ "beforeprint { target: Window, isTrusted: true, currentTarget: Window, " +
+ "… }"
+ );
+ expectActorAttribute(renderedComponent, object.actor);
+ });
+});
+
+describe("Event - keyboard event", () => {
+ const object = stubs.get("testKeyboardEvent");
+
+ it("correctly selects Event Rep", () => {
+ expect(getRep(object)).toBe(Event.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderRep = props => shallow(Event.rep({ object, ...props }));
+ expect(renderRep().text()).toEqual(
+ 'keyup { target: body, key: "Control", charCode: 0, … }'
+ );
+ expect(renderRep({ mode: MODE.LONG }).text()).toEqual(
+ 'keyup { target: body, key: "Control", charCode: 0, keyCode: 17 }'
+ );
+ });
+});
+
+describe("Event - keyboard event with modifiers", () => {
+ const object = stubs.get("testKeyboardEventWithModifiers");
+
+ it("correctly selects Event Rep", () => {
+ expect(getRep(object)).toBe(Event.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderRep = props => shallow(Event.rep({ object, ...props }));
+ expect(renderRep({ mode: MODE.LONG }).text()).toEqual(
+ 'keyup Meta-Shift { target: body, key: "M", charCode: 0, keyCode: 77 }'
+ );
+ });
+});
+
+describe("Event - message event", () => {
+ const object = stubs.get("testMessageEvent");
+
+ it("correctly selects Event Rep", () => {
+ expect(getRep(object)).toBe(Event.rep);
+ });
+
+ it("renders with expected text", () => {
+ const renderRep = props => shallow(Event.rep({ object, ...props }));
+ expect(renderRep().text()).toEqual(
+ 'message { target: Window, isTrusted: false, data: "test data", … }'
+ );
+ expect(renderRep({ mode: MODE.LONG }).text()).toEqual(
+ 'message { target: Window, isTrusted: false, data: "test data", ' +
+ 'origin: "null", lastEventId: "", source: Window, ports: Array, ' +
+ "currentTarget: Window, eventPhase: 2, bubbles: false, … }"
+ );
+ });
+});
+
+describe("Event - mouse event", () => {
+ const object = stubs.get("testMouseEvent");
+ const renderRep = props => shallow(Event.rep({ object, ...props }));
+
+ const grips = getSelectableInInspectorGrips(object);
+
+ it("has stub with one node grip", () => {
+ expect(grips).toHaveLength(1);
+ });
+
+ it("correctly selects Event Rep", () => {
+ expect(getRep(object)).toBe(Event.rep);
+ });
+
+ it("renders with expected text", () => {
+ expect(renderRep({ shouldRenderTooltip: true }).text()).toEqual(
+ "click { target: div#test, clientX: 62, clientY: 18, … }"
+ );
+ expect(renderRep({ shouldRenderTooltip: true }).prop("title")).toEqual(
+ "click"
+ );
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).text()
+ ).toEqual(
+ "click { target: div#test, buttons: 0, clientX: 62, clientY: 18, " +
+ "layerX: 0, layerY: 0 }"
+ );
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).prop("title")
+ ).toEqual("click");
+ });
+
+ it("renders an inspect icon", () => {
+ const onInspectIconClick = jest.fn();
+ const renderedComponent = renderRep({ onInspectIconClick });
+
+ const node = renderedComponent.find(".open-inspector");
+ node.simulate("click", { type: "click" });
+
+ expect(node.exists()).toBeTruthy();
+ expect(onInspectIconClick.mock.calls).toHaveLength(1);
+ expect(onInspectIconClick.mock.calls[0][0]).toEqual(grips[0]);
+ expect(onInspectIconClick.mock.calls[0][1].type).toEqual("click");
+ });
+
+ it("calls the expected function when mouseout is fired on Rep", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOut });
+
+ const node = wrapper.find(".objectBox-node");
+ node.simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toEqual(grips[0]);
+ });
+
+ it("calls the expected function when mouseover is fired on Rep", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOver });
+
+ const node = wrapper.find(".objectBox-node");
+ node.simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toEqual(grips[0]);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/failure.test.js b/devtools/client/shared/components/test/node/components/reps/failure.test.js
new file mode 100644
index 0000000000..6f31ae5687
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/failure.test.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global beforeAll, afterAll */
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/failure.js");
+
+let originalConsoleError;
+beforeAll(() => {
+ // Let's override the console.error function so we don't get an error message
+ // in the jest output for the expected exception.
+ originalConsoleError = window.console.error;
+ window.console.error = () => {};
+});
+
+describe("test Failure", () => {
+ const stub = stubs.get("Failure");
+
+ it("Fallback rendering has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent.text()).toEqual("Invalid object");
+ });
+
+ it("Fallback array rendering has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn0.obj337",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [1, stub, 2],
+ },
+ },
+ })
+ );
+ expect(renderedComponent.text()).toEqual(
+ "Array(3) [ 1, Invalid object, 2 ]"
+ );
+ });
+});
+
+afterAll(() => {
+ // Reverting the override.
+ window.console.error = originalConsoleError;
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/function.test.js b/devtools/client/shared/components/test/node/components/reps/function.test.js
new file mode 100644
index 0000000000..b9ad70dd26
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/function.test.js
@@ -0,0 +1,584 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+const {
+ REPS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const { Func } = REPS;
+const { getFunctionName } = Func;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/function.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const renderRep = (object, props) => {
+ return shallow(Func.rep({ object, ...props }));
+};
+
+describe("Function - Named", () => {
+ // Test declaration: `function testName() { let innerVar = "foo" }`
+ const object = stubs.get("Named");
+
+ it("renders named function as expected", () => {
+ expect(
+ renderRep(object, { mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe("function testName()");
+ expect(
+ renderRep(object, { mode: undefined, shouldRenderTooltip: true }).prop(
+ "title"
+ )
+ ).toBe("function testName()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: [] },
+ { shouldRenderTooltip: true }
+ ).text()
+ ).toBe("function testName()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: [] },
+ { shouldRenderTooltip: true }
+ ).prop("title")
+ ).toBe("function testName()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a"] },
+ { shouldRenderTooltip: true }
+ ).text()
+ ).toBe("function testName(a)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a"] },
+ { shouldRenderTooltip: true }
+ ).prop("title")
+ ).toBe("function testName(a)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { shouldRenderTooltip: true }
+ ).text()
+ ).toBe("function testName(a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { shouldRenderTooltip: true }
+ ).prop("title")
+ ).toBe("function testName(a, b, c)");
+ expect(
+ renderRep(object, { mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe("testName()");
+ expect(
+ renderRep(object, { mode: MODE.TINY, shouldRenderTooltip: true }).prop(
+ "title"
+ )
+ ).toBe("testName()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { mode: MODE.TINY, shouldRenderTooltip: true }
+ ).text()
+ ).toBe("testName(a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { mode: MODE.TINY, shouldRenderTooltip: true }
+ ).prop("title")
+ ).toBe("testName(a, b, c)");
+
+ expectActorAttribute(renderRep(object), object.actor);
+ });
+});
+
+describe("Function - User named", () => {
+ // Test declaration: `function testName() { let innerVar = "foo" }`
+ const object = stubs.get("UserNamed");
+
+ it("renders user named function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(
+ "function testUserName()"
+ );
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "function testUserName()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "function testUserName(a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("function testUserName(a, b, c)");
+ expect(
+ renderRep(object, {
+ mode: MODE.TINY,
+ }).text()
+ ).toBe("testUserName()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("testUserName(a, b, c)");
+ });
+});
+
+describe("Function - Var named", () => {
+ // Test declaration: `let testVarName = function() { }`
+ const object = stubs.get("VarNamed");
+
+ it("renders var named function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(
+ "function testVarName()"
+ );
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "function testVarName()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "function testVarName(a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("function testVarName(a, b, c)");
+ expect(
+ renderRep(object, {
+ mode: MODE.TINY,
+ }).text()
+ ).toBe("testVarName()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("testVarName(a, b, c)");
+ });
+});
+
+describe("Function - Anonymous", () => {
+ // Test declaration: `() => {}`
+ const object = stubs.get("Anon");
+
+ it("renders anonymous function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe("function ()");
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "function ()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "function (a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("function (a, b, c)");
+ expect(
+ renderRep(object, {
+ mode: MODE.TINY,
+ }).text()
+ ).toBe("()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("(a, b, c)");
+ });
+});
+
+describe("Function - Long name", () => {
+ // eslint-disable-next-line max-len
+ // Test declaration: `let f = function loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong() { }`
+ const object = stubs.get("LongName");
+ const functionName =
+ "looooooooooooooooooooooooooooooooooooooooooooooo" +
+ "oo\u2026ooooooooooooooooooooooooooooooooooooooooooooooong";
+
+ it("renders long name function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(
+ `function ${functionName}()`
+ );
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ `function ${functionName}()`
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ `function ${functionName}(a)`
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe(`function ${functionName}(a, b, c)`);
+ expect(
+ renderRep(object, {
+ mode: MODE.TINY,
+ }).text()
+ ).toBe(`${functionName}()`);
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe(`${functionName}(a, b, c)`);
+ });
+});
+
+describe("Function - Async function", () => {
+ const object = stubs.get("AsyncFunction");
+
+ it("renders async function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(
+ "async function waitUntil2017()"
+ );
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe(
+ "async waitUntil2017()"
+ );
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(
+ "async function waitUntil2017()"
+ );
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(
+ "async function waitUntil2017()"
+ );
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "async function waitUntil2017()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "async function waitUntil2017(a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("async function waitUntil2017(a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("async waitUntil2017(a, b, c)");
+ });
+});
+
+describe("Function - Anonymous async function", () => {
+ const object = stubs.get("AnonAsyncFunction");
+
+ it("renders anonymous async function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(
+ "async function ()"
+ );
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe("async ()");
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(
+ "async function ()"
+ );
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(
+ "async function ()"
+ );
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "async function ()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "async function (a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("async function (a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("async (a, b, c)");
+ });
+});
+
+describe("Function - Generator function", () => {
+ const object = stubs.get("GeneratorFunction");
+
+ it("renders generator function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(
+ "function* fib()"
+ );
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe("* fib()");
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(
+ "function* fib()"
+ );
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(
+ "function* fib()"
+ );
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "function* fib()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "function* fib(a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("function* fib(a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("* fib(a, b, c)");
+ });
+});
+
+describe("Function - Anonymous generator function", () => {
+ const object = stubs.get("AnonGeneratorFunction");
+
+ it("renders anonymous generator function as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe("function* ()");
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe("* ()");
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe("function* ()");
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe("function* ()");
+ expect(renderRep({ ...object, parameterNames: [] }).text()).toBe(
+ "function* ()"
+ );
+ expect(renderRep({ ...object, parameterNames: ["a"] }).text()).toBe(
+ "function* (a)"
+ );
+ expect(
+ renderRep({ ...object, parameterNames: ["a", "b", "c"] }).text()
+ ).toBe("function* (a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ }
+ ).text()
+ ).toBe("* (a, b, c)");
+ });
+});
+
+describe("Function - Jump to definition", () => {
+ it("renders an icon when onViewSourceInDebugger props is provided", async () => {
+ let onViewSourceInDebugger;
+ const onViewSourceCalled = new Promise(resolve => {
+ onViewSourceInDebugger = jest.fn(resolve);
+ });
+ const object = stubs.get("getRandom");
+ const renderedComponent = renderRep(object, {
+ onViewSourceInDebugger,
+ });
+
+ const node = renderedComponent.find(".jump-definition");
+ node.simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+ await onViewSourceCalled;
+
+ expect(node.exists()).toBeTruthy();
+ expect(onViewSourceInDebugger.mock.calls).toHaveLength(1);
+ expect(onViewSourceInDebugger.mock.calls[0][0]).toEqual(object.location);
+ });
+
+ it("calls recordTelemetryEvent when jump to definition icon clicked", () => {
+ const onViewSourceInDebugger = jest.fn();
+ const recordTelemetryEvent = jest.fn();
+ const object = stubs.get("getRandom");
+ const renderedComponent = renderRep(object, {
+ onViewSourceInDebugger,
+ recordTelemetryEvent,
+ });
+
+ const node = renderedComponent.find(".jump-definition");
+ node.simulate("click", {
+ type: "click",
+ stopPropagation: () => {},
+ });
+
+ expect(node.exists()).toBeTruthy();
+ expect(recordTelemetryEvent.mock.calls).toHaveLength(1);
+ expect(recordTelemetryEvent.mock.calls[0][0]).toEqual("jump_to_definition");
+ });
+
+ it("no icon when onViewSourceInDebugger props not provided", () => {
+ const object = stubs.get("getRandom");
+ const renderedComponent = renderRep(object);
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+
+ it("does not render an icon when the object has no location", () => {
+ const object = {
+ ...stubs.get("getRandom"),
+ location: null,
+ };
+
+ const renderedComponent = renderRep(object, {
+ onViewSourceInDebugger: () => {},
+ });
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+
+ it("does not render an icon when the object has no url location", () => {
+ const object = {
+ ...stubs.get("getRandom"),
+ };
+ object.location = { ...object.location, url: null };
+ const renderedComponent = renderRep(object, {
+ onViewSourceInDebugger: () => {},
+ });
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+
+ it("no icon when function was declared in console input", () => {
+ const object = stubs.get("EvaledInDebuggerFunction");
+ const renderedComponent = renderRep(object, {
+ onViewSourceInDebugger: () => {},
+ });
+
+ const node = renderedComponent.find(".jump-definition");
+ expect(node.exists()).toBeFalsy();
+ });
+});
+
+describe("Function - Simplify name", () => {
+ const cases = {
+ defaultCase: [["define", "define"]],
+
+ objectProperty: [
+ ["z.foz", "foz"],
+ ["z.foz/baz", "baz"],
+ ["z.foz/baz/y.bay", "bay"],
+ ["outer/x.fox.bax.nx", "nx"],
+ ["outer/fow.baw", "baw"],
+ ["fromYUI._attach", "_attach"],
+ ["Y.ClassNameManager</getClassName", "getClassName"],
+ ["orion.textview.TextView</addHandler", "addHandler"],
+ ["this.eventPool_.createObject", "createObject"],
+ ],
+
+ arrayProperty: [
+ ["this.eventPool_[createObject]", "createObject"],
+ ["jQuery.each(^)/jQuery.fn[o]", "o"],
+ ["viewport[get+D]", "get+D"],
+ ["arr[0]", "0"],
+ ],
+
+ functionProperty: [
+ ["fromYUI._attach/<.", "_attach"],
+ ["Y.ClassNameManager<", "ClassNameManager"],
+ ["fromExtJS.setVisible/cb<", "cb"],
+ ["fromDojo.registerWin/<", "registerWin"],
+ ],
+
+ annonymousProperty: [["jQuery.each(^)", "each"]],
+ };
+
+ Object.keys(cases).forEach(type => {
+ for (const [kase, expected] of cases[type]) {
+ it(`${type} - ${kase}`, () =>
+ expect(getFunctionName({ displayName: kase })).toEqual(expected));
+ }
+ });
+});
+describe("Function - Two properties with same displayName", () => {
+ const object = stubs.get("ObjectProperty");
+
+ it("renders object properties as expected", () => {
+ expect(
+ renderRep(object, { mode: undefined, functionName: "$" }).text()
+ ).toBe("function $:jQuery()");
+ expect(
+ renderRep({ ...object, parameterNames: [] }, { functionName: "$" }).text()
+ ).toBe("function $:jQuery()");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a"] },
+ { functionName: "$" }
+ ).text()
+ ).toBe("function $:jQuery(a)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ functionName: "$",
+ }
+ ).text()
+ ).toBe("function $:jQuery(a, b, c)");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ {
+ mode: MODE.TINY,
+ functionName: "$",
+ }
+ ).text()
+ ).toBe("$:jQuery(a, b, c)");
+ });
+});
+
+describe("Function - Class constructor", () => {
+ const object = stubs.get("EmptyClass");
+
+ it("renders empty class as expected", () => {
+ expect(
+ renderRep(object, { mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe("class EmptyClass {}");
+ expect(
+ renderRep(object, { mode: undefined, shouldRenderTooltip: true }).prop(
+ "title"
+ )
+ ).toBe("class EmptyClass {}");
+ });
+
+ it("renders empty class in MODE.TINY as expected", () => {
+ expect(
+ renderRep(object, { mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe("class EmptyClass");
+ expect(
+ renderRep(object, { mode: MODE.TINY, shouldRenderTooltip: true }).prop(
+ "title"
+ )
+ ).toBe("class EmptyClass");
+ });
+
+ it("renders class with constructor as expected", () => {
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { shouldRenderTooltip: true }
+ ).text()
+ ).toBe("class EmptyClass { constructor(a, b, c) }");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { shouldRenderTooltip: true }
+ ).prop("title")
+ ).toBe("class EmptyClass { constructor(a, b, c) }");
+ });
+
+ it("renders class with constructor in MODE.TINY as expected", () => {
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { mode: MODE.TINY, shouldRenderTooltip: true }
+ ).text()
+ ).toBe("class EmptyClass");
+ expect(
+ renderRep(
+ { ...object, parameterNames: ["a", "b", "c"] },
+ { mode: MODE.TINY, shouldRenderTooltip: true }
+ ).prop("title")
+ ).toBe("class EmptyClass");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/grip-array.test.js b/devtools/client/shared/components/test/node/components/reps/grip-array.test.js
new file mode 100644
index 0000000000..a674c4275d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/grip-array.test.js
@@ -0,0 +1,705 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+const {
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const GripArray = require("resource://devtools/client/shared/components/reps/reps/grip-array.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const {
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+ getGripLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const { maxLengthMap } = GripArray;
+
+function shallowRenderRep(object, props = {}) {
+ return shallow(
+ GripArray.rep({
+ object,
+ ...props,
+ })
+ );
+}
+
+describe("GripArray - basic", () => {
+ const object = stubs.get("testBasic");
+
+ it("correctly selects GripArray Rep", () => {
+ expect(getRep(object)).toBe(GripArray.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+ const defaultOutput = `Array${length} []`;
+
+ let component = renderRep({ mode: undefined, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe("Array");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY, shouldRenderTooltip: true });
+ expect(component.text()).toBe("[]");
+ expect(component.prop("title")).toBe("Array");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe("Array");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe("Array");
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("GripArray - max props", () => {
+ const object = stubs.get("testMaxProps");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput = `Array${length} [ 1, "foo", {} ]`;
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ // Check the custom title with nested objects to make sure nested objects
+ // are not displayed with their parent's title.
+ expect(
+ renderRep({
+ mode: MODE.LONG,
+ title: "CustomTitle",
+ }).text()
+ ).toBe(`CustomTitle${length} [ 1, "foo", {} ]`);
+ });
+});
+
+describe("GripArray - more than short mode max props", () => {
+ const object = stubs.get("testMoreThanShortMaxProps");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ const shortLength = maxLengthMap.get(MODE.SHORT);
+ const shortContent = Array(shortLength).fill('"test string"').join(", ");
+ const longContent = Array(shortLength + 1)
+ .fill('"test string"')
+ .join(", ");
+ const defaultOutput = `Array${length} [ ${shortContent}, … ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ `Array${length} [ ${longContent} ]`
+ );
+ });
+});
+
+describe("GripArray - more than long mode max props", () => {
+ const object = stubs.get("testMoreThanLongMaxProps");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const shortLength = maxLengthMap.get(MODE.SHORT);
+ const longLength = maxLengthMap.get(MODE.LONG);
+ const shortContent = Array(shortLength).fill('"test string"').join(", ");
+ const defaultOutput = `Array${length} [ ${shortContent}, … ]`;
+ const longContent = Array(longLength).fill('"test string"').join(", ");
+
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Array");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe(`${length} […]`);
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Array");
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Array");
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).text()
+ ).toBe(`Array${length} [ ${longContent}, … ]`);
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Array");
+ });
+});
+
+describe("GripArray - recursive array", () => {
+ const object = stubs.get("testRecursiveArray");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+ const childArrayLength = getGripLengthBubbleText(object.preview.items[0], {
+ mode: MODE.TINY,
+ });
+
+ const defaultOutput = `Array${length} [ ${childArrayLength} […] ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("GripArray - preview limit", () => {
+ const object = stubs.get("testPreviewLimit");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const shortOutput = `Array${length} [ 0, 1, 2, … ]`;
+ const longOutput = `Array${length} [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, … ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(shortOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(shortOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
+
+describe("GripArray - empty slots", () => {
+ it("renders an array with empty slots only as expected", () => {
+ const object = stubs.get("Array(5)");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput = `Array${length} [ <5 empty slots> ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput = `Array${length} [ <5 empty slots> ]`;
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("one empty slot at the beginning as expected", () => {
+ const object = stubs.get("[,1,2,3]");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ <1 empty slot>, 1, 2, … ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput = `Array${length} [ <1 empty slot>, 1, 2, 3 ]`;
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("multiple consecutive empty slots at the beginning as expected", () => {
+ const object = stubs.get("[,,,3,4,5]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ <3 empty slots>, 3, 4, … ]`;
+ const longOutput = `Array${length} [ <3 empty slots>, 3, 4, 5 ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("one empty slot in the middle as expected", () => {
+ const object = stubs.get("[0,1,,3,4,5]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ 0, 1, <1 empty slot>, … ]`;
+ const longOutput = `Array${length} [ 0, 1, <1 empty slot>, 3, 4, 5 ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("successive empty slots in the middle as expected", () => {
+ const object = stubs.get("[0,1,,,,5]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ 0, 1, <3 empty slots>, … ]`;
+ const longOutput = `Array${length} [ 0, 1, <3 empty slots>, 5 ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("non successive single empty slots as expected", () => {
+ const object = stubs.get("[0,,2,,4,5]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ 0, <1 empty slot>, 2, … ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ `Array${length} [ 0, <1 empty slot>, 2, <1 empty slot>, 4, 5 ]`
+ );
+ });
+
+ it("multiple multi-slot holes as expected", () => {
+ const object = stubs.get("[0,,,3,,,,7,8]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ 0, <2 empty slots>, 3, … ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ `Array${length} [ 0, <2 empty slots>, 3, <3 empty slots>, 7, 8 ]`
+ );
+ });
+
+ it("a single slot hole at the end as expected", () => {
+ const object = stubs.get("[0,1,2,3,4,,]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ 0, 1, 2, … ]`;
+ const longOutput = `Array${length} [ 0, 1, 2, 3, 4, <1 empty slot> ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("multiple consecutive empty slots at the end as expected", () => {
+ const object = stubs.get("[0,1,2,,,,]");
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ 0, 1, 2, … ]`;
+ const longOutput = `Array${length} [ 0, 1, 2, <3 empty slots> ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
+
+describe("GripArray - NamedNodeMap", () => {
+ const object = stubs.get("testNamedNodeMap");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ const defaultOutput =
+ `NamedNodeMap${length} ` +
+ '[ class="myclass", cellpadding="7", border="3" ]';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`NamedNodeMap${length}`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("GripArray - NodeList", () => {
+ const object = stubs.get("testNodeList");
+ const grips = getSelectableInInspectorGrips(object);
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ it("renders as expected", () => {
+ const defaultOutput =
+ `NodeList${length} [ button#btn-1.btn.btn-log, ` +
+ "button#btn-2.btn.btn-err, button#btn-3.btn.btn-count ]";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`NodeList${length}`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+
+ it("has 3 node grip", () => {
+ expect(grips).toHaveLength(3);
+ });
+
+ it("calls the expected function on mouseover", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOver });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseover");
+ node.at(1).simulate("mouseover");
+ node.at(2).simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(3);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOver.mock.calls[1][0]).toBe(grips[1]);
+ expect(onDOMNodeMouseOver.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("calls the expected function on mouseout", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOut });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseout");
+ node.at(1).simulate("mouseout");
+ node.at(2).simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(3);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOut.mock.calls[1][0]).toBe(grips[1]);
+ expect(onDOMNodeMouseOut.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("calls the expected function on click", () => {
+ const onInspectIconClick = jest.fn();
+ const wrapper = renderRep({ onInspectIconClick });
+ const node = wrapper.find(".open-inspector");
+
+ node.at(0).simulate("click");
+ node.at(1).simulate("click");
+ node.at(2).simulate("click");
+
+ expect(onInspectIconClick.mock.calls).toHaveLength(3);
+ expect(onInspectIconClick.mock.calls[0][0]).toBe(grips[0]);
+ expect(onInspectIconClick.mock.calls[1][0]).toBe(grips[1]);
+ expect(onInspectIconClick.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("no inspect icon when nodes are not connected to the DOM tree", () => {
+ const renderedComponentWithoutInspectIcon = shallowRenderRep(
+ stubs.get("testDisconnectedNodeList")
+ );
+ const node = renderedComponentWithoutInspectIcon.find(".open-inspector");
+ expect(node.exists()).toBe(false);
+ });
+});
+
+describe("GripArray - DocumentFragment", () => {
+ const object = stubs.get("testDocumentFragment");
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput =
+ `DocumentFragment${length} [ li#li-0.list-element, ` +
+ "li#li-1.list-element, li#li-2.list-element, … ]";
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(
+ `DocumentFragment${length}`
+ );
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput =
+ `DocumentFragment${length} [ ` +
+ "li#li-0.list-element, li#li-1.list-element, li#li-2.list-element, " +
+ "li#li-3.list-element, li#li-4.list-element ]";
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
+
+describe("GripArray - Items not in preview", () => {
+ const object = stubs.get("testItemsNotInPreview");
+
+ it("correctly selects GripArray Rep", () => {
+ expect(getRep(object)).toBe(GripArray.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput = `Array${length} [ … ]`;
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe(`${length} […]`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("GripArray - Set", () => {
+ it("correctly selects GripArray Rep", () => {
+ const object = stubs.get("new Set([1,2,3,4])");
+ expect(getRep(object)).toBe(GripArray.rep);
+ });
+
+ it("renders short sets as expected", () => {
+ const object = stubs.get("new Set([1,2,3,4])");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput = `Set${length} [ 1, 2, 3, … ]`;
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe(`Set${length}`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(`Set${length} [ 1, 2, 3, 4 ]`);
+ expectActorAttribute(component, object.actor);
+ });
+
+ it("renders larger sets as expected", () => {
+ const object = stubs.get("new Set([0,1,2,…,19])");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput = `Set${length} [ 0, 1, 2, … ]`;
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe(`Set${length}`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(
+ `Set${length} [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, … ]`
+ );
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("GripArray - WeakSet", () => {
+ it("correctly selects GripArray Rep", () => {
+ const object = stubs.get(
+ "new WeakSet(document.querySelectorAll('button:nth-child(3n)'))"
+ );
+ expect(getRep(object)).toBe(GripArray.rep);
+ });
+
+ it("renders short WeakSets as expected", () => {
+ const object = stubs.get(
+ "new WeakSet(document.querySelectorAll('button:nth-child(3n)'))"
+ );
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+ const defaultOutput = `WeakSet${length} [ button, button, button, … ]`;
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe(`WeakSet${length}`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(
+ `WeakSet${length} [ button, button, button, button ]`
+ );
+ expectActorAttribute(component, object.actor);
+ });
+
+ it("renders larger WeakSets as expected", () => {
+ const object = stubs.get(
+ "new WeakSet(document.querySelectorAll('div, button'))"
+ );
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+ const defaultOutput = `WeakSet${length} [ button, button, button, … ]`;
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe(`WeakSet${length}`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(`WeakSet(12) [ ${"button, ".repeat(10)}… ]`);
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("GripArray - DOMTokenList", () => {
+ const object = stubs.get("DOMTokenList");
+
+ it("correctly selects GripArray Rep", () => {
+ expect(getRep(object)).toBe(GripArray.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getGripLengthBubbleText(object);
+ const defaultOutput = `DOMTokenList${length} []`;
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("GripArray - accessor", () => {
+ it("renders an array with getter as expected", () => {
+ const object = stubs.get("TestArrayWithGetter");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ Getter ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput = `Array${length} [ Getter ]`;
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("renders an array with setter as expected", () => {
+ const object = stubs.get("TestArrayWithSetter");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ Setter ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput = `Array${length} [ Setter ]`;
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+
+ it("renders an array with getter and setter as expected", () => {
+ const object = stubs.get("TestArrayWithGetterAndSetter");
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getGripLengthBubbleText(object);
+
+ const defaultOutput = `Array${length} [ Getter & Setter ]`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`${length} […]`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput = `Array${length} [ Getter & Setter ]`;
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/grip-entry.test.js b/devtools/client/shared/components/test/node/components/reps/grip-entry.test.js
new file mode 100644
index 0000000000..cdfcab755e
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/grip-entry.test.js
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { GripEntry } = REPS;
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ createGripMapEntry,
+ getGripLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-entry.js");
+const nodeStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/element-node.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+
+const renderRep = (object, mode, props) => {
+ return shallow(
+ GripEntry.rep({
+ object,
+ mode,
+ ...props,
+ })
+ );
+};
+
+describe("GripEntry - simple", () => {
+ const stub = stubs.get("A → 0");
+
+ it("Rep correctly selects GripEntry Rep", () => {
+ expect(getRep(stub)).toBe(GripEntry.rep);
+ });
+
+ it("GripEntry rep has expected text content", () => {
+ const renderedComponent = renderRep(stub);
+ expect(renderedComponent.text()).toEqual("A → 0");
+ });
+});
+
+describe("GripEntry - createGripMapEntry", () => {
+ it("return the expected object", () => {
+ const entry = createGripMapEntry("A", 0);
+ expect(entry).toEqual(stubs.get("A → 0"));
+ });
+});
+
+describe("GripEntry - complex", () => {
+ it("Handles complex objects as key and value", () => {
+ let stub = gripArrayStubs.get("testBasic");
+ let length = getGripLengthBubbleText(stub);
+ let entry = createGripMapEntry("A", stub);
+ expect(renderRep(entry, MODE.TINY).text()).toEqual("A → []");
+ expect(renderRep(entry, MODE.SHORT).text()).toEqual(
+ `A → Array${length} []`
+ );
+ expect(renderRep(entry, MODE.LONG).text()).toEqual(`A → Array${length} []`);
+
+ entry = createGripMapEntry(stub, "A");
+ expect(renderRep(entry, MODE.TINY).text()).toEqual('[] → "A"');
+ expect(renderRep(entry, MODE.SHORT).text()).toEqual(
+ `Array${length} [] → "A"`
+ );
+ expect(renderRep(entry, MODE.LONG).text()).toEqual(
+ `Array${length} [] → "A"`
+ );
+
+ stub = gripArrayStubs.get("testMaxProps");
+ length = getGripLengthBubbleText(stub, { mode: MODE.TINY });
+ entry = createGripMapEntry("A", stub);
+ expect(renderRep(entry, MODE.TINY).text()).toEqual(`A → ${length} […]`);
+ length = getGripLengthBubbleText(stub);
+ expect(renderRep(entry, MODE.SHORT).text()).toEqual(
+ `A → Array${length} [ 1, "foo", {} ]`
+ );
+ length = getGripLengthBubbleText(stub, { mode: MODE.LONG });
+ expect(renderRep(entry, MODE.LONG).text()).toEqual(
+ `A → Array${length} [ 1, "foo", {} ]`
+ );
+
+ entry = createGripMapEntry(stub, "A");
+ length = getGripLengthBubbleText(stub, { mode: MODE.TINY });
+ expect(renderRep(entry, MODE.TINY).text()).toEqual(`${length} […] → "A"`);
+ length = getGripLengthBubbleText(stub, { mode: MODE.SHORT });
+ expect(renderRep(entry, MODE.SHORT).text()).toEqual(
+ `Array${length} [ 1, "foo", {} ] → "A"`
+ );
+ length = getGripLengthBubbleText(stub, { mode: MODE.LONG });
+ expect(renderRep(entry, MODE.LONG).text()).toEqual(
+ `Array${length} [ 1, "foo", {} ] → "A"`
+ );
+
+ stub = gripArrayStubs.get("testMoreThanShortMaxProps");
+ length = getGripLengthBubbleText(stub);
+ entry = createGripMapEntry("A", stub);
+ length = getGripLengthBubbleText(stub, { mode: MODE.TINY });
+ expect(renderRep(entry, MODE.TINY).text()).toEqual(`A → ${length} […]`);
+ length = getGripLengthBubbleText(stub, { mode: MODE.SHORT });
+ expect(renderRep(entry, MODE.SHORT).text()).toEqual(
+ `A → Array${length} [ "test string", "test string", "test string", … ]`
+ );
+ length = getGripLengthBubbleText(stub, { mode: MODE.LONG });
+ expect(renderRep(entry, MODE.LONG).text()).toEqual(
+ `A → Array${length} [ "test string", "test string", "test string",\
+ "test string" ]`
+ );
+
+ entry = createGripMapEntry(stub, "A");
+ length = getGripLengthBubbleText(stub, { mode: MODE.TINY });
+ expect(renderRep(entry, MODE.TINY).text()).toEqual(`${length} […] → "A"`);
+ length = getGripLengthBubbleText(stub, { mode: MODE.SHORT });
+ expect(renderRep(entry, MODE.SHORT).text()).toEqual(
+ `Array${length} [ "test string", "test string", "test string", … ] → "A"`
+ );
+ length = getGripLengthBubbleText(stub, { mode: MODE.LONG });
+ expect(renderRep(entry, MODE.LONG).text()).toEqual(
+ `Array${length} [ "test string", "test string", "test string", ` +
+ '"test string" ] → "A"'
+ );
+ });
+
+ it("Handles Element Nodes as key and value", () => {
+ const stub = nodeStubs.get("Node");
+
+ const onInspectIconClick = jest.fn();
+ const onDOMNodeMouseOut = jest.fn();
+ const onDOMNodeMouseOver = jest.fn();
+
+ let entry = createGripMapEntry("A", stub);
+ let renderedComponent = renderRep(entry, MODE.TINY, {
+ onInspectIconClick,
+ onDOMNodeMouseOut,
+ onDOMNodeMouseOver,
+ });
+ expect(renderRep(entry, MODE.TINY).text()).toEqual(
+ "A → input#newtab-customize-button.bar.baz"
+ );
+
+ let node = renderedComponent.find(".objectBox-node");
+ let icon = node.find(".open-inspector");
+ icon.simulate("click", { type: "click" });
+ expect(icon.exists()).toBeTruthy();
+ expect(onInspectIconClick.mock.calls).toHaveLength(1);
+ expect(onInspectIconClick.mock.calls[0][0]).toEqual(stub);
+ expect(onInspectIconClick.mock.calls[0][1].type).toEqual("click");
+
+ node.simulate("mouseout");
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toEqual(stub);
+
+ node.simulate("mouseover");
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toEqual(stub);
+
+ entry = createGripMapEntry(stub, "A");
+ renderedComponent = renderRep(entry, MODE.TINY, {
+ onInspectIconClick,
+ onDOMNodeMouseOut,
+ onDOMNodeMouseOver,
+ });
+ expect(renderRep(entry, MODE.TINY).text()).toEqual(
+ 'input#newtab-customize-button.bar.baz → "A"'
+ );
+
+ node = renderedComponent.find(".objectBox-node");
+ icon = node.find(".open-inspector");
+ icon.simulate("click", { type: "click" });
+ expect(node.exists()).toBeTruthy();
+ expect(onInspectIconClick.mock.calls).toHaveLength(2);
+ expect(onInspectIconClick.mock.calls[1][0]).toEqual(stub);
+ expect(onInspectIconClick.mock.calls[1][1].type).toEqual("click");
+
+ node.simulate("mouseout");
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(2);
+ expect(onDOMNodeMouseOut.mock.calls[1][0]).toEqual(stub);
+
+ node.simulate("mouseover");
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(2);
+ expect(onDOMNodeMouseOver.mock.calls[1][0]).toEqual(stub);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/grip-map.test.js b/devtools/client/shared/components/test/node/components/reps/grip-map.test.js
new file mode 100644
index 0000000000..5ef582d56f
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/grip-map.test.js
@@ -0,0 +1,377 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+
+const { shallow } = require("enzyme");
+const {
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const GripMap = require("resource://devtools/client/shared/components/reps/reps/grip-map.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+const {
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+ getMapLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const { maxLengthMap, getLength } = GripMap;
+
+function shallowRenderRep(object, props = {}) {
+ return shallow(
+ GripMap.rep({
+ object,
+ ...props,
+ })
+ );
+}
+
+describe("GripMap - empty map", () => {
+ const object = stubs.get("testEmptyMap");
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const length = getMapLengthBubbleText(object);
+ const defaultOutput = `Map${length}`;
+
+ let component = renderRep({ mode: undefined, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(`Map(${getLength(object)})`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(`Map(${getLength(object)})`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(`Map(${getLength(object)})`);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG, shouldRenderTooltip: true });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(`Map(${getLength(object)})`);
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("GripMap - Symbol-keyed Map", () => {
+ // Test object:
+ // `new Map([[Symbol("a"), "value-a"], [Symbol("b"), "value-b"]])`
+ const object = stubs.get("testSymbolKeyedMap");
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getMapLengthBubbleText(object);
+ const out = `Map${length} { Symbol("a") → "value-a", Symbol("b") → "value-b" }`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(out);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`Map${length}`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(out);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(out);
+ });
+});
+
+describe("GripMap - WeakMap", () => {
+ // Test object: `new WeakMap([[{a: "key-a"}, "value-a"]])`
+ const object = stubs.get("testWeakMap");
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getMapLengthBubbleText(object);
+ const defaultOutput = `WeakMap${length} { {…} → "value-a" }`;
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).prop("title")
+ ).toBe(`WeakMap(${getLength(object)})`);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.TINY });
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe(`WeakMap${length}`);
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).prop("title")
+ ).toBe(`WeakMap(${getLength(object)})`);
+
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).prop("title")
+ ).toBe(`WeakMap(${getLength(object)})`);
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).prop("title")
+ ).toBe(`WeakMap(${getLength(object)})`);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.LONG });
+
+ expect(
+ renderRep({
+ mode: MODE.LONG,
+ title: "CustomTitle",
+ }).text()
+ ).toBe(`CustomTitle${length} { {…} → "value-a" }`);
+ expect(
+ renderRep({
+ mode: MODE.LONG,
+ title: "CustomTitle",
+ shouldRenderTooltip: true,
+ }).prop("title")
+ ).toBe(`CustomTitle(${getLength(object)})`);
+ });
+});
+
+describe("GripMap - max entries", () => {
+ // Test object:
+ // `new Map([["key-a","value-a"], ["key-b","value-b"], ["key-c","value-c"]])`
+ const object = stubs.get("testMaxEntries");
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("renders as expected", () => {
+ let length = getMapLengthBubbleText(object);
+ const renderRep = props => shallowRenderRep(object, props);
+ const out =
+ `Map${length} { ` +
+ '"key-a" → "value-a", "key-b" → "value-b", "key-c" → "value-c" }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(out);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`Map${length}`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(out);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(out);
+ });
+});
+
+describe("GripMap - more than max entries", () => {
+ // Test object = `new Map(
+ // [["key-0", "value-0"], …, ["key-100", "value-100"]]}`
+ const object = stubs.get("testMoreThanMaxEntries");
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getMapLengthBubbleText(object);
+ const defaultOutput =
+ `Map${length} { "key-0" → "value-0", ` +
+ '"key-1" → "value-1", "key-2" → "value-2", … }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`Map${length}`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ const longString = Array.from({ length: maxLengthMap.get(MODE.LONG) }).map(
+ (_, i) => `"key-${i}" → "value-${i}"`
+ );
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ `Map(${maxLengthMap.get(MODE.LONG) + 1}) { ${longString.join(", ")}, … }`
+ );
+ });
+});
+
+describe("GripMap - uninteresting entries", () => {
+ // Test object:
+ // `new Map([["key-a",null], ["key-b",undefined], ["key-c","value-c"],
+ // ["key-d",4]])`
+ const object = stubs.get("testUninterestingEntries");
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ let length = getMapLengthBubbleText(object);
+ const defaultOutput =
+ `Map${length} { "key-a" → null, ` +
+ '"key-c" → "value-c", "key-d" → 4, … }';
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.TINY });
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe(`Map${length}`);
+
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ length = getMapLengthBubbleText(object, { mode: MODE.LONG });
+ const longOutput =
+ `Map${length} { "key-a" → null, "key-b" → undefined, ` +
+ '"key-c" → "value-c", "key-d" → 4 }';
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
+
+describe("GripMap - Node-keyed entries", () => {
+ const object = stubs.get("testNodeKeyedMap");
+ const renderRep = props => shallowRenderRep(object, props);
+ const grips = getSelectableInInspectorGrips(object);
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("has the expected number of grips", () => {
+ expect(grips).toHaveLength(3);
+ });
+
+ it("calls the expected function on mouseover", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOver });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseover");
+ node.at(1).simulate("mouseover");
+ node.at(2).simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(3);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOver.mock.calls[1][0]).toBe(grips[1]);
+ expect(onDOMNodeMouseOver.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("calls the expected function on mouseout", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOut });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseout");
+ node.at(1).simulate("mouseout");
+ node.at(2).simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(3);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOut.mock.calls[1][0]).toBe(grips[1]);
+ expect(onDOMNodeMouseOut.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("calls the expected function on click", () => {
+ const onInspectIconClick = jest.fn();
+ const wrapper = renderRep({ onInspectIconClick });
+ const node = wrapper.find(".open-inspector");
+
+ node.at(0).simulate("click");
+ node.at(1).simulate("click");
+ node.at(2).simulate("click");
+
+ expect(onInspectIconClick.mock.calls).toHaveLength(3);
+ expect(onInspectIconClick.mock.calls[0][0]).toBe(grips[0]);
+ expect(onInspectIconClick.mock.calls[1][0]).toBe(grips[1]);
+ expect(onInspectIconClick.mock.calls[2][0]).toBe(grips[2]);
+ });
+});
+
+describe("GripMap - Node-valued entries", () => {
+ const object = stubs.get("testNodeValuedMap");
+ const renderRep = props => shallowRenderRep(object, props);
+ const grips = getSelectableInInspectorGrips(object);
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("has the expected number of grips", () => {
+ expect(grips).toHaveLength(3);
+ });
+
+ it("calls the expected function on mouseover", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOver });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseover");
+ node.at(1).simulate("mouseover");
+ node.at(2).simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(3);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOver.mock.calls[1][0]).toBe(grips[1]);
+ expect(onDOMNodeMouseOver.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("calls the expected function on mouseout", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOut });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseout");
+ node.at(1).simulate("mouseout");
+ node.at(2).simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(3);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOut.mock.calls[1][0]).toBe(grips[1]);
+ expect(onDOMNodeMouseOut.mock.calls[2][0]).toBe(grips[2]);
+ });
+
+ it("calls the expected function on click", () => {
+ const onInspectIconClick = jest.fn();
+ const wrapper = renderRep({ onInspectIconClick });
+ const node = wrapper.find(".open-inspector");
+
+ node.at(0).simulate("click");
+ node.at(1).simulate("click");
+ node.at(2).simulate("click");
+
+ expect(onInspectIconClick.mock.calls).toHaveLength(3);
+ expect(onInspectIconClick.mock.calls[0][0]).toBe(grips[0]);
+ expect(onInspectIconClick.mock.calls[1][0]).toBe(grips[1]);
+ expect(onInspectIconClick.mock.calls[2][0]).toBe(grips[2]);
+ });
+});
+
+describe("GripMap - Disconnected node-valued entries", () => {
+ const object = stubs.get("testDisconnectedNodeValuedMap");
+ const renderRep = props => shallowRenderRep(object, props);
+ const grips = getSelectableInInspectorGrips(object);
+
+ it("correctly selects GripMap Rep", () => {
+ expect(getRep(object)).toBe(GripMap.rep);
+ });
+
+ it("has the expected number of grips", () => {
+ expect(grips).toHaveLength(3);
+ });
+
+ it("no inspect icon when nodes are not connected to the DOM tree", () => {
+ const onInspectIconClick = jest.fn();
+ const wrapper = renderRep({ onInspectIconClick });
+
+ const node = wrapper.find(".open-inspector");
+ expect(node.exists()).toBe(false);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/grip.test.js b/devtools/client/shared/components/test/node/components/reps/grip.test.js
new file mode 100644
index 0000000000..7debeead3b
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/grip.test.js
@@ -0,0 +1,705 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+
+const { shallow } = require("enzyme");
+const {
+ getRep,
+ Rep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const Grip = require("resource://devtools/client/shared/components/reps/reps/grip.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+const gripArrayStubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+
+const {
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+ getGripLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const { maxLengthMap } = Grip;
+
+function shallowRenderRep(object, props = {}) {
+ return shallow(
+ Grip.rep({
+ object,
+ ...props,
+ })
+ );
+}
+
+describe("Grip - empty object", () => {
+ // Test object: `{}`
+ const object = stubs.get("testBasic");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { }";
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe("{}");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("Grip - Boolean object", () => {
+ // Test object: `new Boolean(true)`
+ const object = stubs.get("testBooleanObject");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Boolean { true }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("Boolean");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Number object", () => {
+ // Test object: `new Number(42)`
+ const object = stubs.get("testNumberObject");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Number { 42 }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("Number");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - String object", () => {
+ // Test object: `new String("foo")`
+ const object = stubs.get("testStringObject");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = 'String { "foo" }';
+
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).prop("title")
+ ).toBe("String");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe("String");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).prop("title")
+ ).toBe("String");
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).prop("title")
+ ).toBe("String");
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).prop("title")
+ ).toBe("String");
+ });
+});
+
+describe("Grip - Proxy", () => {
+ // Test object: `new Proxy({a:1},[1,2,3])`
+ const object = stubs.get("testProxy");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const handler = object.preview.ownProperties["<handler>"].value;
+ const handlerLength = getGripLengthBubbleText(handler, {
+ mode: MODE.TINY,
+ });
+ const out = `Proxy { <target>: {…}, <handler>: ${handlerLength} […] }`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(out);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("Proxy");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(out);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(out);
+ });
+});
+
+describe("Grip - ArrayBuffer", () => {
+ // Test object: `new ArrayBuffer(10)`
+ const object = stubs.get("testArrayBuffer");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "ArrayBuffer { byteLength: 10 }";
+
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).prop("title")
+ ).toBe("ArrayBuffer");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe("ArrayBuffer");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).prop("title")
+ ).toBe("ArrayBuffer");
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).prop("title")
+ ).toBe("ArrayBuffer");
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).prop("title")
+ ).toBe("ArrayBuffer");
+ });
+});
+
+describe("Grip - SharedArrayBuffer", () => {
+ // Test object: `new SharedArrayBuffer(5)`
+ const object = stubs.get("testSharedArrayBuffer");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "SharedArrayBuffer { byteLength: 5 }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("SharedArrayBuffer");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - ApplicationCache", () => {
+ // Test object: `window.applicationCache`
+ const object = stubs.get("testApplicationCache");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput =
+ "OfflineResourceList { status: 0, onchecking: null, onerror: null, … }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("OfflineResourceList");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ const longOutput =
+ "OfflineResourceList { status: 0, onchecking: null, " +
+ "onerror: null, onnoupdate: null, ondownloading: null, " +
+ "onprogress: null, onupdateready: null, oncached: null, " +
+ "onobsolete: null, mozItems: DOMStringList [] }";
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
+
+describe("Grip - Object with max props", () => {
+ // Test object: `{a: "a", b: "b", c: "c"}`
+ const object = stubs.get("testMaxProps");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = 'Object { a: "a", b: "b", c: "c" }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Object with more than short mode max props", () => {
+ // Test object: `{a: undefined, b: 1, more: 2, d: 3}`;
+ const object = stubs.get("testMoreProp");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { b: 1, more: 2, d: 3, … }";
+
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: undefined, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Object");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).text()
+ ).toBe("{…}");
+ expect(
+ renderRep({ mode: MODE.TINY, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Object");
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).text()
+ ).toBe(defaultOutput);
+ expect(
+ renderRep({ mode: MODE.SHORT, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Object");
+
+ const longOutput = "Object { a: undefined, b: 1, more: 2, d: 3 }";
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).text()
+ ).toBe(longOutput);
+ expect(
+ renderRep({ mode: MODE.LONG, shouldRenderTooltip: true }).prop("title")
+ ).toBe("Object");
+ });
+});
+
+describe("Grip - Object with more than long mode max props", () => {
+ // Test object = `{p0: "0", p1: "1", p2: "2", …, p100: "100"}`
+ const object = stubs.get("testMoreThanMaxProps");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = 'Object { p0: "0", p1: "1", p2: "2", … }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+
+ const props = Array.from({ length: maxLengthMap.get(MODE.LONG) }).map(
+ (item, i) => `p${i}: "${i}"`
+ );
+ const longOutput = `Object { ${props.join(", ")}, … }`;
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(longOutput);
+ });
+});
+
+describe("Grip - Object with non-enumerable properties", () => {
+ // Test object: `Object.defineProperty({}, "foo", {enumerable : false});`
+ const object = stubs.get("testNonEnumerableProps");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { … }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Object with nested object", () => {
+ // Test object: `{objProp: {id: 1}, strProp: "test string"}`
+ const object = stubs.get("testNestedObject");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = 'Object { objProp: {…}, strProp: "test string" }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+
+ // Check the custom title with nested objects to make sure nested objects
+ // are not displayed with their parent's title.
+ expect(
+ renderRep({
+ mode: MODE.LONG,
+ title: "CustomTitle",
+ }).text()
+ ).toBe('CustomTitle { objProp: {…}, strProp: "test string" }');
+ });
+});
+
+describe("Grip - Object with nested array", () => {
+ // Test object: `{arrProp: ["foo", "bar", "baz"]}`
+ const object = stubs.get("testNestedArray");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const propLength = getGripLengthBubbleText(
+ object.preview.ownProperties.arrProp.value,
+ { mode: MODE.TINY }
+ );
+ const defaultOutput = `Object { arrProp: ${propLength} […] }`;
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Object with connected nodes", () => {
+ const object = stubs.get("testObjectWithNodes");
+ const grips = getSelectableInInspectorGrips(object);
+ const renderRep = props => shallowRenderRep(object, props);
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("has the expected number of node grip", () => {
+ expect(grips).toHaveLength(2);
+ });
+
+ it("renders as expected", () => {
+ const defaultOutput =
+ "Object { foo: button#btn-1.btn.btn-log, bar: button#btn-2.btn.btn-err }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+
+ it("calls the expected function on mouseover", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOver });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseover");
+ node.at(1).simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(2);
+ expect(onDOMNodeMouseOver.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOver.mock.calls[1][0]).toBe(grips[1]);
+ });
+
+ it("calls the expected function on mouseout", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = renderRep({ onDOMNodeMouseOut });
+ const node = wrapper.find(".objectBox-node");
+
+ node.at(0).simulate("mouseout");
+ node.at(1).simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(2);
+ expect(onDOMNodeMouseOut.mock.calls[0][0]).toBe(grips[0]);
+ expect(onDOMNodeMouseOut.mock.calls[1][0]).toBe(grips[1]);
+ });
+
+ it("calls the expected function on click", () => {
+ const onInspectIconClick = jest.fn();
+ const wrapper = renderRep({ onInspectIconClick });
+ const node = wrapper.find(".open-inspector");
+
+ node.at(0).simulate("click");
+ node.at(1).simulate("click");
+
+ expect(onInspectIconClick.mock.calls).toHaveLength(2);
+ expect(onInspectIconClick.mock.calls[0][0]).toBe(grips[0]);
+ expect(onInspectIconClick.mock.calls[1][0]).toBe(grips[1]);
+ });
+});
+
+describe("Grip - Object with disconnected nodes", () => {
+ const object = stubs.get("testObjectWithDisconnectedNodes");
+ const renderRep = props => shallowRenderRep(object, props);
+ const grips = getSelectableInInspectorGrips(object);
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("has the expected number of grips", () => {
+ expect(grips).toHaveLength(2);
+ });
+
+ it("no inspect icon when nodes are not connected to the DOM tree", () => {
+ const onInspectIconClick = jest.fn();
+ const wrapper = renderRep({ onInspectIconClick });
+
+ const node = wrapper.find(".open-inspector");
+ expect(node.exists()).toBe(false);
+ });
+});
+
+describe("Grip - Object with getter", () => {
+ const object = stubs.get("TestObjectWithGetter");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { x: Getter }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Object with setter", () => {
+ const object = stubs.get("TestObjectWithSetter");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { x: Setter }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Object with getter and setter", () => {
+ const object = stubs.get("TestObjectWithGetterAndSetter");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { x: Getter & Setter }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - Object with symbol properties", () => {
+ const object = stubs.get("TestObjectWithSymbolProperties");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput =
+ 'Object { x: 10, Symbol(): "first unnamed symbol", ' +
+ 'Symbol(): "second unnamed symbol", … }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ 'Object { x: 10, Symbol(): "first unnamed symbol", ' +
+ 'Symbol(): "second unnamed symbol", Symbol("named"): "named symbol", ' +
+ "Symbol(Symbol.iterator): () }"
+ );
+ });
+});
+
+describe("Grip - Object with more than max symbol properties", () => {
+ const object = stubs.get("TestObjectWithMoreThanMaxSymbolProperties");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput =
+ 'Object { Symbol("i-0"): "value-0", Symbol("i-1"): "value-1", ' +
+ 'Symbol("i-2"): "value-2", … }';
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(
+ 'Object { Symbol("i-0"): "value-0", Symbol("i-1"): "value-1", ' +
+ 'Symbol("i-2"): "value-2", Symbol("i-3"): "value-3", ' +
+ 'Symbol("i-4"): "value-4", Symbol("i-5"): "value-5", ' +
+ 'Symbol("i-6"): "value-6", Symbol("i-7"): "value-7", ' +
+ 'Symbol("i-8"): "value-8", Symbol("i-9"): "value-9", … }'
+ );
+ });
+});
+
+describe("Grip - Without preview", () => {
+ // Test object: `[1, "foo", {}]`
+ const object = gripArrayStubs.get("testMaxProps").preview.items[2];
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Object { }";
+
+ let component = renderRep({ mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.TINY });
+ expect(component.text()).toBe("{}");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("Grip - Generator object", () => {
+ // Test object:
+ // function* genFunc() {
+ // var a = 5; while (a < 10) { yield a++; }
+ // };
+ // genFunc();
+ const object = stubs.get("Generator");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "Generator { }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("Generator");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Grip - DeadObject object", () => {
+ // Test object (executed in a privileged content, like about:preferences):
+ // `var s = Cu.Sandbox(null);Cu.nukeSandbox(s);s;`
+
+ const object = stubs.get("DeadObject");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallowRenderRep(object, props);
+ const defaultOutput = "DeadObject { }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("DeadObject");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+// TODO: Re-enable and fix this test.
+describe.skip("Grip - Object with __proto__ property", () => {
+ const object = stubs.get("ObjectWith__proto__Property");
+
+ it("correctly selects Grip Rep", () => {
+ expect(getRep(object)).toBe(Grip.rep);
+ });
+
+ it("renders as expected", () => {
+ const renderRep = props => shallow(Rep({ object, ...props }));
+ const defaultOutput = "Object { __proto__: [] }";
+
+ expect(renderRep({ mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.TINY }).text()).toBe("{…}");
+ expect(renderRep({ mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep({ mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+// Test that object that might look like raw objects or arrays are rendered
+// as grips when the `noGrip` parameter is not passed.
+describe("Object - noGrip prop", () => {
+ it("empty object", () => {
+ expect(getRep({})).toBe(Grip.rep);
+ });
+
+ it("object with custom property", () => {
+ expect(getRep({ foo: 123 })).toBe(Grip.rep);
+ });
+
+ it("empty array", () => {
+ expect(getRep([])).toBe(Grip.rep);
+ });
+
+ it("array with some item", () => {
+ expect(getRep([123])).toBe(Grip.rep);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/helper-tests.test.js b/devtools/client/shared/components/test/node/components/reps/helper-tests.test.js
new file mode 100644
index 0000000000..6fc1be64a3
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/helper-tests.test.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+const {
+ getGripLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+describe("getGripLengthBubbleText - Zero length", () => {
+ const object = stubs.get("testBasic");
+
+ it("length bubble is invisible", () => {
+ const output = "";
+ let text = getGripLengthBubbleText(object, { mode: undefined });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.SHORT });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ expect(text).toBe(output);
+ });
+
+ it("length bubble is visible", () => {
+ const output = "(0)";
+ let text = getGripLengthBubbleText(object, {
+ mode: undefined,
+ showZeroLength: true,
+ });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, {
+ mode: MODE.TINY,
+ showZeroLength: true,
+ });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, {
+ mode: MODE.SHORT,
+ showZeroLength: true,
+ });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, {
+ mode: MODE.LONG,
+ showZeroLength: true,
+ });
+ expect(text).toBe(output);
+ });
+});
+
+describe("getGripLengthBubbleText - Obvious length for some modes", () => {
+ const object = stubs.get("testMoreThanShortMaxProps");
+ const visibleOutput = `(${object.preview.length})`;
+
+ it("text renders as expected", () => {
+ let text = getGripLengthBubbleText(object, { mode: undefined });
+ expect(text).toBe(visibleOutput);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(text).toBe(visibleOutput);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.SHORT });
+ expect(text).toBe(visibleOutput);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ expect(text).toBe(visibleOutput);
+
+ const visibilityThreshold = 5;
+ text = getGripLengthBubbleText(object, {
+ mode: undefined,
+ visibilityThreshold,
+ });
+ expect(text).toBe(visibleOutput);
+
+ text = getGripLengthBubbleText(object, {
+ mode: MODE.TINY,
+ visibilityThreshold,
+ });
+ expect(text).toBe(visibleOutput);
+
+ text = getGripLengthBubbleText(object, {
+ mode: MODE.SHORT,
+ visibilityThreshold,
+ });
+ expect(text).toBe(visibleOutput);
+
+ text = getGripLengthBubbleText(object, {
+ mode: MODE.LONG,
+ visibilityThreshold,
+ });
+ expect(text).toBe("");
+ });
+});
+
+describe("getGripLengthBubbleText - Visible length", () => {
+ const object = stubs.get("testMoreThanLongMaxProps");
+ const output = `(${object.preview.length})`;
+
+ it("length bubble is always visible", () => {
+ let text = getGripLengthBubbleText(object, { mode: undefined });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.TINY });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.SHORT });
+ expect(text).toBe(output);
+
+ text = getGripLengthBubbleText(object, { mode: MODE.LONG });
+ expect(text).toBe(output);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/infinity.test.js b/devtools/client/shared/components/test/node/components/reps/infinity.test.js
new file mode 100644
index 0000000000..93fd310917
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/infinity.test.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { InfinityRep, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/infinity.js");
+
+describe("testInfinity", () => {
+ const stub = stubs.get("Infinity");
+
+ it("Rep correctly selects Infinity Rep", () => {
+ expect(getRep(stub)).toBe(InfinityRep.rep);
+ });
+
+ it("Infinity rep has expected text content for Infinity", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent.text()).toEqual("Infinity");
+ });
+
+ it("Infinity rep has expected title content for Infinity", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.prop("title")).toEqual("Infinity");
+ });
+});
+
+describe("testNegativeInfinity", () => {
+ const stub = stubs.get("NegativeInfinity");
+
+ it("Rep correctly selects Infinity Rep", () => {
+ expect(getRep(stub)).toBe(InfinityRep.rep);
+ });
+
+ it("Infinity rep has expected text content for negative Infinity", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent.text()).toEqual("-Infinity");
+ });
+
+ it("Infinity rep has expected title content for negative Infinity", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.prop("title")).toEqual("-Infinity");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/long-string.test.js b/devtools/client/shared/components/test/node/components/reps/long-string.test.js
new file mode 100644
index 0000000000..ea6bb43e97
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/long-string.test.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ ELLIPSIS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const { StringRep } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/long-string.js");
+
+function quoteNewlines(text) {
+ return text.split("\n").join("\\n");
+}
+
+describe("long StringRep", () => {
+ it("selects String Rep", () => {
+ const stub = stubs.get("testMultiline");
+
+ expect(getRep(stub)).toEqual(StringRep.rep);
+ });
+
+ it("renders with expected text content for multiline string", () => {
+ const stub = stubs.get("testMultiline");
+ const renderedComponent = shallow(
+ StringRep.rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ quoteNewlines(`"${stub.initial}${ELLIPSIS}"`)
+ );
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it(
+ "renders with expected text content for multiline string with " +
+ "specified number of characters",
+ () => {
+ const stub = stubs.get("testMultiline");
+ const renderedComponent = shallow(
+ StringRep.rep({
+ object: stub,
+ cropLimit: 20,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ `"a\\naaaaaaaaaaaaaaaaaa${ELLIPSIS}"`
+ );
+ }
+ );
+
+ it("renders with expected text for multiline string when open", () => {
+ const stub = stubs.get("testMultiline");
+ const renderedComponent = shallow(
+ StringRep.rep({
+ object: stub,
+ member: { open: true },
+ cropLimit: 20,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ quoteNewlines(`"${stub.initial}${ELLIPSIS}"`)
+ );
+ });
+
+ it(
+ "renders with expected text content when grip has a fullText" +
+ "property and is open",
+ () => {
+ const stub = stubs.get("testLoadedFullText");
+ const renderedComponent = shallow(
+ StringRep.rep({
+ object: stub,
+ member: { open: true },
+ cropLimit: 20,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ quoteNewlines(`"${stub.fullText}"`)
+ );
+ }
+ );
+
+ it(
+ "renders with expected text content when grip has a fullText " +
+ "property and is not open",
+ () => {
+ const stub = stubs.get("testLoadedFullText");
+ const renderedComponent = shallow(
+ StringRep.rep({
+ object: stub,
+ cropLimit: 20,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ `"a\\naaaaaaaaaaaaaaaaaa${ELLIPSIS}"`
+ );
+ }
+ );
+
+ it("expected to omit quotes", () => {
+ const stub = stubs.get("testMultiline");
+ const renderedComponent = shallow(
+ StringRep.rep({
+ object: stub,
+ cropLimit: 20,
+ useQuotes: false,
+ })
+ );
+
+ expect(renderedComponent.html()).toEqual(
+ '<span data-link-actor-id="server1.conn1.child1/longString58" ' +
+ `class="objectBox objectBox-string">a\naaaaaaaaaaaaaaaaaa${ELLIPSIS}` +
+ "</span>"
+ );
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/nan.test.js b/devtools/client/shared/components/test/node/components/reps/nan.test.js
new file mode 100644
index 0000000000..429e70acab
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/nan.test.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { NaNRep, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/nan.js");
+
+describe("NaN", () => {
+ const stub = stubs.get("NaN");
+
+ it("selects NaN Rep as expected", () => {
+ expect(getRep(stub)).toBe(NaNRep.rep);
+ });
+
+ it("renders NaN Rep as expected", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent).toMatchSnapshot();
+ });
+
+ it("NaN rep renders with the correct title element", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.prop("title")).toBe("NaN");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/null.test.js b/devtools/client/shared/components/test/node/components/reps/null.test.js
new file mode 100644
index 0000000000..af5f615cad
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/null.test.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { Null, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/null.js");
+
+describe("testNull", () => {
+ const stub = stubs.get("Null");
+
+ it("Rep correctly selects Null Rep", () => {
+ expect(getRep(stub)).toBe(Null.rep);
+ });
+
+ it("Rep correctly selects Null Rep for plain JS null object", () => {
+ expect(getRep(null, undefined, true)).toBe(Null.rep);
+ });
+
+ it("Null rep has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent.text()).toEqual("null");
+ });
+
+ it("Null rep displays null for title", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.prop("title")).toEqual("null");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/number.test.js b/devtools/client/shared/components/test/node/components/reps/number.test.js
new file mode 100644
index 0000000000..c0bbab3b0a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/number.test.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Number, Rep } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/number.js");
+
+describe("Int", () => {
+ const stub = stubs.get("Int");
+
+ it("correctly selects Number Rep for Integer value", () => {
+ expect(getRep(stub)).toBe(Number.rep);
+ });
+
+ it("renders with expected text content for integer", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("5");
+ expect(renderedComponent.prop("title")).toBe("5");
+ });
+});
+
+describe("Boolean", () => {
+ const stubTrue = stubs.get("True");
+ const stubFalse = stubs.get("False");
+
+ it("correctly selects Number Rep for boolean value", () => {
+ expect(getRep(stubTrue)).toBe(Number.rep);
+ });
+
+ it("renders with expected text content for boolean true", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stubTrue,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("true");
+ expect(renderedComponent.prop("title")).toBe("true");
+ });
+
+ it("renders with expected text content for boolean false", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stubFalse,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("false");
+ expect(renderedComponent.prop("title")).toBe("false");
+ });
+});
+
+describe("Negative Zero", () => {
+ const stubNegativeZeroGrip = stubs.get("NegZeroGrip");
+ const stubNegativeZeroValue = -0;
+
+ it("correctly selects Number Rep for negative zero grip", () => {
+ expect(getRep(stubNegativeZeroGrip)).toBe(Number.rep);
+ });
+
+ it("correctly selects Number Rep for negative zero value", () => {
+ expect(getRep(stubNegativeZeroValue)).toBe(Number.rep);
+ });
+
+ it("renders with expected text content for negative zero grip", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stubNegativeZeroGrip,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("-0");
+ expect(renderedComponent.prop("title")).toBe("-0");
+ });
+
+ it("renders with expected text content for negative zero value", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stubNegativeZeroValue,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("-0");
+ expect(renderedComponent.prop("title")).toBe("-0");
+ });
+});
+
+describe("Zero", () => {
+ it("correctly selects Number Rep for zero value", () => {
+ expect(getRep(0)).toBe(Number.rep);
+ });
+
+ it("renders with expected text content for zero value", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: 0,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("0");
+ expect(renderedComponent.prop("title")).toBe("0");
+ });
+});
+
+describe("Unsafe Int", () => {
+ it("renders with expected test content for a long number", () => {
+ const renderedComponent = shallow(
+ Rep({
+ // eslint-disable-next-line no-loss-of-precision
+ object: 900719925474099122,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("900719925474099100");
+ expect(renderedComponent.prop("title")).toBe("900719925474099100");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/object-with-text.test.js b/devtools/client/shared/components/test/node/components/reps/object-with-text.test.js
new file mode 100644
index 0000000000..164024f7e6
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/object-with-text.test.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { shallow } = require("enzyme");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/object-with-text.js");
+const { ObjectWithText, Rep } = REPS;
+
+describe("Object with text - CSSStyleRule", () => {
+ const gripStub = stubs.get("ShadowRule");
+
+ // Test that correct rep is chosen
+ it("selects ObjectsWithText Rep", () => {
+ expect(getRep(gripStub)).toEqual(ObjectWithText.rep);
+ });
+
+ // Test rendering
+ it("renders with the correct text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: gripStub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual('CSSStyleRule ".Shadow"');
+ expect(renderedComponent.prop("title")).toEqual('CSSStyleRule ".Shadow"');
+ expectActorAttribute(renderedComponent, gripStub.actor);
+ });
+});
+
+describe("Object with text - CSSMediaRule", () => {
+ const gripStub = stubs.get("CSSMediaRule");
+
+ // Test that correct rep is chosen
+ it("selects ObjectsWithText Rep", () => {
+ expect(getRep(gripStub)).toEqual(ObjectWithText.rep);
+ });
+
+ // Test rendering
+ it("renders with the correct text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: gripStub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ const text =
+ 'CSSMediaRule "(min-height: 680px), screen and (orientation: portrait)"';
+ expect(renderedComponent.text()).toEqual(text);
+ expect(renderedComponent.prop("title")).toEqual(text);
+ expectActorAttribute(renderedComponent, gripStub.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/object-with-url.test.js b/devtools/client/shared/components/test/node/components/reps/object-with-url.test.js
new file mode 100644
index 0000000000..055afbfde4
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/object-with-url.test.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { ObjectWithURL } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/object-with-url.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+describe("ObjectWithURL", () => {
+ const stub = stubs.get("ObjectWithUrl");
+
+ it("selects the correct rep", () => {
+ expect(getRep(stub)).toEqual(ObjectWithURL.rep);
+ });
+
+ it("renders with correct class name and content", () => {
+ const renderedComponent = shallow(
+ ObjectWithURL.rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.text()).toBe(
+ "Location https://www.mozilla.org/en-US/"
+ );
+ expect(renderedComponent.prop("title")).toBe(
+ "Location https://www.mozilla.org/en-US/"
+ );
+ expect(renderedComponent.hasClass("objectBox-Location")).toBe(true);
+
+ const innerNode = renderedComponent.find(".objectPropValue");
+ expect(innerNode.text()).toBe("https://www.mozilla.org/en-US/");
+
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/object.test.js b/devtools/client/shared/components/test/node/components/reps/object.test.js
new file mode 100644
index 0000000000..9b32e51d20
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/object.test.js
@@ -0,0 +1,356 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Obj } = REPS;
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+
+const renderComponent = (object, props) => {
+ return shallow(Obj.rep({ object, ...props }));
+};
+
+describe("Object - Basic", () => {
+ const object = {};
+ const defaultOutput = "Object { }";
+
+ it("selects the correct rep", () => {
+ expect(getRep(object, undefined, true)).toBe(Obj.rep);
+ });
+
+ it("renders basic object as expected", () => {
+ expect(renderComponent(object, { mode: undefined }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: undefined }).prop("title")).toEqual(
+ "Object"
+ );
+
+ expect(renderComponent(object, { mode: MODE.TINY }).text()).toEqual("{}");
+ expect(renderComponent(object, { mode: MODE.TINY }).prop("title")).toEqual(
+ "Object"
+ );
+
+ expect(renderComponent(object, { mode: MODE.SHORT }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.LONG }).text()).toEqual(
+ defaultOutput
+ );
+ });
+});
+
+describe("Object - Max props", () => {
+ const object = { a: "a", b: "b", c: "c" };
+ const defaultOutput = 'Object { a: "a", b: "b", c: "c" }';
+
+ it("renders object with max props as expected", () => {
+ expect(renderComponent(object, { mode: undefined }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.TINY }).text()).toEqual("{…}");
+ expect(renderComponent(object, { mode: MODE.SHORT }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.LONG }).text()).toEqual(
+ defaultOutput
+ );
+ });
+});
+
+describe("Object - Many props", () => {
+ const object = {};
+ for (let i = 0; i < 100; i++) {
+ object[`p${i}`] = i;
+ }
+ const defaultOutput = "Object { p0: 0, p1: 1, p2: 2, … }";
+
+ it("renders object with many props as expected", () => {
+ expect(renderComponent(object, { mode: undefined }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.TINY }).text()).toEqual("{…}");
+ expect(renderComponent(object, { mode: MODE.SHORT }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.LONG }).text()).toEqual(
+ defaultOutput
+ );
+ });
+});
+
+describe("Object - Uninteresting props", () => {
+ const object = { a: undefined, b: undefined, c: "c", d: 0 };
+ const defaultOutput = 'Object { c: "c", d: 0, a: undefined, … }';
+
+ it("renders object with uninteresting props as expected", () => {
+ expect(renderComponent(object, { mode: undefined }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.TINY }).text()).toEqual("{…}");
+ expect(renderComponent(object, { mode: MODE.SHORT }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.LONG }).text()).toEqual(
+ defaultOutput
+ );
+ });
+});
+
+describe("Object - Escaped property names", () => {
+ const object = { "": 1, "quote-this": 2, noquotes: 3 };
+ const defaultOutput = 'Object { "": 1, "quote-this": 2, noquotes: 3 }';
+
+ it("renders object with escaped property names as expected", () => {
+ expect(renderComponent(object, { mode: undefined }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.TINY }).text()).toEqual("{…}");
+ expect(renderComponent(object, { mode: MODE.SHORT }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.LONG }).text()).toEqual(
+ defaultOutput
+ );
+ });
+});
+
+describe("Object - Nested", () => {
+ const object = {
+ objProp: {
+ id: 1,
+ arr: [2],
+ },
+ strProp: "test string",
+ arrProp: [1],
+ };
+ const defaultOutput =
+ 'Object { strProp: "test string", objProp: {…},' + " arrProp: […] }";
+
+ it("renders nested object as expected", () => {
+ expect(
+ renderComponent(object, { mode: undefined, noGrip: true }).text()
+ ).toEqual(defaultOutput);
+ expect(
+ renderComponent(object, { mode: MODE.TINY, noGrip: true }).text()
+ ).toEqual("{…}");
+ expect(
+ renderComponent(object, { mode: MODE.SHORT, noGrip: true }).text()
+ ).toEqual(defaultOutput);
+ expect(
+ renderComponent(object, { mode: MODE.LONG, noGrip: true }).text()
+ ).toEqual(defaultOutput);
+ });
+});
+
+describe("Object - More prop", () => {
+ const object = {
+ a: undefined,
+ b: 1,
+ more: 2,
+ d: 3,
+ };
+ const defaultOutput = "Object { b: 1, more: 2, d: 3, … }";
+
+ it("renders object with more properties as expected", () => {
+ expect(renderComponent(object, { mode: undefined }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.TINY }).text()).toEqual("{…}");
+ expect(renderComponent(object, { mode: MODE.SHORT }).text()).toEqual(
+ defaultOutput
+ );
+ expect(renderComponent(object, { mode: MODE.LONG }).text()).toEqual(
+ defaultOutput
+ );
+ });
+});
+
+describe("Object - Custom Title", () => {
+ const customTitle = "MyCustomObject";
+ const object = { a: "a", b: "b", c: "c" };
+ const defaultOutput = `${customTitle} { a: "a", b: "b", c: "c" }`;
+
+ it("renders object with more properties as expected", () => {
+ expect(
+ renderComponent(object, { mode: undefined, title: customTitle }).text()
+ ).toEqual(defaultOutput);
+ expect(
+ renderComponent(object, { mode: undefined, title: customTitle }).prop(
+ "title"
+ )
+ ).toEqual(customTitle);
+ expect(
+ renderComponent(object, { mode: MODE.TINY, title: customTitle }).text()
+ ).toEqual(customTitle);
+ expect(
+ renderComponent(object, { mode: MODE.TINY, title: customTitle }).prop(
+ "title"
+ )
+ ).toEqual(customTitle);
+ expect(
+ renderComponent(object, { mode: MODE.SHORT, title: customTitle }).text()
+ ).toEqual(defaultOutput);
+ expect(
+ renderComponent(object, { mode: MODE.LONG, title: customTitle }).text()
+ ).toEqual(defaultOutput);
+ });
+});
+
+// Test that object that might look like Grips are rendered as Object when
+// passed the `noGrip` property.
+describe("Object - noGrip prop", () => {
+ it("object with type property", () => {
+ expect(getRep({ type: "string" }, undefined, true)).toBe(Obj.rep);
+ });
+
+ it("object with actor property", () => {
+ expect(getRep({ actor: "fake/actorId" }, undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Attribute grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/attribute.js");
+ expect(getRep(stubs.get("Attribute"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("CommentNode grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/comment-node.js");
+ expect(getRep(stubs.get("Comment"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("DateTime grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/date-time.js");
+ expect(getRep(stubs.get("DateTime"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Document grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/document.js");
+ expect(getRep(stubs.get("Document"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("ElementNode grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/element-node.js");
+ expect(getRep(stubs.get("BodyNode"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Error grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/error.js");
+ expect(getRep(stubs.get("SimpleError"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Event grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/event.js");
+ expect(getRep(stubs.get("testEvent"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Function grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/function.js");
+ expect(getRep(stubs.get("Named"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Array grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js");
+ expect(getRep(stubs.get("testMaxProps"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Map grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-map.js");
+ expect(getRep(stubs.get("testSymbolKeyedMap"), undefined, true)).toBe(
+ Obj.rep
+ );
+ });
+
+ it("Object grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js");
+ expect(getRep(stubs.get("testMaxProps"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Infinity grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/infinity.js");
+ expect(getRep(stubs.get("Infinity"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("LongString grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/long-string.js");
+ expect(getRep(stubs.get("testMultiline"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("NaN grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/nan.js");
+ expect(getRep(stubs.get("NaN"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Null grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/null.js");
+ expect(getRep(stubs.get("Null"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Number grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/number.js");
+ expect(getRep(stubs.get("NegZeroGrip"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("ObjectWithText grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/object-with-text.js");
+ expect(getRep(stubs.get("ShadowRule"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("ObjectWithURL grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/object-with-url.js");
+ expect(getRep(stubs.get("ObjectWithUrl"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Promise grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/promise.js");
+ expect(getRep(stubs.get("Pending"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("RegExp grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/regexp.js");
+ expect(getRep(stubs.get("RegExp"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Stylesheet grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/stylesheet.js");
+ expect(getRep(stubs.get("StyleSheet"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Symbol grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/symbol.js");
+ expect(getRep(stubs.get("Symbol"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("TextNode grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/text-node.js");
+ expect(getRep(stubs.get("testRendering"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Undefined grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/undefined.js");
+ expect(getRep(stubs.get("Undefined"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Window grip", () => {
+ const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+ expect(getRep(stubs.get("Window"), undefined, true)).toBe(Obj.rep);
+ });
+
+ it("Object with class property", () => {
+ const object = {
+ class: "Array",
+ };
+ expect(getRep(object, undefined, true)).toBe(Obj.rep);
+
+ expect(
+ renderComponent(object, { mode: MODE.SHORT, noGrip: true }).text()
+ ).toEqual('Object { class: "Array" }');
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/promise.test.js b/devtools/client/shared/components/test/node/components/reps/promise.test.js
new file mode 100644
index 0000000000..03862d19f6
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/promise.test.js
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { PromiseRep } = REPS;
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/promise.js");
+const {
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+ getGripLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const renderRep = (object, props) => {
+ return shallow(PromiseRep.rep({ object, ...props }));
+};
+
+describe("Promise - Pending", () => {
+ const object = stubs.get("Pending");
+ const defaultOutput = 'Promise { <state>: "pending" }';
+
+ it("correctly selects PromiseRep Rep for pending Promise", () => {
+ expect(getRep(object)).toBe(PromiseRep.rep);
+ });
+
+ it("renders as expected", () => {
+ let component = renderRep(object, {
+ mode: undefined,
+ shouldRenderTooltip: true,
+ });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe("Promise");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep(object, {
+ mode: MODE.TINY,
+ shouldRenderTooltip: true,
+ });
+ expect(component.text()).toBe('Promise { "pending" }');
+ expect(component.prop("title")).toBe("Promise");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep(object, {
+ mode: MODE.SHORT,
+ shouldRenderTooltip: true,
+ });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe("Promise");
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep(object, {
+ mode: MODE.LONG,
+ shouldRenderTooltip: true,
+ });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe("Promise");
+ expectActorAttribute(component, object.actor);
+ });
+});
+
+describe("Promise - fulfilled with string", () => {
+ const object = stubs.get("FulfilledWithString");
+ const defaultOutput = 'Promise { <state>: "fulfilled", <value>: "foo" }';
+
+ it("selects PromiseRep Rep for Promise fulfilled with a string", () => {
+ expect(getRep(object)).toBe(PromiseRep.rep);
+ });
+
+ it("should render as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe(
+ 'Promise { "fulfilled" }'
+ );
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Promise - fulfilled with object", () => {
+ const object = stubs.get("FulfilledWithObject");
+ const defaultOutput = 'Promise { <state>: "fulfilled", <value>: {…} }';
+
+ it("selects PromiseRep Rep for Promise fulfilled with an object", () => {
+ expect(getRep(object)).toBe(PromiseRep.rep);
+ });
+
+ it("should render as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe(
+ 'Promise { "fulfilled" }'
+ );
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Promise - fulfilled with array", () => {
+ const object = stubs.get("FulfilledWithArray");
+ const length = getGripLengthBubbleText(
+ object.preview.ownProperties["<value>"].value,
+ {
+ mode: MODE.TINY,
+ }
+ );
+ const out = `Promise { <state>: "fulfilled", <value>: ${length} […] }`;
+
+ it("selects PromiseRep Rep for Promise fulfilled with an array", () => {
+ expect(getRep(object)).toBe(PromiseRep.rep);
+ });
+
+ it("should render as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(out);
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe(
+ 'Promise { "fulfilled" }'
+ );
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(out);
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(out);
+ });
+});
+
+describe("Promise - fulfilled with node", () => {
+ const stub = stubs.get("FulfilledWithNode");
+ const grips = getSelectableInInspectorGrips(stub);
+
+ it("has one node grip", () => {
+ expect(grips).toHaveLength(1);
+ });
+
+ it("calls the expected function on mouseover", () => {
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = renderRep(stub, { onDOMNodeMouseOver });
+ const node = wrapper.find(".objectBox-node");
+
+ node.simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOver).toHaveBeenCalledWith(grips[0]);
+ });
+
+ it("calls the expected function on mouseout", () => {
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = renderRep(stub, { onDOMNodeMouseOut });
+ const node = wrapper.find(".objectBox-node");
+
+ node.simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOut).toHaveBeenCalledWith(grips[0]);
+ });
+
+ it("no inspect icon when the node is not connected to the DOM tree", () => {
+ const renderedComponentWithoutInspectIcon = renderRep(
+ stubs.get("FulfilledWithDisconnectedNode")
+ );
+ const node = renderedComponentWithoutInspectIcon.find(".open-inspector");
+
+ expect(node.exists()).toBe(false);
+ });
+
+ it("renders an inspect icon", () => {
+ const onInspectIconClick = jest.fn();
+ const renderedComponent = renderRep(stub, { onInspectIconClick });
+ const icon = renderedComponent.find(".open-inspector");
+
+ icon.simulate("click");
+
+ expect(icon.exists()).toBe(true);
+ expect(onInspectIconClick.mock.calls).toHaveLength(1);
+ });
+});
+
+describe("Promise - rejected with number", () => {
+ const object = stubs.get("RejectedWithNumber");
+ const defaultOutput = 'Promise { <state>: "rejected", <reason>: 123 }';
+
+ it("selects PromiseRep Rep for Promise rejected with an object", () => {
+ expect(getRep(object)).toBe(PromiseRep.rep);
+ });
+
+ it("should render as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe(
+ 'Promise { "rejected" }'
+ );
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
+
+describe("Promise - rejected with object", () => {
+ const object = stubs.get("RejectedWithObject");
+ const defaultOutput = 'Promise { <state>: "rejected", <reason>: {…} }';
+
+ it("selects PromiseRep Rep for Promise rejected with an object", () => {
+ expect(getRep(object)).toBe(PromiseRep.rep);
+ });
+
+ it("should render as expected", () => {
+ expect(renderRep(object, { mode: undefined }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.TINY }).text()).toBe(
+ 'Promise { "rejected" }'
+ );
+ expect(renderRep(object, { mode: MODE.SHORT }).text()).toBe(defaultOutput);
+ expect(renderRep(object, { mode: MODE.LONG }).text()).toBe(defaultOutput);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/regexp.test.js b/devtools/client/shared/components/test/node/components/reps/regexp.test.js
new file mode 100644
index 0000000000..7e1ea841b8
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/regexp.test.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Rep, RegExp } = REPS;
+const {
+ ELLIPSIS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/regexp.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+describe("test RegExp", () => {
+ const stub = stubs.get("RegExp");
+
+ it("selects RegExp Rep", () => {
+ expect(getRep(stub)).toEqual(RegExp.rep);
+ });
+
+ it("renders with expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("/ab+c/i");
+ expect(renderedComponent.prop("title")).toEqual("/ab+c/i");
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it("renders regexp with longString displayString with expected text content", () => {
+ const longStringDisplayStringRegexpStub = stubs.get(
+ "longString displayString RegExp"
+ );
+ const renderedComponent = shallow(
+ Rep({
+ object: longStringDisplayStringRegexpStub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ `/${"ab ".repeat(333)}${ELLIPSIS}`
+ );
+ expectActorAttribute(
+ renderedComponent,
+ longStringDisplayStringRegexpStub.actor
+ );
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/string-with-url.test.js b/devtools/client/shared/components/test/node/components/reps/string-with-url.test.js
new file mode 100644
index 0000000000..a7f287a302
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/string-with-url.test.js
@@ -0,0 +1,630 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { mount } = require("enzyme");
+const {
+ REPS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Rep } = REPS;
+const {
+ getGripLengthBubbleText,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+const renderRep = (string, props) =>
+ mount(
+ Rep({
+ object: string,
+ ...props,
+ })
+ );
+
+const testLinkClick = (link, openLink, url) => {
+ let syntheticEvent;
+ const preventDefault = jest.fn().mockImplementation(function () {
+ // This refers to the event object for which preventDefault is called (in
+ // this case it is the syntheticEvent that is passed to onClick and
+ // consequently to openLink).
+ syntheticEvent = this;
+ });
+
+ link.simulate("click", { preventDefault });
+ // Prevent defaults behavior on click
+ expect(preventDefault).toHaveBeenCalled();
+ expect(openLink).toHaveBeenCalledWith(url, syntheticEvent);
+};
+
+describe("test String with URL", () => {
+ it("renders a URL", () => {
+ const url = "http://example.com";
+ const openLink = jest.fn();
+ const element = renderRep(url, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a href when openLink isn't defined", () => {
+ const url = "http://example.com";
+ const element = renderRep(url, { useQuotes: false });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(null);
+ expect(link.prop("title")).toBe(url);
+ expect(link.prop("rel")).toBe("noopener noreferrer");
+ });
+
+ it("renders a href when no openLink but isInContentPage is true", () => {
+ const url = "http://example.com";
+ const element = renderRep(url, { useQuotes: false, isInContentPage: true });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+ expect(link.prop("rel")).toBe("noopener noreferrer");
+ });
+
+ it("renders a simple quoted URL", () => {
+ const url = "http://example.com";
+ const string = `'${url}'`;
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(string);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a double quoted URL", () => {
+ const url = "http://example.com";
+ const string = `"${url}"`;
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(string);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a quoted URL when useQuotes is true", () => {
+ const url = "http://example.com";
+ const string = `"${url}"`;
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: true });
+ expect(element.text()).toEqual(`'"${url}"'`);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a simple https URL", () => {
+ const url = "https://example.com";
+ const openLink = jest.fn();
+ const element = renderRep(url, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a simple http URL with one slash", () => {
+ const url = "https:/example.com";
+ const openLink = jest.fn();
+ const element = renderRep(url, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a URL with port", () => {
+ const url = "https://example.com:443";
+ const openLink = jest.fn();
+ const element = renderRep(url, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a URL with non-empty path", () => {
+ const url = "http://example.com/foo";
+ const openLink = jest.fn();
+ const element = renderRep(url, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a URL when surrounded by non-URL tokens", () => {
+ const url = "http://example.com";
+ const string = `foo ${url} bar`;
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(string);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a URL and whitespace is be preserved", () => {
+ const url = "http://example.com";
+ const string = `foo\n${url}\nbar\n`;
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(string);
+
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders multiple URLs", () => {
+ const url1 = "http://example.com";
+ const url2 = "https://example.com/foo";
+ const string = `${url1} ${url2}`;
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(string);
+ const links = element.find("a");
+ expect(links).toHaveLength(2);
+
+ const firstLink = links.at(0);
+ expect(firstLink.prop("href")).toBe(url1);
+ expect(firstLink.prop("title")).toBe(url1);
+ testLinkClick(firstLink, openLink, url1);
+
+ const secondLink = links.at(1);
+ expect(secondLink.prop("href")).toBe(url2);
+ expect(secondLink.prop("title")).toBe(url2);
+ testLinkClick(secondLink, openLink, url2);
+ });
+
+ it("renders multiple URLs with various spacing", () => {
+ const url1 = "http://example.com";
+ const url2 = "https://example.com/foo";
+ const string = ` ${url1} ${url2} ${url2} ${url1} `;
+ const element = renderRep(string, { useQuotes: false });
+ expect(element.text()).toEqual(string);
+ const links = element.find("a");
+ expect(links).toHaveLength(4);
+ });
+
+ it("renders a cropped URL", () => {
+ const url = "http://example.com";
+ const openLink = jest.fn();
+ const element = renderRep(url, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 15,
+ });
+
+ expect(element.text()).toEqual("http://…ple.com");
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders a non-cropped URL", () => {
+ const url = "http://example.com/foobarbaz";
+ const openLink = jest.fn();
+ const element = renderRep(url, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 50,
+ });
+
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders URL on an open string", () => {
+ const url = "http://example.com";
+ const openLink = jest.fn();
+ const element = renderRep(url, {
+ openLink,
+ useQuotes: false,
+ member: {
+ open: true,
+ },
+ });
+
+ expect(element.text()).toEqual(url);
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("renders URLs with a stripped string between", () => {
+ const text = "- http://example.fr --- http://example.us -";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 41,
+ });
+
+ expect(element.text()).toEqual("- http://example.fr … http://example.us -");
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example.fr");
+ expect(linkFr.prop("title")).toBe("http://example.fr");
+
+ const linkUs = element.find("a").at(1);
+ expect(linkUs.prop("href")).toBe("http://example.us");
+ expect(linkUs.prop("title")).toBe("http://example.us");
+ });
+
+ it("renders URLs with a cropped string between", () => {
+ const text = "- http://example.fr ---- http://example.us -";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 42,
+ });
+
+ expect(element.text()).toEqual(
+ "- http://example.fr -…- http://example.us -"
+ );
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example.fr");
+ expect(linkFr.prop("title")).toBe("http://example.fr");
+
+ const linkUs = element.find("a").at(1);
+ expect(linkUs.prop("href")).toBe("http://example.us");
+ expect(linkUs.prop("title")).toBe("http://example.us");
+ });
+
+ it("renders successive cropped URLs, 1 at the start, 1 at the end", () => {
+ const text = "- http://example-long.fr http://example.us -";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 20,
+ });
+
+ expect(element.text()).toEqual("- http://e…ample.us -");
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example-long.fr");
+ expect(linkFr.prop("title")).toBe("http://example-long.fr");
+
+ const linkUs = element.find("a").at(1);
+ expect(linkUs.prop("href")).toBe("http://example.us");
+ expect(linkUs.prop("title")).toBe("http://example.us");
+ });
+
+ it("renders successive URLs, one cropped in the middle", () => {
+ const text =
+ "- http://example-long.fr http://example.com http://example.us -";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 60,
+ });
+
+ expect(element.text()).toEqual(
+ "- http://example-long.fr http:…xample.com http://example.us -"
+ );
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example-long.fr");
+ expect(linkFr.prop("title")).toBe("http://example-long.fr");
+
+ const linkCom = element.find("a").at(1);
+ expect(linkCom.prop("href")).toBe("http://example.com");
+ expect(linkCom.prop("title")).toBe("http://example.com");
+
+ const linkUs = element.find("a").at(2);
+ expect(linkUs.prop("href")).toBe("http://example.us");
+ expect(linkUs.prop("title")).toBe("http://example.us");
+ });
+
+ it("renders successive cropped URLs with cropped elements between", () => {
+ const text =
+ "- http://example.fr test http://example.es test http://example.us -";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 20,
+ });
+
+ expect(element.text()).toEqual("- http://e…ample.us -");
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example.fr");
+ expect(linkFr.prop("title")).toBe("http://example.fr");
+
+ const linkUs = element.find("a").at(1);
+ expect(linkUs.prop("href")).toBe("http://example.us");
+ expect(linkUs.prop("title")).toBe("http://example.us");
+ });
+
+ it("renders a cropped URL followed by a cropped string", () => {
+ const text = "http://example.fr abcdefghijkl";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 20,
+ });
+
+ expect(element.text()).toEqual("http://exa…cdefghijkl");
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example.fr");
+ expect(linkFr.prop("title")).toBe("http://example.fr");
+ });
+
+ it("renders a cropped string followed by a cropped URL", () => {
+ const text = "abcdefghijkl stripped http://example.fr ";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 20,
+ });
+
+ expect(element.text()).toEqual("abcdefghij…xample.fr ");
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example.fr");
+ expect(linkFr.prop("title")).toBe("http://example.fr");
+ });
+
+ it("renders URLs without unrelated characters", () => {
+ const text =
+ "global(http://example.com) and local(http://example.us)" +
+ " and maybe https://example.fr, “https://example.cz“, https://example.es?";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ });
+
+ expect(element.text()).toEqual(text);
+ const linkCom = element.find("a").at(0);
+ expect(linkCom.prop("href")).toBe("http://example.com");
+
+ const linkUs = element.find("a").at(1);
+ expect(linkUs.prop("href")).toBe("http://example.us");
+
+ const linkFr = element.find("a").at(2);
+ expect(linkFr.prop("href")).toBe("https://example.fr");
+
+ const linkCz = element.find("a").at(3);
+ expect(linkCz.prop("href")).toBe("https://example.cz");
+
+ const linkEs = element.find("a").at(4);
+ expect(linkEs.prop("href")).toBe("https://example.es");
+ });
+
+ it("renders a cropped URL with urlCropLimit", () => {
+ const xyzUrl = "http://xyz.com/abcdefghijklmnopqrst";
+ const text = `${xyzUrl} is the best`;
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ urlCropLimit: 20,
+ });
+
+ expect(element.text()).toEqual(text);
+ const link = element.find("a.cropped-url").at(0);
+ expect(link.prop("href")).toBe(xyzUrl);
+ expect(link.prop("title")).toBe(xyzUrl);
+ const linkParts = link.find("span");
+ expect(linkParts.at(0).hasClass("cropped-url-start")).toBe(true);
+ expect(linkParts.at(0).text()).toEqual("http://xyz");
+ expect(linkParts.at(1).hasClass("cropped-url-middle")).toBe(true);
+ expect(linkParts.at(2).hasClass("cropped-url-end")).toBe(true);
+ expect(linkParts.at(2).text()).toEqual("klmnopqrst");
+ });
+
+ it("renders multiple cropped URL", () => {
+ const xyzUrl = "http://xyz.com/abcdefghijklmnopqrst";
+ const abcUrl = "http://abc.com/abcdefghijklmnopqrst";
+ const text = `${xyzUrl} is lit, not ${abcUrl}`;
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ urlCropLimit: 20,
+ });
+
+ expect(element.text()).toEqual(`${xyzUrl} is lit, not ${abcUrl}`);
+
+ const links = element.find("a.cropped-url");
+ const xyzLink = links.at(0);
+ expect(xyzLink.prop("href")).toBe(xyzUrl);
+ expect(xyzLink.prop("title")).toBe(xyzUrl);
+ const xyzLinkParts = xyzLink.find("span");
+ expect(xyzLinkParts.at(0).hasClass("cropped-url-start")).toBe(true);
+ expect(xyzLinkParts.at(0).text()).toEqual("http://xyz");
+ expect(xyzLinkParts.at(1).hasClass("cropped-url-middle")).toBe(true);
+ expect(xyzLinkParts.at(2).hasClass("cropped-url-end")).toBe(true);
+ expect(xyzLinkParts.at(2).text()).toEqual("klmnopqrst");
+
+ const abc = links.at(1);
+ expect(abc.prop("href")).toBe(abcUrl);
+ expect(abc.prop("title")).toBe(abcUrl);
+ const abcLinkParts = abc.find("span");
+ expect(abcLinkParts.at(0).hasClass("cropped-url-start")).toBe(true);
+ expect(abcLinkParts.at(0).text()).toEqual("http://abc");
+ expect(abcLinkParts.at(1).hasClass("cropped-url-middle")).toBe(true);
+ expect(abcLinkParts.at(2).hasClass("cropped-url-end")).toBe(true);
+ expect(abcLinkParts.at(2).text()).toEqual("klmnopqrst");
+ });
+
+ it("renders full URL if smaller than cropLimit", () => {
+ const xyzUrl = "http://example.com/";
+
+ const openLink = jest.fn();
+ const element = renderRep(xyzUrl, {
+ openLink,
+ useQuotes: false,
+ urlCropLimit: 20,
+ });
+
+ expect(element.text()).toEqual(xyzUrl);
+ const link = element.find("a").at(0);
+ expect(link.prop("href")).toBe(xyzUrl);
+ expect(link.prop("title")).toBe(xyzUrl);
+ expect(link.find(".cropped-url-start").length).toBe(0);
+ });
+
+ it("renders cropped URL followed by cropped string with urlCropLimit", () => {
+ const text = "http://example.fr abcdefghijkl";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 20,
+ });
+
+ expect(element.text()).toEqual("http://exa…cdefghijkl");
+ const linkFr = element.find("a").at(0);
+ expect(linkFr.prop("href")).toBe("http://example.fr");
+ expect(linkFr.prop("title")).toBe("http://example.fr");
+ });
+
+ it("does not render a link if the URL has no scheme", () => {
+ const url = "example.com";
+ const element = renderRep(url, { useQuotes: false });
+ expect(element.text()).toEqual(url);
+ expect(element.find("a").exists()).toBeFalsy();
+ });
+
+ it("does not render a link if the URL has an invalid scheme", () => {
+ const url = "foo://example.com";
+ const element = renderRep(url, { useQuotes: false });
+ expect(element.text()).toEqual(url);
+ expect(element.find("a").exists()).toBeFalsy();
+ });
+
+ it("does not render an invalid URL that requires cropping", () => {
+ const text =
+ "//www.youtubeinmp3.com/download/?video=https://www.youtube.com/watch?v=8vkfsCIfDFc";
+ const openLink = jest.fn();
+ const element = renderRep(text, {
+ openLink,
+ useQuotes: false,
+ cropLimit: 60,
+ });
+ expect(element.text()).toEqual(
+ "//www.youtubeinmp3.com/downloa…outube.com/watch?v=8vkfsCIfDFc"
+ );
+ expect(element.find("a").exists()).toBeFalsy();
+ });
+
+ it("does render a link in a plain array", () => {
+ const url = "http://example.com/abcdefghijabcdefghij";
+ const string = `${url} some other text`;
+ const object = [string];
+ const openLink = jest.fn();
+ const element = renderRep(object, { openLink, noGrip: true });
+ expect(element.text()).toEqual(`[ "${string}" ]`);
+
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("does render a link in a grip array", () => {
+ const object =
+ require("resource://devtools/client/shared/components/test/node/stubs/reps/grip-array.js").get(
+ '["http://example.com/abcdefghijabcdefghij some other text"]'
+ );
+ const length = getGripLengthBubbleText(object);
+ const openLink = jest.fn();
+ const element = renderRep(object, { openLink });
+
+ const url = "http://example.com/abcdefghijabcdefghij";
+ const string = `${url} some other text`;
+ expect(element.text()).toEqual(`Array${length} [ "${string}" ]`);
+
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("does render a link in a plain object", () => {
+ const url = "http://example.com/abcdefghijabcdefghij";
+ const string = `${url} some other text`;
+ const object = { test: string };
+ const openLink = jest.fn();
+ const element = renderRep(object, { openLink, noGrip: true });
+ expect(element.text()).toEqual(`Object { test: "${string}" }`);
+
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("does render a link in a grip object", () => {
+ const object =
+ require("resource://devtools/client/shared/components/test/node/stubs/reps/grip.js").get(
+ '{test: "http://example.com/ some other text"}'
+ );
+ const openLink = jest.fn();
+ const element = renderRep(object, { openLink });
+
+ const url = "http://example.com/";
+ const string = `${url} some other text`;
+ expect(element.text()).toEqual(`Object { test: "${string}" }`);
+
+ const link = element.find("a");
+ expect(link.prop("href")).toBe(url);
+ expect(link.prop("title")).toBe(url);
+
+ testLinkClick(link, openLink, url);
+ });
+
+ it("does not render links for js URL", () => {
+ const url = "javascript:x=42";
+ const string = `${url} some other text`;
+
+ const openLink = jest.fn();
+ const element = renderRep(string, { openLink, useQuotes: false });
+ expect(element.text()).toEqual(string);
+ const link = element.find("a");
+ expect(link.exists()).toBe(false);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/string.test.js b/devtools/client/shared/components/test/node/components/reps/string.test.js
new file mode 100644
index 0000000000..4e179ecab0
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/string.test.js
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow, mount } = require("enzyme");
+const {
+ ELLIPSIS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const {
+ REPS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Rep } = REPS;
+
+const renderRep = (string, props) =>
+ mount(
+ Rep({
+ object: string,
+ ...props,
+ })
+ );
+
+const testCases = [
+ {
+ name: "testMultiline",
+ props: {
+ object: "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n",
+ },
+ result:
+ '"aaaaaaaaaaaaaaaaaaaaa\\nbbbbbbbbbbbbbbbbbbb\\ncccccccccccccccc\\n"',
+ },
+ {
+ name: "testMultilineLimit",
+ props: {
+ object: "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n",
+ cropLimit: 20,
+ },
+ result: `\"aaaaaaaaa${ELLIPSIS}cccccc\\n\"`,
+ },
+ {
+ name: "testMultilineOpen",
+ props: {
+ object: "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n",
+ member: { open: true },
+ },
+ result:
+ '"aaaaaaaaaaaaaaaaaaaaa\\nbbbbbbbbbbbbbbbbbbb\\ncccccccccccccccc\\n"',
+ },
+ {
+ name: "testUseQuotes",
+ props: {
+ object: "abc",
+ useQuotes: false,
+ },
+ result: "abc",
+ },
+ {
+ name: "testNonPrintableCharacters",
+ props: {
+ object: "a\x01b",
+ useQuotes: false,
+ },
+ result: "a\ufffdb",
+ },
+ {
+ name: "testQuoting",
+ props: {
+ object:
+ "\t\n\r\"'\\\x1f\x9f\ufeff\ufffe\ud8000\u2063\ufffc\u2028\ueeee\ufffd",
+ useQuotes: true,
+ },
+ result:
+ "`\\t\\n\\r\"'\\\\\\u001f\\u009f\\ufeff\\ufffe\\ud8000\\u2063" +
+ "\\ufffc\\u2028\\ueeee\ufffd`",
+ },
+ {
+ name: "testUnpairedSurrogate",
+ props: {
+ object: "\uDC23",
+ useQuotes: true,
+ },
+ result: '"\\udc23"',
+ },
+ {
+ name: "testValidSurrogate",
+ props: {
+ object: "\ud83d\udeec",
+ useQuotes: true,
+ },
+ result: '"\ud83d\udeec"',
+ },
+ {
+ name: "testNoEscapeWhitespace",
+ props: {
+ object: "line 1\r\nline 2\n\tline 3",
+ useQuotes: true,
+ escapeWhitespace: false,
+ },
+ result: '"line 1\r\nline 2\n\tline 3"',
+ },
+ {
+ name: "testIgnoreFullTextWhenOpen",
+ props: {
+ object: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ fullText:
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
+ "aaaaaaaaaaaaa",
+ member: { open: true },
+ },
+ result: '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
+ },
+ {
+ name: "testIgnoreFullTextWithLimit",
+ props: {
+ object: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ fullText:
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
+ "aaaaaaaaaaaaa",
+ cropLimit: 20,
+ },
+ result: `\"aaaaaaaaa${ELLIPSIS}aaaaaaaa\"`,
+ },
+ {
+ name: "testIgnoreFullTextWhenOpenWithLimit",
+ props: {
+ object: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ fullText:
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
+ "aaaaaaaaaaaaa",
+ member: { open: true },
+ cropLimit: 20,
+ },
+ result: '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
+ },
+ {
+ name: "testEmptyStringWithoutQuotes",
+ props: {
+ object: "",
+ transformEmptyString: true,
+ useQuotes: false,
+ },
+ result: "<empty string>",
+ },
+ {
+ name: "testEmptyStringWithoutQuotesAndNoTransform",
+ props: {
+ object: "",
+ useQuotes: false,
+ transformEmptyString: false,
+ },
+ result: "",
+ },
+ {
+ name: "testEmptyStringWithQuotes",
+ props: {
+ object: "",
+ useQuotes: true,
+ transformEmptyString: true,
+ },
+ result: `""`,
+ },
+ {
+ name: "testEmptyStringWithQuotesAndNoTransforms",
+ props: {
+ object: "",
+ useQuotes: true,
+ transformEmptyString: false,
+ },
+ result: `""`,
+ },
+ {
+ name: "testQuotingSingleQuote",
+ props: {
+ object: "'",
+ useQuotes: true,
+ },
+ result: `"'"`,
+ },
+ {
+ name: "testQuotingDoubleQuote",
+ props: {
+ object: '"',
+ useQuotes: true,
+ },
+ result: `'"'`,
+ },
+ {
+ name: "testQuotingBacktick",
+ props: {
+ object: "`",
+ useQuotes: true,
+ },
+ result: '"`"',
+ },
+ {
+ name: "testQuotingSingleAndDoubleQuotes",
+ props: {
+ object: "'\"",
+ useQuotes: true,
+ },
+ result: "`'\"`",
+ },
+ {
+ name: "testQuotingSingleAndDoubleQuotesAnd${",
+ props: {
+ object: "'\"${",
+ useQuotes: true,
+ },
+ result: '"\'\\"${"',
+ },
+ {
+ name: "testQuotingSingleQuoteAndBacktick",
+ props: {
+ object: "'`",
+ useQuotes: true,
+ },
+ result: '"\'`"',
+ },
+ {
+ name: "testQuotingDoubleQuoteAndBacktick",
+ props: {
+ object: '"`',
+ useQuotes: true,
+ },
+ result: "'\"`'",
+ },
+ {
+ name: "testQuotingSingleAndDoubleQuotesAndBacktick",
+ props: {
+ object: "'\"`",
+ useQuotes: true,
+ },
+ result: '"\'\\"`"',
+ },
+];
+
+describe("test String", () => {
+ for (const testCase of testCases) {
+ it(`String rep ${testCase.name}`, () => {
+ const renderedComponent = shallow(Rep(testCase.props));
+ expect(renderedComponent.text()).toEqual(testCase.result);
+ });
+ }
+
+ it("If shouldRenderTooltip, StringRep displays a tooltip title on the span element.", () => {
+ const tooltipText = "This is a tooltip";
+ const element = renderRep(tooltipText, { shouldRenderTooltip: true });
+ expect(element.prop("title")).toBe('"This is a tooltip"');
+ });
+
+ it("If !shouldRenderTooltip, StringRep doesn't display a tooltip title.", () => {
+ const noTooltip = "There is no tooltip";
+ const element = renderRep(noTooltip, { shouldRenderTooltip: false });
+ expect(element.prop("title")).toBe(undefined);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/stylesheet.test.js b/devtools/client/shared/components/test/node/components/reps/stylesheet.test.js
new file mode 100644
index 0000000000..31c40facfe
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/stylesheet.test.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { StyleSheet, Rep } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/stylesheet.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+describe("Test StyleSheet", () => {
+ const stub = stubs.get("StyleSheet")._grip;
+
+ it("selects the StyleSheet Rep", () => {
+ expect(getRep(stub)).toEqual(StyleSheet.rep);
+ });
+
+ it("renders with the expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "CSSStyleSheet https://example.com/styles.css"
+ );
+ expect(renderedComponent.prop("title")).toEqual(
+ "CSSStyleSheet https://example.com/styles.css"
+ );
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/symbol.test.js b/devtools/client/shared/components/test/node/components/reps/symbol.test.js
new file mode 100644
index 0000000000..4959e9a813
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/symbol.test.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+const {
+ REPS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const { Rep } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/symbol.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+describe("test Symbol", () => {
+ const stub = stubs.get("Symbol");
+
+ it("renders with the expected content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual('Symbol("foo")');
+ expect(renderedComponent.prop("title")).toBe("Symbol(foo)");
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("test Symbol without identifier", () => {
+ const stub = stubs.get("SymbolWithoutIdentifier");
+
+ it("renders the expected content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Symbol()");
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
+
+describe("test Symbol with long string", () => {
+ const stub = stubs.get("SymbolWithLongString");
+
+ it("renders the expected content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ 'Symbol("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…")'
+ );
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/test-helpers.js b/devtools/client/shared/components/test/node/components/reps/test-helpers.js
new file mode 100644
index 0000000000..d601f71e88
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/test-helpers.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ lengthBubble,
+} = require("resource://devtools/client/shared/components/reps/shared/grip-length-bubble.js");
+const {
+ maxLengthMap: arrayLikeMaxLengthMap,
+ getLength: getArrayLikeLength,
+} = require("resource://devtools/client/shared/components/reps/reps/grip-array.js");
+const {
+ maxLengthMap: mapMaxLengths,
+ getLength: getMapLength,
+} = require("resource://devtools/client/shared/components/reps/reps/grip-map.js");
+const {
+ getGripPreviewItems,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const nodeConstants = require("resource://devtools/client/shared/components/reps/shared/dom-node-constants.js");
+
+/**
+ * Get an array of all the items from the grip in parameter (including the grip
+ * itself) which can be selected in the inspector.
+ *
+ * @param {Object} Grip
+ * @return {Array} Flat array of the grips which can be selected in the
+ * inspector
+ */
+function getSelectableInInspectorGrips(grip) {
+ const grips = new Set(getFlattenedGrips([grip]));
+ return [...grips].filter(isGripSelectableInInspector);
+}
+
+/**
+ * Indicate if a Grip can be selected in the inspector,
+ * i.e. if it represents a node element.
+ *
+ * @param {Object} Grip
+ * @return {Boolean}
+ */
+function isGripSelectableInInspector(grip) {
+ return (
+ grip &&
+ typeof grip === "object" &&
+ grip.preview &&
+ [nodeConstants.TEXT_NODE, nodeConstants.ELEMENT_NODE].includes(
+ grip.preview.nodeType
+ )
+ );
+}
+
+/**
+ * Get a flat array of all the grips and their preview items.
+ *
+ * @param {Array} Grips
+ * @return {Array} Flat array of the grips and their preview items
+ */
+function getFlattenedGrips(grips) {
+ return grips.reduce((res, grip) => {
+ const previewItems = getGripPreviewItems(grip);
+ const flatPreviewItems = previewItems.length
+ ? getFlattenedGrips(previewItems)
+ : [];
+
+ return [...res, grip, ...flatPreviewItems];
+ }, []);
+}
+
+function expectActorAttribute(wrapper, expectedValue) {
+ const actorIdAttribute = "data-link-actor-id";
+ const attrElement = wrapper.find(`[${actorIdAttribute}]`);
+ expect(attrElement.exists()).toBeTruthy();
+ expect(attrElement.first().prop("data-link-actor-id")).toBe(expectedValue);
+}
+
+function getGripLengthBubbleText(object, props) {
+ const component = lengthBubble({
+ object,
+ maxLengthMap: arrayLikeMaxLengthMap,
+ getLength: getArrayLikeLength,
+ ...props,
+ });
+
+ return component ? shallow(component).text() : "";
+}
+
+function getMapLengthBubbleText(object, props) {
+ return getGripLengthBubbleText(object, {
+ maxLengthMap: mapMaxLengths,
+ getLength: getMapLength,
+ showZeroLength: true,
+ ...props,
+ });
+}
+
+function createGripMapEntry(key, value) {
+ return {
+ type: "mapEntry",
+ preview: {
+ key,
+ value,
+ },
+ };
+}
+
+module.exports = {
+ createGripMapEntry,
+ expectActorAttribute,
+ getSelectableInInspectorGrips,
+ getGripLengthBubbleText,
+ getMapLengthBubbleText,
+};
diff --git a/devtools/client/shared/components/test/node/components/reps/text-node.test.js b/devtools/client/shared/components/test/node/components/reps/text-node.test.js
new file mode 100644
index 0000000000..dff4b15b0c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/text-node.test.js
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const { TextNode } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/text-node.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+const {
+ ELLIPSIS,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+
+function quoteNewlines(text) {
+ return text.split("\n").join("\\n");
+}
+
+describe("TextNode", () => {
+ it("selects TextNode Rep as expected", () => {
+ expect(getRep(stubs.get("testRendering")._grip)).toBe(TextNode.rep);
+ });
+
+ it("renders as expected", () => {
+ const object = stubs.get("testRendering")._grip;
+ const renderRep = props => shallow(TextNode.rep({ object, ...props }));
+
+ const defaultOutput = '#text "hello world"';
+
+ let component = renderRep({ shouldRenderTooltip: true, mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.TINY });
+ expect(component.text()).toBe("#text");
+ expect(component.prop("title")).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultOutput);
+ expectActorAttribute(component, object.actor);
+ });
+
+ it("renders as expected with EOL", () => {
+ const object = stubs.get("testRenderingWithEOL")._grip;
+ const renderRep = props => shallow(TextNode.rep({ object, ...props }));
+
+ const defaultOutput = quoteNewlines('#text "hello\nworld"');
+ const defaultTooltip = '#text "hello\nworld"';
+
+ let component = renderRep({ shouldRenderTooltip: true, mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.TINY });
+ expect(component.text()).toBe("#text");
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+ });
+
+ it("renders as expected with double quote", () => {
+ const object = stubs.get("testRenderingWithDoubleQuote")._grip;
+ const renderRep = props => shallow(TextNode.rep({ object, ...props }));
+
+ const defaultOutput = "#text 'hello\"world'";
+ const defaultTooltip = '#text "hello"world"';
+
+ let component = renderRep({ shouldRenderTooltip: true, mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.TINY });
+ expect(component.text()).toBe("#text");
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+ });
+
+ it("renders as expected with long string", () => {
+ const object = stubs.get("testRenderingWithLongString")._grip;
+ const renderRep = props => shallow(TextNode.rep({ object, ...props }));
+ const initialString = object.preview.textContent.initial;
+
+ const defaultOutput = `#text "${quoteNewlines(initialString)}${ELLIPSIS}"`;
+ const defaultTooltip = `#text "${initialString}"`;
+
+ let component = renderRep({ shouldRenderTooltip: true, mode: undefined });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.TINY });
+ expect(component.text()).toBe("#text");
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.SHORT });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+
+ component = renderRep({ shouldRenderTooltip: true, mode: MODE.LONG });
+ expect(component.text()).toBe(defaultOutput);
+ expect(component.prop("title")).toBe(defaultTooltip);
+ });
+
+ it("calls the expected function on mouseover", () => {
+ const object = stubs.get("testRendering")._grip;
+ const onDOMNodeMouseOver = jest.fn();
+ const wrapper = shallow(TextNode.rep({ object, onDOMNodeMouseOver }));
+
+ wrapper.simulate("mouseover");
+
+ expect(onDOMNodeMouseOver.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOver).toHaveBeenCalledWith(object);
+ });
+
+ it("calls the expected function on mouseout", () => {
+ const object = stubs.get("testRendering")._grip;
+ const onDOMNodeMouseOut = jest.fn();
+ const wrapper = shallow(TextNode.rep({ object, onDOMNodeMouseOut }));
+
+ wrapper.simulate("mouseout");
+
+ expect(onDOMNodeMouseOut.mock.calls).toHaveLength(1);
+ expect(onDOMNodeMouseOut).toHaveBeenCalledWith(object);
+ });
+
+ it("displays a button when the node is connected", () => {
+ const object = stubs.get("testRendering")._grip;
+
+ const onInspectIconClick = jest.fn();
+ const wrapper = shallow(TextNode.rep({ object, onInspectIconClick }));
+
+ const inspectIconNode = wrapper.find(".open-inspector");
+ expect(inspectIconNode !== null).toBe(true);
+
+ const event = Symbol("click-event");
+ inspectIconNode.simulate("click", event);
+
+ // The function is called once
+ expect(onInspectIconClick.mock.calls).toHaveLength(1);
+ const [arg1, arg2] = onInspectIconClick.mock.calls[0];
+ // First argument is the grip
+ expect(arg1).toBe(object);
+ // Second one is the event
+ expect(arg2).toBe(event);
+ });
+
+ it("does not display a button when the node is connected", () => {
+ const object = stubs.get("testRenderingDisconnected")._grip;
+
+ const onInspectIconClick = jest.fn();
+ const wrapper = shallow(TextNode.rep({ object, onInspectIconClick }));
+ expect(wrapper.find(".open-inspector")).toHaveLength(0);
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/undefined.test.js b/devtools/client/shared/components/test/node/components/reps/undefined.test.js
new file mode 100644
index 0000000000..56fa512f55
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/undefined.test.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const { Undefined, Rep } = REPS;
+
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/undefined.js");
+// Test that correct rep is chosen
+describe("Test Undefined", () => {
+ const stub = stubs.get("Undefined");
+
+ it("selects Undefined as expected", () => {
+ expect(getRep(stub)).toBe(Undefined.rep);
+ });
+
+ it("Rep correctly selects Undefined Rep for plain JS undefined", () => {
+ expect(getRep(undefined, undefined, true)).toBe(Undefined.rep);
+ });
+
+ it("Undefined rep has expected text content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent.text()).toEqual("undefined");
+ });
+
+ it("Undefined rep has expected class names", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+ expect(renderedComponent.hasClass("objectBox objectBox-undefined")).toEqual(
+ true
+ );
+ });
+
+ it("Undefined rep has expected title", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+ expect(renderedComponent.prop("title")).toEqual("undefined");
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/reps/window.test.js b/devtools/client/shared/components/test/node/components/reps/window.test.js
new file mode 100644
index 0000000000..9645ca288c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/reps/window.test.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const { shallow } = require("enzyme");
+
+const {
+ REPS,
+ getRep,
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const { Rep, Window } = REPS;
+const stubs = require("resource://devtools/client/shared/components/test/node/stubs/reps/window.js");
+const {
+ expectActorAttribute,
+} = require("resource://devtools/client/shared/components/test/node/components/reps/test-helpers.js");
+
+describe("test Window", () => {
+ const stub = stubs.get("Window")._grip;
+
+ it("selects Window Rep correctly", () => {
+ expect(getRep(stub)).toBe(Window.rep);
+ });
+
+ it("renders with correct class name", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.hasClass("objectBox-Window")).toBe(true);
+ expectActorAttribute(renderedComponent, stub.actor);
+ });
+
+ it("renders with correct content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "Window data:text/html;charset=utf-8,stub generation"
+ );
+ expect(renderedComponent.prop("title")).toEqual(
+ "Window data:text/html;charset=utf-8,stub generation"
+ );
+ });
+
+ it("renders with correct inner HTML structure and content", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ })
+ );
+
+ expect(renderedComponent.find(".location").text()).toEqual(
+ "data:text/html;charset=utf-8,stub generation"
+ );
+ });
+
+ it("renders with expected text in TINY mode", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ mode: MODE.TINY,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Window");
+ expect(renderedComponent.prop("title")).toEqual(
+ "Window data:text/html;charset=utf-8,stub generation"
+ );
+ });
+
+ it("renders with expected text in LONG mode", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: stub,
+ mode: MODE.LONG,
+ shouldRenderTooltip: true,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "Window data:text/html;charset=utf-8,stub generation"
+ );
+ expect(renderedComponent.prop("title")).toEqual(
+ "Window data:text/html;charset=utf-8,stub generation"
+ );
+ });
+
+ it("renders expected text in TINY mode with Custom display class", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: {
+ ...stub,
+ displayClass: "Custom",
+ },
+ mode: MODE.TINY,
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual("Custom");
+ });
+
+ it("renders expected text in LONG mode with Custom display class", () => {
+ const renderedComponent = shallow(
+ Rep({
+ object: {
+ ...stub,
+ displayClass: "Custom",
+ },
+ mode: MODE.LONG,
+ title: "Custom",
+ })
+ );
+
+ expect(renderedComponent.text()).toEqual(
+ "Custom data:text/html;charset=utf-8,stub generation"
+ );
+ });
+});
diff --git a/devtools/client/shared/components/test/node/components/tree.test.js b/devtools/client/shared/components/test/node/components/tree.test.js
new file mode 100644
index 0000000000..c70b66e8ff
--- /dev/null
+++ b/devtools/client/shared/components/test/node/components/tree.test.js
@@ -0,0 +1,911 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+/* global jest */
+
+const React = require("react");
+const { mount } = require("enzyme");
+const dom = require("react-dom-factories");
+
+const { Component, createFactory } = React;
+const Tree = createFactory(
+ require("resource://devtools/client/shared/components/Tree.js")
+);
+
+function mountTree(overrides = {}) {
+ return mount(
+ createFactory(
+ class container extends Component {
+ constructor(props) {
+ super(props);
+ const state = {
+ expanded: overrides.expanded || new Set(),
+ focused: overrides.focused,
+ active: overrides.active,
+ };
+ delete overrides.focused;
+ delete overrides.active;
+ this.state = state;
+ }
+
+ render() {
+ return Tree(
+ Object.assign(
+ {
+ getParent: x => TEST_TREE.parent[x],
+ getChildren: x => TEST_TREE.children[x],
+ renderItem: (x, depth, focused, arrow) => {
+ return dom.div(
+ {},
+ arrow,
+ focused ? "[" : null,
+ x,
+ focused ? "]" : null
+ );
+ },
+ getRoots: () => ["A", "M"],
+ getKey: x => `key-${x}`,
+ itemHeight: 1,
+ onFocus: x => {
+ this.setState(previousState => {
+ return { focused: x };
+ });
+ },
+ onActivate: x => {
+ this.setState(previousState => {
+ return { active: x };
+ });
+ },
+ onExpand: x => {
+ this.setState(previousState => {
+ const expanded = new Set(previousState.expanded);
+ expanded.add(x);
+ return { expanded };
+ });
+ },
+ onCollapse: x => {
+ this.setState(previousState => {
+ const expanded = new Set(previousState.expanded);
+ expanded.delete(x);
+ return { expanded };
+ });
+ },
+ isExpanded: x => this.state && this.state.expanded.has(x),
+ focused: this.state.focused,
+ active: this.state.active,
+ },
+ overrides
+ )
+ );
+ }
+ }
+ )()
+ );
+}
+
+describe("Tree", () => {
+ it("does not throw", () => {
+ expect(mountTree()).toBeTruthy();
+ });
+
+ it("Don't auto expand root with very large number of children", () => {
+ const children = Array.from(
+ { length: 51 },
+ (_, i) => `should-not-be-visible-${i + 1}`
+ );
+ // N has a lot of children, so it won't be automatically expanded
+ const wrapper = mountTree({
+ autoExpandDepth: 2,
+ autoExpandNodeChildrenLimit: 50,
+ getChildren: item => {
+ if (item === "N") {
+ return children;
+ }
+
+ return TEST_TREE.children[item] || [];
+ },
+ });
+ const ids = getTreeNodes(wrapper).map(node => node.prop("id"));
+ expect(ids).toMatchSnapshot();
+ });
+
+ it("is accessible", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJMN".split("")),
+ });
+ expect(wrapper.getDOMNode().getAttribute("role")).toBe("tree");
+ expect(wrapper.getDOMNode().getAttribute("tabIndex")).toBe("0");
+
+ const expected = {
+ A: { id: "key-A", level: 1, expanded: true },
+ B: { id: "key-B", level: 2, expanded: true },
+ C: { id: "key-C", level: 2, expanded: true },
+ D: { id: "key-D", level: 2, expanded: true },
+ E: { id: "key-E", level: 3, expanded: true },
+ F: { id: "key-F", level: 3, expanded: true },
+ G: { id: "key-G", level: 3, expanded: true },
+ H: { id: "key-H", level: 3, expanded: true },
+ I: { id: "key-I", level: 3, expanded: true },
+ J: { id: "key-J", level: 3, expanded: true },
+ K: { id: "key-K", level: 4, expanded: undefined },
+ L: { id: "key-L", level: 4, expanded: undefined },
+ M: { id: "key-M", level: 1, expanded: true },
+ N: { id: "key-N", level: 2, expanded: true },
+ O: { id: "key-O", level: 3, expanded: undefined },
+ };
+
+ getTreeNodes(wrapper).forEach(node => {
+ const key = node.prop("id").replace("key-", "");
+ const item = expected[key];
+
+ expect(node.prop("id")).toBe(item.id);
+ expect(node.prop("role")).toBe("treeitem");
+ expect(node.prop("aria-level")).toBe(item.level);
+ expect(node.prop("aria-expanded")).toBe(item.expanded);
+ });
+ });
+
+ it("renders as expected", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ });
+
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ });
+
+ it("renders as expected when passed a className", () => {
+ const wrapper = mountTree({
+ className: "testClassName",
+ });
+
+ expect(wrapper.find(".tree").hasClass("testClassName")).toBe(true);
+ });
+
+ it("renders as expected when passed a style", () => {
+ const wrapper = mountTree({
+ style: {
+ color: "red",
+ },
+ });
+
+ expect(wrapper.getDOMNode().style.color).toBe("red");
+ });
+
+ it("renders as expected when passed a label", () => {
+ const wrapper = mountTree({
+ label: "testAriaLabel",
+ });
+ expect(wrapper.getDOMNode().getAttribute("aria-label")).toBe(
+ "testAriaLabel"
+ );
+ });
+
+ it("renders as expected when passed an aria-labelledby", () => {
+ const wrapper = mountTree({
+ labelledby: "testAriaLabelBy",
+ });
+ expect(wrapper.getDOMNode().getAttribute("aria-labelledby")).toBe(
+ "testAriaLabelBy"
+ );
+ });
+
+ it("renders as expected with collapsed nodes", () => {
+ const wrapper = mountTree({
+ expanded: new Set("MNO".split("")),
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ });
+
+ it("renders as expected when passed autoDepth:1", () => {
+ const wrapper = mountTree({
+ autoExpandDepth: 1,
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ });
+
+ it("calls shouldItemUpdate when provided", () => {
+ const shouldItemUpdate = jest.fn((prev, next) => true);
+ const wrapper = mountTree({
+ shouldItemUpdate,
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(shouldItemUpdate.mock.calls).toHaveLength(0);
+
+ wrapper.find("Tree").first().instance().forceUpdate();
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(shouldItemUpdate.mock.calls).toHaveLength(2);
+
+ expect(shouldItemUpdate.mock.calls[0][0]).toBe("A");
+ expect(shouldItemUpdate.mock.calls[0][1]).toBe("A");
+ expect(shouldItemUpdate.mock.results[0].value).toBe(true);
+ expect(shouldItemUpdate.mock.calls[1][0]).toBe("M");
+ expect(shouldItemUpdate.mock.calls[1][1]).toBe("M");
+ expect(shouldItemUpdate.mock.results[1].value).toBe(true);
+ });
+
+ it("active item - renders as expected when clicking away", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "G",
+ active: "G",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-G");
+
+ getTreeNodes(wrapper).first().simulate("click");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+ expect(wrapper.find(".active").exists()).toBe(false);
+ });
+
+ it("active item - renders as expected when tree blurs", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "G",
+ active: "G",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-G");
+
+ wrapper.simulate("blur");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").exists()).toBe(false);
+ });
+
+ it("active item - renders as expected when moving away with keyboard", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ active: "L",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+
+ simulateKeyDown(wrapper, "ArrowUp");
+ expect(wrapper.find(".active").exists()).toBe(false);
+ });
+
+ it("active item - renders as expected when using keyboard and Enter", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ });
+ wrapper.getDOMNode().focus();
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").exists()).toBe(false);
+
+ simulateKeyDown(wrapper, "Enter");
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.getDOMNode()
+ );
+
+ simulateKeyDown(wrapper, "Escape");
+ expect(wrapper.find(".active").exists()).toBe(false);
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.getDOMNode()
+ );
+ });
+
+ it("active item - renders as expected when using keyboard and Space", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ });
+ wrapper.getDOMNode().focus();
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").exists()).toBe(false);
+
+ simulateKeyDown(wrapper, " ");
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+
+ simulateKeyDown(wrapper, "Escape");
+ expect(wrapper.find(".active").exists()).toBe(false);
+ });
+
+ it("active item - focus is inside the tree node when possible", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ renderItem: renderItemWithFocusableContent,
+ });
+ wrapper.getDOMNode().focus();
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").exists()).toBe(false);
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.getDOMNode()
+ );
+
+ simulateKeyDown(wrapper, "Enter");
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.find("#active-anchor").getDOMNode()
+ );
+ });
+
+ it("active item - navigate inside the tree node", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ renderItem: renderItemWithFocusableContent,
+ });
+ wrapper.getDOMNode().focus();
+ simulateKeyDown(wrapper, "Enter");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.find("#active-anchor").getDOMNode()
+ );
+
+ simulateKeyDown(wrapper, "Tab");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.find("#active-anchor").getDOMNode()
+ );
+
+ simulateKeyDown(wrapper, "Tab", { shiftKey: true });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.find("#active-anchor").getDOMNode()
+ );
+ });
+
+ it("active item - focus is inside the tree node and then blur", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ renderItem: renderItemWithFocusableContent,
+ });
+ wrapper.getDOMNode().focus();
+ simulateKeyDown(wrapper, "Enter");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.find(".active").prop("id")).toBe("key-L");
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.find("#active-anchor").getDOMNode()
+ );
+
+ wrapper.find("#active-anchor").simulate("blur");
+ expect(wrapper.find(".active").exists()).toBe(false);
+ expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
+ wrapper.getDOMNode().ownerDocument.body
+ );
+ });
+
+ it("renders as expected when given a focused item", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "G",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-G"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-G");
+
+ getTreeNodes(wrapper).first().simulate("click");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+
+ getTreeNodes(wrapper).first().simulate("click");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+
+ wrapper.simulate("blur");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().hasAttribute("aria-activedescendant")).toBe(
+ false
+ );
+ expect(wrapper.find(".focused").exists()).toBe(false);
+ });
+
+ it("renders as expected when navigating up with the keyboard", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-L"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-L");
+
+ simulateKeyDown(wrapper, "ArrowUp");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-K"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-K");
+
+ simulateKeyDown(wrapper, "ArrowUp");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-E"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-E");
+ });
+
+ it("renders as expected navigating up with the keyboard on a root", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "A",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+
+ simulateKeyDown(wrapper, "ArrowUp");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+ });
+
+ it("renders as expected when navigating down with the keyboard", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "K",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-K"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-K");
+
+ simulateKeyDown(wrapper, "ArrowDown");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-L"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-L");
+
+ simulateKeyDown(wrapper, "ArrowDown");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-F"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-F");
+ });
+
+ it("renders as expected navigating down with keyboard on last node", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "O",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-O"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-O");
+
+ simulateKeyDown(wrapper, "ArrowDown");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-O"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-O");
+ });
+
+ it("renders as expected when navigating with right/left arrows", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-L"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-L");
+
+ simulateKeyDown(wrapper, "ArrowLeft");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-E"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-E");
+
+ simulateKeyDown(wrapper, "ArrowLeft");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-E"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-E");
+
+ simulateKeyDown(wrapper, "ArrowRight");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-E"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-E");
+
+ simulateKeyDown(wrapper, "ArrowRight");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-K"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-K");
+ });
+
+ it("renders as expected when navigating with left arrows on roots", () => {
+ const wrapper = mountTree({
+ focused: "M",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-M"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-M");
+
+ simulateKeyDown(wrapper, "ArrowLeft");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+
+ simulateKeyDown(wrapper, "ArrowLeft");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+ });
+
+ it("renders as expected when navigating with home/end", () => {
+ const wrapper = mountTree({
+ focused: "M",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-M"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-M");
+
+ simulateKeyDown(wrapper, "Home");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+
+ simulateKeyDown(wrapper, "Home");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+
+ simulateKeyDown(wrapper, "End");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-M"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-M");
+
+ simulateKeyDown(wrapper, "End");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-M"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-M");
+
+ simulateKeyDown(wrapper, "ArrowRight");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-M"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-M");
+
+ simulateKeyDown(wrapper, "End");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-N"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-N");
+
+ simulateKeyDown(wrapper, "End");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-N"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-N");
+
+ simulateKeyDown(wrapper, "Home");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-A");
+ });
+
+ it("renders as expected navigating with arrows on unexpandable roots", () => {
+ const wrapper = mountTree({
+ focused: "A",
+ isExpandable: item => false,
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+
+ simulateKeyDown(wrapper, "ArrowRight");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-M"
+ );
+
+ simulateKeyDown(wrapper, "ArrowLeft");
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-A"
+ );
+ });
+
+ it("calls onFocus when expected", () => {
+ const onFocus = jest.fn(x => {
+ wrapper &&
+ wrapper.setState(() => {
+ return { focused: x };
+ });
+ });
+
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "I",
+ onFocus,
+ });
+
+ simulateKeyDown(wrapper, "ArrowUp");
+ expect(onFocus.mock.calls[0][0]).toBe("H");
+
+ simulateKeyDown(wrapper, "ArrowUp");
+ expect(onFocus.mock.calls[1][0]).toBe("C");
+
+ simulateKeyDown(wrapper, "ArrowLeft");
+ simulateKeyDown(wrapper, "ArrowLeft");
+ expect(onFocus.mock.calls[2][0]).toBe("A");
+
+ simulateKeyDown(wrapper, "ArrowRight");
+ expect(onFocus.mock.calls[3][0]).toBe("B");
+
+ simulateKeyDown(wrapper, "ArrowDown");
+ expect(onFocus.mock.calls[4][0]).toBe("E");
+ });
+
+ it("focus treeRef when a node is clicked", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ });
+ const treeRef = wrapper.find("Tree").first().instance().treeRef.current;
+ treeRef.focus = jest.fn();
+
+ getTreeNodes(wrapper).first().simulate("click");
+ expect(treeRef.focus.mock.calls).toHaveLength(1);
+ });
+
+ it("doesn't focus treeRef when focused is null", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "A",
+ });
+ const treeRef = wrapper.find("Tree").first().instance().treeRef.current;
+ treeRef.focus = jest.fn();
+ wrapper.simulate("blur");
+ expect(treeRef.focus.mock.calls).toHaveLength(0);
+ });
+
+ it("ignores key strokes when pressing modifiers", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ focused: "L",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-L"
+ );
+ expect(wrapper.find(".focused").prop("id")).toBe("key-L");
+
+ const testKeys = [
+ { key: "ArrowDown" },
+ { key: "ArrowUp" },
+ { key: "ArrowLeft" },
+ { key: "ArrowRight" },
+ ];
+ const modifiers = [
+ { altKey: true },
+ { ctrlKey: true },
+ { metaKey: true },
+ { shiftKey: true },
+ ];
+
+ for (const key of testKeys) {
+ for (const modifier of modifiers) {
+ wrapper.simulate("keydown", Object.assign({}, key, modifier));
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
+ "key-L"
+ );
+ }
+ }
+ });
+
+ it("renders arrows as expected when nodes are expanded", () => {
+ const wrapper = mountTree({
+ expanded: new Set("ABCDEFGHIJKLMNO".split("")),
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+
+ getTreeNodes(wrapper).forEach(n => {
+ if ("ABECDMN".split("").includes(getSanitizedNodeText(n))) {
+ expect(n.find(".arrow.expanded").exists()).toBe(true);
+ } else {
+ expect(n.find(".arrow").exists()).toBe(false);
+ }
+ });
+ });
+
+ it("renders arrows as expected when nodes are collapsed", () => {
+ const wrapper = mountTree();
+ expect(formatTree(wrapper)).toMatchSnapshot();
+
+ getTreeNodes(wrapper).forEach(n => {
+ const arrow = n.find(".arrow");
+ expect(arrow.exists()).toBe(true);
+ expect(arrow.hasClass("expanded")).toBe(false);
+ });
+ });
+
+ it("uses isExpandable prop if it exists to render tree nodes", () => {
+ const wrapper = mountTree({
+ isExpandable: item => item === "A",
+ });
+ expect(formatTree(wrapper)).toMatchSnapshot();
+ });
+
+ it("adds the expected data-expandable attribute", () => {
+ const wrapper = mountTree({
+ isExpandable: item => item === "A",
+ });
+ const nodes = getTreeNodes(wrapper);
+ expect(nodes.at(0).prop("data-expandable")).toBe(true);
+ expect(nodes.at(1).prop("data-expandable")).toBe(false);
+ });
+});
+
+function getTreeNodes(wrapper) {
+ return wrapper.find(".tree-node");
+}
+
+function simulateKeyDown(wrapper, key, options) {
+ wrapper.simulate("keydown", {
+ key,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ ...options,
+ });
+}
+
+function renderItemWithFocusableContent(x, depth, focused, arrow) {
+ const children = [arrow, focused ? "[" : null, x];
+ if (x === "L") {
+ children.push(dom.a({ id: "active-anchor", href: "#" }, " anchor"));
+ }
+
+ if (focused) {
+ children.push("]");
+ }
+
+ return dom.div({}, ...children);
+}
+
+/*
+ * Takes an Enzyme wrapper (obtained with mount/mount/…) and
+ * returns a stringified version of the Tree, e.g.
+ *
+ * â–¼ A
+ * | â–¼ B
+ * | | â–¼ E
+ * | | | K
+ * | | | L
+ * | | F
+ * | | G
+ * | â–¼ C
+ * | | H
+ * | | I
+ * | â–¼ D
+ * | | J
+ * â–¼ M
+ * | â–¼ N
+ * | | O
+ *
+ */
+function formatTree(wrapper) {
+ const textTree = getTreeNodes(wrapper)
+ .map(node => {
+ const level = (node.prop("aria-level") || 1) - 1;
+ const indentStr = "| ".repeat(level);
+ const arrow = node.find(".arrow");
+ let arrowStr = " ";
+ if (arrow.exists()) {
+ arrowStr = arrow.hasClass("expanded") ? "▼ " : "▶︎ ";
+ }
+
+ return `${indentStr}${arrowStr}${getSanitizedNodeText(node)}`;
+ })
+ .join("\n");
+
+ // Wrap in new lines so tree nodes are aligned as expected.
+ return `\n${textTree}\n`;
+}
+
+function getSanitizedNodeText(node) {
+ // Stripping off the invisible space used in the indent.
+ return node.text().replace(/^\u200B+/, "");
+}
+
+// Encoding of the following tree/forest:
+//
+// A
+// |-- B
+// | |-- E
+// | | |-- K
+// | | `-- L
+// | |-- F
+// | `-- G
+// |-- C
+// | |-- H
+// | `-- I
+// `-- D
+// `-- J
+// M
+// `-- N
+// `-- O
+
+var TEST_TREE = {
+ children: {
+ A: ["B", "C", "D"],
+ B: ["E", "F", "G"],
+ C: ["H", "I"],
+ D: ["J"],
+ E: ["K", "L"],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+ M: ["N"],
+ N: ["O"],
+ O: [],
+ },
+ parent: {
+ A: null,
+ B: "A",
+ C: "A",
+ D: "A",
+ E: "B",
+ F: "B",
+ G: "B",
+ H: "C",
+ I: "C",
+ J: "D",
+ K: "E",
+ L: "E",
+ M: null,
+ N: "M",
+ O: "N",
+ },
+};
diff --git a/devtools/client/shared/components/test/node/jest.config.js b/devtools/client/shared/components/test/node/jest.config.js
new file mode 100644
index 0000000000..9c5a211c25
--- /dev/null
+++ b/devtools/client/shared/components/test/node/jest.config.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* global __dirname */
+
+"use strict";
+
+const sharedJestConfig = require(`${__dirname}/../../../test-helpers/shared-jest.config`);
+
+module.exports = {
+ ...sharedJestConfig,
+ setupFiles: ["<rootDir>/setup.js"],
+ snapshotSerializers: ["enzyme-to-json/serializer"],
+ testURL: "http://localhost/",
+};
diff --git a/devtools/client/shared/components/test/node/package.json b/devtools/client/shared/components/test/node/package.json
new file mode 100644
index 0000000000..9db588176e
--- /dev/null
+++ b/devtools/client/shared/components/test/node/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "devtools-client-shared-components-tests",
+ "license": "MPL-2.0",
+ "version": "0.0.1",
+ "engines": {
+ "node": ">=8.9.4"
+ },
+ "scripts": {
+ "test": "jest",
+ "test-ci": "jest --json"
+ },
+ "dependencies": {
+ "@babel/plugin-proposal-class-properties": "7.10.4",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-proposal-optional-chaining": "^7.8.3",
+ "babel-plugin-transform-amd-to-commonjs": "1.4.0",
+ "enzyme": "^3.9.0",
+ "enzyme-adapter-react-16": "^1.13.2",
+ "enzyme-to-json": "^3.3.5",
+ "jest": "^24.6.0",
+ "jsdom": "20.0.0",
+ "react": "16.4.1",
+ "react-dom": "16.4.1",
+ "react-dom-factories": "1.0.2",
+ "react-test-renderer": "16.4.1"
+ }
+}
diff --git a/devtools/client/shared/components/test/node/setup.js b/devtools/client/shared/components/test/node/setup.js
new file mode 100644
index 0000000000..570e4462ae
--- /dev/null
+++ b/devtools/client/shared/components/test/node/setup.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Configure enzyme with React 16 adapter.
+const Enzyme = require("enzyme");
+const Adapter = require("enzyme-adapter-react-16");
+Enzyme.configure({ adapter: new Adapter() });
+
+const {
+ setMocksInGlobal,
+} = require("resource://devtools/client/shared/test-helpers/shared-node-helpers.js");
+setMocksInGlobal();
diff --git a/devtools/client/shared/components/test/node/stubs/object-inspector/grip.js b/devtools/client/shared/components/test/node/stubs/object-inspector/grip.js
new file mode 100644
index 0000000000..5df79fd62d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/object-inspector/grip.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const stubs = new Map();
+
+stubs.set("proto-properties-symbols", {
+ ownProperties: {
+ a: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 1,
+ },
+ },
+ from: "server2.conn13.child19/propertyIterator160",
+ prototype: {
+ type: "object",
+ actor: "server2.conn13.child19/obj162",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 15,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownSymbols: [],
+ ownPropertiesLength: 15,
+ ownSymbolsLength: 0,
+ safeGetterValues: {},
+ },
+ },
+ ownSymbols: [
+ {
+ name: "Symbol()",
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "hello",
+ },
+ },
+ ],
+});
+
+stubs.set("longs-string-safe-getter", {
+ ownProperties: {
+ baseVal: {
+ getterValue: {
+ type: "longString",
+ initial: "data:image/png;base64,initial",
+ length: 95080,
+ actor: "server1.conn1.child1/longString28",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+ from: "server1.conn1.child1/propertyIterator30",
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/object-inspector/map.js b/devtools/client/shared/components/test/node/stubs/object-inspector/map.js
new file mode 100644
index 0000000000..ed97b51c88
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/object-inspector/map.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const stubs = new Map();
+
+stubs.set("properties", {
+ from: "server2.conn14.child18/obj30",
+ prototype: {
+ type: "object",
+ actor: "server2.conn14.child18/obj31",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 11,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownSymbols: [],
+ ownPropertiesLength: 11,
+ ownSymbolsLength: 2,
+ safeGetterValues: {},
+ },
+ },
+ ownProperties: {},
+ ownSymbols: [],
+ safeGetterValues: {
+ size: {
+ getterValue: 2,
+ getterPrototypeLevel: 2,
+ enumerable: false,
+ writable: true,
+ },
+ },
+});
+
+stubs.set("11-entries", {
+ ownProperties: {
+ "0": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-0",
+ value: "value-0",
+ },
+ },
+ },
+ "1": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-1",
+ value: "value-1",
+ },
+ },
+ },
+ "2": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-2",
+ value: "value-2",
+ },
+ },
+ },
+ "3": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-3",
+ value: "value-3",
+ },
+ },
+ },
+ "4": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-4",
+ value: "value-4",
+ },
+ },
+ },
+ "5": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-5",
+ value: "value-5",
+ },
+ },
+ },
+ "6": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-6",
+ value: "value-6",
+ },
+ },
+ },
+ "7": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-7",
+ value: "value-7",
+ },
+ },
+ },
+ "8": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-8",
+ value: "value-8",
+ },
+ },
+ },
+ "9": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-9",
+ value: "value-9",
+ },
+ },
+ },
+ "10": {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: "key-10",
+ value: "value-10",
+ },
+ },
+ },
+ },
+ from: "server4.conn4.child19/propertyIterator54",
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/object-inspector/performance.js b/devtools/client/shared/components/test/node/stubs/object-inspector/performance.js
new file mode 100644
index 0000000000..5488070f14
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/object-inspector/performance.js
@@ -0,0 +1,784 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+const stubs = new Map();
+
+stubs.set("performance", {
+ from: "server2.conn4.child1/obj30",
+ prototype: {
+ type: "object",
+ actor: "server2.conn4.child1/obj33",
+ class: "PerformancePrototype",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 16,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ now: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj34",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "now",
+ displayName: "now",
+ },
+ },
+ getEntries: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj35",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "getEntries",
+ displayName: "getEntries",
+ },
+ },
+ getEntriesByType: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj36",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "getEntriesByType",
+ displayName: "getEntriesByType",
+ },
+ },
+ getEntriesByName: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj37",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "getEntriesByName",
+ displayName: "getEntriesByName",
+ },
+ },
+ clearResourceTimings: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj38",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "clearResourceTimings",
+ displayName: "clearResourceTimings",
+ },
+ },
+ setResourceTimingBufferSize: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj39",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "setResourceTimingBufferSize",
+ displayName: "setResourceTimingBufferSize",
+ },
+ },
+ mark: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj40",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "mark",
+ displayName: "mark",
+ },
+ },
+ clearMarks: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj41",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "clearMarks",
+ displayName: "clearMarks",
+ },
+ },
+ measure: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj42",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "measure",
+ displayName: "measure",
+ },
+ },
+ clearMeasures: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj43",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "clearMeasures",
+ displayName: "clearMeasures",
+ },
+ },
+ },
+ ownPropertiesLength: 16,
+ },
+ },
+ ownProperties: {
+ userTimingJsNow: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: false,
+ },
+ userTimingJsNowPrefixed: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: false,
+ },
+ userTimingJsUserTiming: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: false,
+ },
+ userTimingJsUserTimingPrefixed: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: false,
+ },
+ userTimingJsPerformanceTimeline: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: false,
+ },
+ userTimingJsPerformanceTimelinePrefixed: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: false,
+ },
+ timeOrigin: {
+ enumerable: true,
+ writable: true,
+ value: 1500971976372.9033,
+ },
+ timing: {
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj44",
+ class: "PerformanceTiming",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ navigationStart: {
+ getterValue: 1500971976373,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ unloadEventStart: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ unloadEventEnd: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectStart: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectEnd: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ fetchStart: {
+ getterValue: 1500971982226,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domainLookupStart: {
+ getterValue: 1500971982251,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domainLookupEnd: {
+ getterValue: 1500971982255,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ connectStart: {
+ getterValue: 1500971982255,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ connectEnd: {
+ getterValue: 1500971982638,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+ },
+ },
+ },
+ navigation: {
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn4.child1/obj45",
+ class: "PerformanceNavigation",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ type: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectCount: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+ },
+ },
+ },
+ onresourcetimingbufferfull: {
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "null",
+ },
+ },
+ },
+ safeGetterValues: {
+ timeOrigin: {
+ getterValue: 1500971976372.9033,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ timing: {
+ getterValue: {
+ type: "object",
+ actor: "server2.conn4.child1/obj44",
+ class: "PerformanceTiming",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ navigationStart: {
+ getterValue: 1500971976373,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ unloadEventStart: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ unloadEventEnd: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectStart: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectEnd: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ fetchStart: {
+ getterValue: 1500971982226,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domainLookupStart: {
+ getterValue: 1500971982251,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domainLookupEnd: {
+ getterValue: 1500971982255,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ connectStart: {
+ getterValue: 1500971982255,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ connectEnd: {
+ getterValue: 1500971982638,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+ },
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ navigation: {
+ getterValue: {
+ type: "object",
+ actor: "server2.conn4.child1/obj45",
+ class: "PerformanceNavigation",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ type: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectCount: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+ },
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onresourcetimingbufferfull: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+});
+
+stubs.set("timing", {
+ from: "server1.conn1.child1/obj31",
+ prototype: {
+ type: "object",
+ actor: "server1.conn1.child1/obj32",
+ class: "PerformanceTimingPrototype",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 23,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ toJSON: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj33",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "toJSON",
+ displayName: "toJSON",
+ },
+ },
+ navigationStart: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj34",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get navigationStart",
+ displayName: "get navigationStart",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ unloadEventStart: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj35",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get unloadEventStart",
+ displayName: "get unloadEventStart",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ unloadEventEnd: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj36",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get unloadEventEnd",
+ displayName: "get unloadEventEnd",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ redirectStart: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj37",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get redirectStart",
+ displayName: "get redirectStart",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ redirectEnd: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj38",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get redirectEnd",
+ displayName: "get redirectEnd",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ fetchStart: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj39",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get fetchStart",
+ displayName: "get fetchStart",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ domainLookupStart: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj40",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get domainLookupStart",
+ displayName: "get domainLookupStart",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ domainLookupEnd: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj41",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get domainLookupEnd",
+ displayName: "get domainLookupEnd",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ connectStart: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server1.conn1.child1/obj42",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get connectStart",
+ displayName: "get connectStart",
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ },
+ ownPropertiesLength: 23,
+ },
+ },
+ ownProperties: {},
+ safeGetterValues: {
+ navigationStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ unloadEventStart: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ unloadEventEnd: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectStart: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ redirectEnd: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ fetchStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domainLookupStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domainLookupEnd: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ connectStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ connectEnd: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ secureConnectionStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ requestStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ responseStart: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ responseEnd: {
+ getterValue: 1500967716401,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domLoading: {
+ getterValue: 1500967716426,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domInteractive: {
+ getterValue: 1500967716552,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domContentLoadedEventStart: {
+ getterValue: 1500967716696,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domContentLoadedEventEnd: {
+ getterValue: 1500967716715,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ domComplete: {
+ getterValue: 1500967716719,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ loadEventStart: {
+ getterValue: 1500967716719,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ loadEventEnd: {
+ getterValue: 1500967716720,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/accessible.js b/devtools/client/shared/components/test/node/stubs/reps/accessible.js
new file mode 100644
index 0000000000..3c9834e6d3
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/accessible.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("Document", {
+ actor: "server1.conn1.child1/accessible31",
+ typeName: "accessible",
+ preview: {
+ name: "New Tab",
+ role: "document",
+ isConnected: true,
+ },
+});
+
+stubs.set("ButtonMenu", {
+ actor: "server1.conn1.child1/accessible38",
+ typeName: "accessible",
+ preview: {
+ name: "New to Nightly? Let’s get started.",
+ role: "buttonmenu",
+ isConnected: true,
+ },
+});
+
+stubs.set("NoName", {
+ actor: "server1.conn1.child1/accessible93",
+ typeName: "accessible",
+ preview: {
+ name: null,
+ role: "text container",
+ isConnected: true,
+ },
+});
+
+stubs.set("NoPreview", {
+ actor: "server1.conn1.child1/accessible93",
+ typeName: "accessible",
+});
+
+stubs.set("DisconnectedAccessible", {
+ actor: null,
+ typeName: "accessible",
+ preview: {
+ name: null,
+ role: "section",
+ isConnected: false,
+ },
+});
+
+const name = "a".repeat(1000);
+stubs.set("AccessibleWithLongName", {
+ actor: "server1.conn1.child1/accessible98",
+ typeName: "accessible",
+ preview: {
+ name,
+ role: "text leaf",
+ isConnected: true,
+ },
+});
+
+stubs.set("PushButton", {
+ actor: "server1.conn1.child1/accessible157",
+ typeName: "accessible",
+ preview: {
+ name: "Search",
+ role: "pushbutton",
+ isConnected: true,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/accessor.js b/devtools/client/shared/components/test/node/stubs/reps/accessor.js
new file mode 100644
index 0000000000..cee4a836ce
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/accessor.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+
+stubs.set("getter", {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server2.conn1.child1/obj106",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get x",
+ displayName: "get x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ set: {
+ type: "undefined",
+ },
+});
+
+stubs.set("setter", {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "undefined",
+ },
+ set: {
+ type: "object",
+ actor: "server2.conn1.child1/obj116",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "set x",
+ displayName: "set x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+});
+
+stubs.set("getter setter", {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server2.conn1.child1/obj127",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get x",
+ displayName: "get x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ set: {
+ type: "object",
+ actor: "server2.conn1.child1/obj128",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "set x",
+ displayName: "set x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+});
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/attribute.js b/devtools/client/shared/components/test/node/stubs/reps/attribute.js
new file mode 100644
index 0000000000..d820da07f7
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/attribute.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Attribute`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj24",
+ "class": "Attr",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "isConnected": false,
+ "value": "autocomplete-suggestions"
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.4985715593006155
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj24"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/big-int.js b/devtools/client/shared/components/test/node/stubs/reps/big-int.js
new file mode 100644
index 0000000000..423145359b
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/big-int.js
@@ -0,0 +1,196 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("1n", {
+ type: "BigInt",
+ text: "1",
+});
+
+stubs.set("-2n", {
+ type: "BigInt",
+ text: "-2",
+});
+
+stubs.set("0n", {
+ type: "BigInt",
+ text: "0",
+});
+
+stubs.set("[1n,-2n,0n]", {
+ type: "object",
+ actor: "server1.conn15.child1/obj27",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [
+ {
+ type: "BigInt",
+ text: "1",
+ },
+ {
+ type: "BigInt",
+ text: "-2",
+ },
+ {
+ type: "BigInt",
+ text: "0",
+ },
+ ],
+ },
+});
+
+stubs.set("new Set([1n,-2n,0n])", {
+ type: "object",
+ actor: "server1.conn15.child1/obj29",
+ class: "Set",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [
+ {
+ type: "BigInt",
+ text: "1",
+ },
+ {
+ type: "BigInt",
+ text: "-2",
+ },
+ {
+ type: "BigInt",
+ text: "0",
+ },
+ ],
+ },
+});
+
+stubs.set("new Map([ [1n, -1n], [-2n, 0n], [0n, -2n]])", {
+ type: "object",
+ actor: "server1.conn15.child1/obj32",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 3,
+ entries: [
+ [
+ {
+ type: "BigInt",
+ text: "1",
+ },
+ {
+ type: "BigInt",
+ text: "-1",
+ },
+ ],
+ [
+ {
+ type: "BigInt",
+ text: "-2",
+ },
+ {
+ type: "BigInt",
+ text: "0",
+ },
+ ],
+ [
+ {
+ type: "BigInt",
+ text: "0",
+ },
+ {
+ type: "BigInt",
+ text: "-2",
+ },
+ ],
+ ],
+ },
+});
+
+stubs.set("({simple: 1n, negative: -2n, zero: 0n})", {
+ type: "object",
+ actor: "server1.conn15.child1/obj34",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 3,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ simple: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "BigInt",
+ text: "1",
+ },
+ },
+ negative: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "BigInt",
+ text: "-2",
+ },
+ },
+ zero: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "BigInt",
+ text: "0",
+ },
+ },
+ },
+ ownSymbols: [],
+ ownPropertiesLength: 3,
+ ownSymbolsLength: 0,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("Promise.resolve(1n)", {
+ type: "object",
+ actor: "server1.conn15.child1/obj36",
+ class: "Promise",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: {
+ type: "BigInt",
+ text: "1",
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js b/devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js
new file mode 100644
index 0000000000..8a9353cd7e
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is a fake test so we can have support files in the stubs.ini, which are then
+// referenced as support files in the webconsole mochitest ini file.
+
+"use strict";
+
+add_task(function() {
+ ok(true, "this is not a test");
+});
diff --git a/devtools/client/shared/components/test/node/stubs/reps/comment-node.js b/devtools/client/shared/components/test/node/stubs/reps/comment-node.js
new file mode 100644
index 0000000000..5ee0970283
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/comment-node.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Comment`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal4294967299/obj26",
+ "class": "Comment",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 8,
+ "nodeName": "#comment",
+ "isConnected": false,
+ "textContent": "test\nand test\nand test\nand test\nand test\nand test\nand test"
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.7876406289746626
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal4294967299/obj26"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/date-time.js b/devtools/client/shared/components/test/node/stubs/reps/date-time.js
new file mode 100644
index 0000000000..9027397104
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/date-time.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`DateTime`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal4294967299/obj28",
+ "class": "Date",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "timestamp": 1459372644859
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal4294967299/obj28"
+});
+
+stubs.set(`InvalidDateTime`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal4294967299/obj30",
+ "class": "Date",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "timestamp": {
+ "type": "NaN"
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal4294967299/obj30"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/document-type.js b/devtools/client/shared/components/test/node/stubs/reps/document-type.js
new file mode 100644
index 0000000000..a499670b58
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/document-type.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("html", {
+ type: "object",
+ actor: "server1.conn7.child1/obj195",
+ class: "DocumentType",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 10,
+ nodeName: "html",
+ isConnected: true,
+ },
+});
+
+stubs.set("unnamed", {
+ type: "object",
+ actor: "server1.conn7.child1/obj195",
+ class: "DocumentType",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 10,
+ nodeName: "",
+ isConnected: true,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/document.js b/devtools/client/shared/components/test/node/stubs/reps/document.js
new file mode 100644
index 0000000000..555bbff2d1
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/document.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("Document", {
+ type: "object",
+ class: "HTMLDocument",
+ actor: "server1.conn17.obj115",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 9,
+ nodeName: "#document",
+ location: "https://www.mozilla.org/en-US/firefox/new/",
+ },
+});
+
+stubs.set("Location-less Document", {
+ type: "object",
+ actor: "server1.conn6.child1/obj31",
+ class: "HTMLDocument",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 9,
+ nodeName: "#document",
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/element-node.js b/devtools/client/shared/components/test/node/stubs/reps/element-node.js
new file mode 100644
index 0000000000..b0b4369b3c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/element-node.js
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("BodyNode", {
+ type: "object",
+ actor: "server1.conn1.child1/obj30",
+ class: "HTMLBodyElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "body",
+ attributes: {
+ class: "body-class",
+ id: "body-id",
+ },
+ attributesLength: 2,
+ },
+});
+
+stubs.set("DocumentElement", {
+ type: "object",
+ actor: "server1.conn1.child1/obj40",
+ class: "HTMLHtmlElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "html",
+ attributes: {
+ dir: "ltr",
+ lang: "en-US",
+ },
+ attributesLength: 2,
+ },
+});
+
+stubs.set("Node", {
+ type: "object",
+ actor: "server1.conn2.child1/obj116",
+ class: "HTMLInputElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "input",
+ isConnected: true,
+ attributes: {
+ id: "newtab-customize-button",
+ dir: "ltr",
+ title: "Customize your New Tab page",
+ class: "bar baz",
+ value: "foo",
+ type: "button",
+ },
+ attributesLength: 6,
+ },
+});
+
+stubs.set("DisconnectedNode", {
+ type: "object",
+ actor: "server1.conn2.child1/obj116",
+ class: "HTMLInputElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "input",
+ isConnected: false,
+ attributes: {
+ id: "newtab-customize-button",
+ dir: "ltr",
+ title: "Customize your New Tab page",
+ class: "bar baz",
+ value: "foo",
+ type: "button",
+ },
+ attributesLength: 6,
+ },
+});
+
+stubs.set("NodeWithLeadingAndTrailingSpacesClassName", {
+ type: "object",
+ actor: "server1.conn3.child1/obj59",
+ class: "HTMLBodyElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "body",
+ attributes: {
+ id: "nightly-whatsnew",
+ class: " html-ltr ",
+ },
+ attributesLength: 2,
+ },
+});
+
+stubs.set("NodeWithSpacesInClassName", {
+ type: "object",
+ actor: "server1.conn3.child1/obj59",
+ class: "HTMLBodyElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "body",
+ attributes: {
+ class: "a b c",
+ },
+ attributesLength: 1,
+ },
+});
+
+stubs.set("NodeWithoutAttributes", {
+ type: "object",
+ actor: "server1.conn1.child1/obj32",
+ class: "HTMLParagraphElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "p",
+ attributes: {},
+ attributesLength: 1,
+ },
+});
+
+stubs.set("LotsOfAttributes", {
+ type: "object",
+ actor: "server1.conn2.child1/obj30",
+ class: "HTMLParagraphElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "p",
+ attributes: {
+ id: "lots-of-attributes",
+ a: "",
+ b: "",
+ c: "",
+ d: "",
+ e: "",
+ f: "",
+ g: "",
+ h: "",
+ i: "",
+ j: "",
+ k: "",
+ l: "",
+ m: "",
+ n: "",
+ },
+ attributesLength: 15,
+ },
+});
+
+stubs.set("SvgNode", {
+ type: "object",
+ actor: "server1.conn1.child1/obj42",
+ class: "SVGClipPathElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "clipPath",
+ attributes: {
+ id: "clip",
+ class: "svg-element",
+ },
+ attributesLength: 0,
+ },
+});
+
+stubs.set("SvgNodeInXHTML", {
+ type: "object",
+ actor: "server1.conn3.child1/obj34",
+ class: "SVGCircleElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "svg:circle",
+ attributes: {
+ class: "svg-element",
+ cx: "0",
+ cy: "0",
+ r: "5",
+ },
+ attributesLength: 3,
+ },
+});
+
+stubs.set("NodeWithLongAttribute", {
+ type: "object",
+ actor: "server1.conn1.child1/obj32",
+ class: "HTMLParagraphElement",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "p",
+ attributes: {
+ "data-test": "a".repeat(100),
+ },
+ attributesLength: 1,
+ },
+});
+
+const initialText = "a".repeat(1000);
+stubs.set("NodeWithLongStringAttribute", {
+ type: "object",
+ actor: "server1.conn1.child1/obj28",
+ class: "HTMLDivElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "div",
+ isConnected: false,
+ attributes: {
+ "data-test": {
+ type: "longString",
+ initial: initialText,
+ length: 50000,
+ actor: "server1.conn1.child1/longString29",
+ },
+ },
+ attributesLength: 1,
+ },
+});
+
+stubs.set("MarkerPseudoElement", {
+ type: "object",
+ actor: "server1.conn1.child1/obj26",
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "_moz_generated_content_marker",
+ attributes: {},
+ attributesLength: 0,
+ isMarkerPseudoElement: true,
+ },
+});
+
+stubs.set("BeforePseudoElement", {
+ type: "object",
+ actor: "server1.conn1.child1/obj27",
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "_moz_generated_content_before",
+ attributes: {},
+ attributesLength: 0,
+ isBeforePseudoElement: true,
+ },
+});
+
+stubs.set("AfterPseudoElement", {
+ type: "object",
+ actor: "server1.conn1.child1/obj28",
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "_moz_generated_content_after",
+ attributes: {},
+ attributesLength: 0,
+ isAfterPseudoElement: true,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/error.js b/devtools/client/shared/components/test/node/stubs/reps/error.js
new file mode 100644
index 0000000000..ea4b7ac4ee
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/error.js
@@ -0,0 +1,396 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("SimpleError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1020",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "Error message",
+ stack: "@debugger eval code:1:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 1,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("MultilineStackError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1021",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "bar",
+ stack:
+ "errorBar@debugger eval code:6:15\n" +
+ "errorFoo@debugger eval code:3:3\n" +
+ "@debugger eval code:8:1\n",
+ fileName: "debugger eval code",
+ lineNumber: 6,
+ columnNumber: 15,
+ },
+});
+
+stubs.set("ErrorWithoutStacktrace", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1020",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "Error message",
+ },
+});
+
+stubs.set("EvalError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1022",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "EvalError",
+ message: "EvalError message",
+ stack: "@debugger eval code:10:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 10,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("InternalError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1023",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "InternalError",
+ message: "InternalError message",
+ stack: "@debugger eval code:11:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 11,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("RangeError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1024",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "RangeError",
+ message: "RangeError message",
+ stack: "@debugger eval code:12:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 12,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("ReferenceError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1025",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "ReferenceError",
+ message: "ReferenceError message",
+ stack: "@debugger eval code:13:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 13,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("SyntaxError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1026",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "SyntaxError",
+ message: "SyntaxError message",
+ stack: "@debugger eval code:14:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 14,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("TypeError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1027",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "TypeError",
+ message: "TypeError message",
+ stack: "@debugger eval code:15:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 15,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("URIError", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1028",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "URIError",
+ message: "URIError message",
+ stack: "@debugger eval code:16:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 16,
+ columnNumber: 13,
+ },
+});
+
+/**
+ * Example code:
+ * try {
+ * var foo = document.querySelector("foo;()bar!");
+ * } catch (ex) {
+ * ex;
+ * }
+ */
+stubs.set("DOMException", {
+ type: "object",
+ actor: "server2.conn2.child3/obj32",
+ class: "DOMException",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMException",
+ name: "SyntaxError",
+ message: "'foo;()bar!' is not a valid selector",
+ code: 12,
+ result: 2152923148,
+ filename: "debugger eval code",
+ lineNumber: 1,
+ columnNumber: 0,
+ },
+});
+
+stubs.set("base-loader Error", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1020",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "Error message",
+ stack:
+ "onPacket@resource://devtools/shared/base-loader.sys.mjs -> resource://devtools/client/debugger-client.js:856:9\n" +
+ "send/<@resource://devtools/shared/base-loader.sys.mjs -> resource://devtools/shared/transport/transport.js:569:13\n" +
+ "exports.makeInfallible/<@resource://devtools/shared/base-loader.sys.mjs -> resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14\n" +
+ "exports.makeInfallible/<@resource://devtools/shared/base-loader.sys.mjs -> resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14\n",
+ fileName: "debugger-client.js",
+ lineNumber: 859,
+ columnNumber: 9,
+ },
+});
+
+stubs.set("longString stack Error", {
+ type: "object",
+ actor: "server1.conn2.child1/obj33",
+ class: "Error",
+ isError: true,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "",
+ stack: {
+ type: "longString",
+ initial:
+ "NgForOf.prototype.ngOnChanges@webpack-internal:///./node_modules/@angular/common/esm5/common.js:2656:27\n checkAndUpdateDirectiveInline@webpack-internal:///./node_modules/@angular/core/esm5/core.js:12581:9\n checkAndUpdateNodeInline@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14109:20\n checkAndUpdateNode@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14052:16\n debugCheckAndUpdateNode@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14945:55\n debugCheckDirectivesFn@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14886:13\n View_MetaTableComponent_6/<@ng:///AppModule/MetaTableComponent.ngfactory.js:98:5\n debugUpdateDirectives@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14871:12\n checkAndUpdateView@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14018:5\n callViewAction@webpack-internal:///./node_modules/@angular/core/esm5/core.js:14369:21\n execEmbeddedViewsAction@webpack-internal:///./node_modules/@an",
+ length: 11907,
+ actor: "server1.conn2.child1/longString31",
+ },
+ fileName: "debugger eval code",
+ lineNumber: 1,
+ columnNumber: 5,
+ },
+});
+
+stubs.set("longString stack Error - cut-off location", {
+ type: "object",
+ actor: "server1.conn1.child1/obj33",
+ class: "Error",
+ isError: true,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 6,
+ preview: {
+ kind: "Error",
+ name: "InternalError",
+ message: "too much recursion",
+ stack: {
+ type: "longString",
+ initial:
+ "execute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:32:1\nexecute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21\nexecute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21\nexecute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21\nexecute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21\nexecute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21\nexecute/AppComponent</AppComponent.prototype.doStuff@https://angular-3eqab4.stackblitz.io/tmp/appfiles/src/app/app.component.ts:33:21\nexecute/AppComponent</AppComponent.prototype.doStuff@https://an",
+ length: 17151,
+ actor: "server1.conn1.child1/longString27",
+ },
+ fileName:
+ "https://c.staticblitz.com/assets/engineblock-bc7b07e99ec5c6739c766b4898e4cff5acfddc137ccb7218377069c32731f1d0.js line 1 > eval",
+ lineNumber: 32,
+ columnNumber: 1,
+ },
+});
+
+stubs.set("Error with V8-like stack", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1020",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "BOOM",
+ stack: "Error: BOOM\ngetAccount@http://moz.com/script.js:1:2",
+ fileName: "http://moz.com/script.js:1:2",
+ lineNumber: 1,
+ columnNumber: 2,
+ },
+});
+
+stubs.set("Error with invalid stack", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1020",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "bad stack",
+ stack: "bar\nbaz\nfoo\n\n\n\n\n\n\n",
+ fileName: "http://moz.com/script.js:1:2",
+ lineNumber: 1,
+ columnNumber: 2,
+ },
+});
+
+stubs.set("Error with undefined-grip stack", {
+ type: "object",
+ actor: "server0.conn0.child1/obj88",
+ class: "Error",
+ isError: true,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "InternalError",
+ message: "too much recursion",
+ stack: {
+ type: "undefined",
+ },
+ fileName: "debugger eval code",
+ lineNumber: 13,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("Error with undefined-grip name", {
+ type: "object",
+ actor: "server0.conn0.child1/obj88",
+ class: "Error",
+ isError: true,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: {
+ type: "undefined",
+ },
+ message: "too much recursion",
+ stack: "@debugger eval code:16:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 13,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("Error with undefined-grip message", {
+ type: "object",
+ actor: "server0.conn0.child1/obj88",
+ class: "Error",
+ isError: true,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ message: { type: "undefined" },
+ stack: "@debugger eval code:16:13\n",
+ fileName: "debugger eval code",
+ lineNumber: 13,
+ columnNumber: 13,
+ },
+});
+
+stubs.set("Error with stack having frames with multiple @", {
+ type: "object",
+ actor: "server1.conn1.child1/obj1021",
+ class: "Error",
+ isError: true,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Error",
+ name: "Error",
+ message: "bar",
+ stack:
+ "errorBar@https://example.com/turbo/from-npm.js@0.8.26/dist/from-npm.js:814:31\n" +
+ "errorFoo@https://example.com/turbo/from-npm.js@0.8.26/dist/from-npm.js:815:31\n" +
+ "@https://example.com/turbo/from-npm.js@0.8.26/dist/from-npm.js:816:31\n",
+ fileName: "from-npm.js",
+ lineNumber: 6,
+ columnNumber: 15,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/event.js b/devtools/client/shared/components/test/node/stubs/reps/event.js
new file mode 100644
index 0000000000..c0c49c5e42
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/event.js
@@ -0,0 +1,269 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+
+stubs.set("testEvent", {
+ type: "object",
+ class: "Event",
+ actor: "server1.conn23.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMEvent",
+ type: "beforeprint",
+ properties: {
+ isTrusted: true,
+ currentTarget: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn23.obj37",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "http://example.com",
+ },
+ },
+ eventPhase: 2,
+ bubbles: false,
+ cancelable: false,
+ defaultPrevented: false,
+ timeStamp: 1466780008434005,
+ originalTarget: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn23.obj38",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "http://example.com",
+ },
+ },
+ explicitOriginalTarget: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn23.obj39",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "http://example.com",
+ },
+ },
+ NONE: 0,
+ },
+ target: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn23.obj36",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "http://example.com",
+ },
+ },
+ },
+});
+
+stubs.set("testMouseEvent", {
+ type: "object",
+ class: "MouseEvent",
+ actor: "server1.conn20.obj39",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMEvent",
+ type: "click",
+ properties: {
+ buttons: 0,
+ clientX: 62,
+ clientY: 18,
+ layerX: 0,
+ layerY: 0,
+ },
+ target: {
+ type: "object",
+ class: "HTMLDivElement",
+ actor: "server1.conn20.obj40",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "div",
+ isConnected: true,
+ attributes: {
+ id: "test",
+ },
+ attributesLength: 1,
+ },
+ },
+ },
+});
+
+stubs.set("testKeyboardEvent", {
+ type: "object",
+ class: "KeyboardEvent",
+ actor: "server1.conn21.obj49",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMEvent",
+ type: "keyup",
+ properties: {
+ key: "Control",
+ charCode: 0,
+ keyCode: 17,
+ },
+ target: {
+ type: "object",
+ class: "HTMLBodyElement",
+ actor: "server1.conn21.obj50",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "body",
+ attributes: {},
+ attributesLength: 0,
+ },
+ },
+ eventKind: "key",
+ modifiers: [],
+ },
+});
+
+stubs.set("testKeyboardEventWithModifiers", {
+ type: "object",
+ class: "KeyboardEvent",
+ actor: "server1.conn21.obj49",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMEvent",
+ type: "keyup",
+ properties: {
+ key: "M",
+ charCode: 0,
+ keyCode: 77,
+ },
+ target: {
+ type: "object",
+ class: "HTMLBodyElement",
+ actor: "server1.conn21.obj50",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "body",
+ attributes: {},
+ attributesLength: 0,
+ },
+ },
+ eventKind: "key",
+ modifiers: ["Meta", "Shift"],
+ },
+});
+
+stubs.set("testMessageEvent", {
+ type: "object",
+ class: "MessageEvent",
+ actor: "server1.conn3.obj34",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "DOMEvent",
+ type: "message",
+ properties: {
+ isTrusted: false,
+ data: "test data",
+ origin: "null",
+ lastEventId: "",
+ source: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn3.obj36",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "",
+ },
+ },
+ ports: {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn3.obj37",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ },
+ currentTarget: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn3.obj38",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "",
+ },
+ },
+ eventPhase: 2,
+ bubbles: false,
+ cancelable: false,
+ },
+ target: {
+ type: "object",
+ class: "Window",
+ actor: "server1.conn3.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 760,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "http://example.com",
+ },
+ },
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/failure.js b/devtools/client/shared/components/test/node/stubs/reps/failure.js
new file mode 100644
index 0000000000..f839d5eab5
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/failure.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("Failure", {
+ type: "object",
+ class: "RegExp",
+ actor: "server1.conn22.obj39",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ get displayString() {
+ throw new Error("failure");
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/function.js b/devtools/client/shared/components/test/node/stubs/reps/function.js
new file mode 100644
index 0000000000..a0ba6ddf8d
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/function.js
@@ -0,0 +1,227 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("Named", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn6.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: false,
+ name: "testName",
+ displayName: "testName",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("UserNamed", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn6.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: false,
+ name: "testName",
+ userDisplayName: "testUserName",
+ displayName: "testName",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("VarNamed", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj41",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: false,
+ displayName: "testVarName",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("Anon", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj45",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: false,
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("LongName", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj67",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: false,
+ name:
+ "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooong",
+ displayName:
+ "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+ "oooooooooooooooooooooooooooooooooooooooooong",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("AsyncFunction", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj45",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: true,
+ isGenerator: false,
+ name: "waitUntil2017",
+ displayName: "waitUntil2017",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("AnonAsyncFunction", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj45",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: true,
+ isGenerator: false,
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("GeneratorFunction", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj45",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: true,
+ name: "fib",
+ displayName: "fib",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("AnonGeneratorFunction", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj45",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAsync: false,
+ isGenerator: true,
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("getRandom", {
+ type: "object",
+ actor: "server1.conn7.child1/obj984",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 3,
+ name: "getRandom",
+ displayName: "getRandom",
+ location: {
+ url: "https://nchevobbe.github.io/demo/console-test-app.html",
+ line: 314,
+ },
+});
+
+stubs.set("EvaledInDebuggerFunction", {
+ type: "object",
+ actor: "server1.conn2.child1/obj29",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 3,
+ name: "evaledInDebugger",
+ displayName: "evaledInDebugger",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("ObjectProperty", {
+ type: "object",
+ class: "Function",
+ actor: "server1.conn7.obj45",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isAync: false,
+ isGenerator: false,
+ name: "$",
+ displayName: "jQuery",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+});
+
+stubs.set("EmptyClass", {
+ actor: "server0.conn0.child1/obj27",
+ class: "Function",
+ displayName: "EmptyClass",
+ extensible: true,
+ frozen: false,
+ isAsync: false,
+ isClassConstructor: true,
+ isGenerator: false,
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ name: "EmptyClass",
+ parameterNames: [],
+ sealed: false,
+ type: "object",
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/grip-array.js b/devtools/client/shared/components/test/node/stubs/reps/grip-array.js
new file mode 100644
index 0000000000..93b07fa136
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/grip-array.js
@@ -0,0 +1,1087 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ maxLengthMap,
+} = require("resource://devtools/client/shared/components/reps/reps/grip-array.js");
+const stubs = new Map();
+
+stubs.set("testBasic", {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn0.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "ArrayLike",
+ length: 0,
+ items: [],
+ },
+});
+
+stubs.set("DOMTokenList", {
+ type: "object",
+ actor: "server2.conn4.child12/obj39",
+ class: "DOMTokenList",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 0,
+ items: [],
+ },
+});
+
+stubs.set("testMaxProps", {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn1.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [
+ 1,
+ "foo",
+ {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn1.obj36",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ },
+ ],
+ },
+});
+
+stubs.set("testMoreThanShortMaxProps", {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn1.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: maxLengthMap.get(MODE.SHORT) + 1,
+ preview: {
+ kind: "ArrayLike",
+ length: maxLengthMap.get(MODE.SHORT) + 1,
+ items: new Array(maxLengthMap.get(MODE.SHORT) + 1).fill("test string"),
+ },
+});
+
+stubs.set("testMoreThanLongMaxProps", {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn1.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: maxLengthMap.get(MODE.LONG) + 1,
+ items: new Array(maxLengthMap.get(MODE.LONG) + 1).fill("test string"),
+ },
+});
+
+stubs.set("testPreviewLimit", {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn1.obj31",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 12,
+ preview: {
+ kind: "ArrayLike",
+ length: 11,
+ items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ },
+});
+
+stubs.set("testRecursiveArray", {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn3.obj42",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ preview: {
+ kind: "ArrayLike",
+ length: 1,
+ items: [
+ {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn3.obj43",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ preview: {
+ kind: "ArrayLike",
+ length: 1,
+ },
+ },
+ ],
+ },
+});
+
+stubs.set("testNamedNodeMap", {
+ type: "object",
+ class: "NamedNodeMap",
+ actor: "server1.conn3.obj42",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 6,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [
+ {
+ type: "object",
+ class: "Attr",
+ actor: "server1.conn3.obj43",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 2,
+ nodeName: "class",
+ value: "myclass",
+ },
+ },
+ {
+ type: "object",
+ class: "Attr",
+ actor: "server1.conn3.obj44",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 2,
+ nodeName: "cellpadding",
+ value: "7",
+ },
+ },
+ {
+ type: "object",
+ class: "Attr",
+ actor: "server1.conn3.obj44",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 2,
+ nodeName: "border",
+ value: "3",
+ },
+ },
+ ],
+ },
+});
+
+stubs.set("testNodeList", {
+ type: "object",
+ actor: "server1.conn1.child1/obj51",
+ class: "NodeList",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 3,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj52",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj53",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj54",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-3",
+ class: "btn btn-count",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ },
+});
+
+stubs.set("testDisconnectedNodeList", {
+ type: "object",
+ actor: "server1.conn1.child1/obj51",
+ class: "NodeList",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 3,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ items: [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj52",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj53",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj54",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-3",
+ class: "btn btn-count",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ },
+});
+
+stubs.set("testDocumentFragment", {
+ type: "object",
+ actor: "server1.conn1.child1/obj45",
+ class: "DocumentFragment",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 11,
+ nodeName: "#document-fragment",
+ childNodesLength: 5,
+ childNodes: [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj46",
+ class: "HTMLLIElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "li",
+ attributes: {
+ id: "li-0",
+ class: "list-element",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj47",
+ class: "HTMLLIElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "li",
+ attributes: {
+ id: "li-1",
+ class: "list-element",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj48",
+ class: "HTMLLIElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "li",
+ attributes: {
+ id: "li-2",
+ class: "list-element",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj49",
+ class: "HTMLLIElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "li",
+ attributes: {
+ id: "li-3",
+ class: "list-element",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj50",
+ class: "HTMLLIElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "li",
+ attributes: {
+ id: "li-4",
+ class: "list-element",
+ },
+ attributesLength: 2,
+ },
+ },
+ ],
+ },
+});
+
+stubs.set("Array(5)", {
+ type: "object",
+ actor: "server1.conn4.child1/obj33",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "ArrayLike",
+ length: 5,
+ items: [null, null, null, null, null],
+ },
+});
+
+stubs.set("[,1,2,3]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj35",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 4,
+ items: [null, 1, 2, 3],
+ },
+});
+
+stubs.set("[,,,3,4,5]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj37",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 6,
+ items: [null, null, null, 3, 4, 5],
+ },
+});
+
+stubs.set("[0,1,,3,4,5]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj65",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 6,
+ preview: {
+ kind: "ArrayLike",
+ length: 6,
+ items: [0, 1, null, 3, 4, 5],
+ },
+});
+
+stubs.set("[0,1,,,,5]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj83",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 6,
+ items: [0, 1, null, null, null, 5],
+ },
+});
+
+stubs.set("[0,,2,,4,5]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj85",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 5,
+ preview: {
+ kind: "ArrayLike",
+ length: 6,
+ items: [0, null, 2, null, 4, 5],
+ },
+});
+
+stubs.set("[0,,,3,,,,7,8]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj87",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 5,
+ preview: {
+ kind: "ArrayLike",
+ length: 9,
+ items: [0, null, null, 3, null, null, null, 7, 8],
+ },
+});
+
+stubs.set("[0,1,2,3,4,,]", {
+ type: "object",
+ actor: "server1.conn4.child1/obj89",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 6,
+ preview: {
+ kind: "ArrayLike",
+ length: 6,
+ items: [0, 1, 2, 3, 4, null],
+ },
+});
+
+stubs.set("[0,1,2,,,,]", {
+ type: "object",
+ actor: "server1.conn13.child1/obj88",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 6,
+ items: [0, 1, 2, null, null, null],
+ },
+});
+
+// We can have cases where we don't have the array items in the preview,
+// (e.g. in the packet for `Promise.resolve([1, 2, 3])`), but we have the
+// length of the array.
+stubs.set("testItemsNotInPreview", {
+ type: "object",
+ actor: "server2.conn0.child1/obj135",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ },
+});
+
+stubs.set("new Set([1,2,3,4])", {
+ type: "object",
+ actor: "server2.conn8.child18/obj30",
+ class: "Set",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 4,
+ items: [1, 2, 3, 4],
+ },
+});
+
+stubs.set("new Set([0,1,2,…,19])", {
+ type: "object",
+ actor: "server2.conn8.child18/obj42",
+ class: "Set",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 20,
+ items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ },
+});
+
+stubs.set("new WeakSet(document.querySelectorAll('button:nth-child(3n)'))", {
+ type: "object",
+ actor: "server2.conn11.child18/obj107",
+ class: "WeakSet",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 4,
+ items: [
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj108",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "g",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj109",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "E",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj110",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "l",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj111",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "r",
+ },
+ attributesLength: 2,
+ },
+ },
+ ],
+ },
+});
+
+stubs.set("new WeakSet(document.querySelectorAll('div, button'))", {
+ type: "object",
+ actor: "server2.conn11.child18/obj172",
+ class: "WeakSet",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 12,
+ items: [
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj173",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "L",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj174",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "E",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj175",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "t",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj176",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "G",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj177",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "g",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj178",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "e",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj179",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "T",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj180",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "l",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj181",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "C",
+ },
+ attributesLength: 2,
+ },
+ },
+ {
+ type: "object",
+ actor: "server2.conn11.child18/obj182",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ type: "button",
+ "data-key": "c",
+ },
+ attributesLength: 2,
+ },
+ },
+ ],
+ },
+});
+
+stubs.set('["http://example.com/abcdefghijabcdefghij some other text"]', {
+ type: "object",
+ actor: "server2.conn3.child17/obj37",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ preview: {
+ kind: "ArrayLike",
+ length: 1,
+ items: ["http://example.com/abcdefghijabcdefghij some other text"],
+ },
+});
+
+stubs.set("Array(234)", {
+ type: "object",
+ actor: "server4.conn2.child19/obj668",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 235,
+ preview: {
+ kind: "ArrayLike",
+ length: 234,
+ items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ },
+});
+
+stubs.set("Array(23456)", {
+ type: "object",
+ actor: "server4.conn2.child19/obj668",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 23457,
+ preview: {
+ kind: "ArrayLike",
+ length: 23456,
+ items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ },
+});
+
+stubs.set("TestArrayWithGetter", {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj21",
+ class: "Array",
+ ownPropertyLength: 2,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isError: false,
+ preview: {
+ kind: "ArrayLike",
+ length: 1,
+ items: [{
+ type: "accessor",
+ get: {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj22",
+ }
+ }]
+ }
+});
+
+stubs.set("TestArrayWithSetter", {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj24",
+ class: "Array",
+ ownPropertyLength: 2,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isError: false,
+ preview: {
+ kind: "ArrayLike",
+ length: 1,
+ items: [{
+ type: "accessor",
+ set: {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj25",
+ }
+ }]
+ }
+});
+
+stubs.set("TestArrayWithGetterAndSetter", {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj28",
+ class: "Array",
+ ownPropertyLength: 2,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ isError: false,
+ preview: {
+ kind: "ArrayLike",
+ length: 1,
+ items: [{
+ type: "accessor",
+ get: {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj29",
+ },
+ set: {
+ type: "object",
+ actor: "server0.conn0.windowGlobal13/obj30",
+ }
+ }]
+ }
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/grip-entry.js b/devtools/client/shared/components/test/node/stubs/reps/grip-entry.js
new file mode 100644
index 0000000000..6c21389845
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/grip-entry.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("A → 0", {
+ type: "mapEntry",
+ preview: {
+ key: "A",
+ value: 0,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/grip-map.js b/devtools/client/shared/components/test/node/stubs/reps/grip-map.js
new file mode 100644
index 0000000000..8c2af0956f
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/grip-map.js
@@ -0,0 +1,908 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ maxLengthMap,
+} = require("resource://devtools/client/shared/components/reps/reps/grip-map.js");
+
+const stubs = new Map();
+
+stubs.set("testEmptyMap", {
+ type: "object",
+ actor: "server1.conn1.child1/obj97",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 0,
+ entries: [],
+ },
+});
+
+stubs.set("testSymbolKeyedMap", {
+ type: "object",
+ actor: "server1.conn1.child1/obj118",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 2,
+ entries: [
+ [
+ {
+ type: "symbol",
+ name: "a",
+ },
+ "value-a",
+ ],
+ [
+ {
+ type: "symbol",
+ name: "b",
+ },
+ "value-b",
+ ],
+ ],
+ },
+});
+
+stubs.set("testWeakMap", {
+ type: "object",
+ actor: "server1.conn1.child1/obj115",
+ class: "WeakMap",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 1,
+ entries: [
+ [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj116",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ },
+ "value-a",
+ ],
+ ],
+ },
+});
+
+stubs.set("testMaxEntries", {
+ type: "object",
+ actor: "server1.conn1.child1/obj109",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 3,
+ entries: [
+ ["key-a", "value-a"],
+ ["key-b", "value-b"],
+ ["key-c", "value-c"],
+ ],
+ },
+});
+
+stubs.set("testMoreThanMaxEntries", {
+ type: "object",
+ class: "Map",
+ actor: "server1.conn0.obj332",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: maxLengthMap.get(MODE.LONG) + 1,
+ entries: Array.from({ length: 10 }).map((_, i) => {
+ return [`key-${i}`, `value-${i}`];
+ }),
+ },
+});
+
+stubs.set("testUninterestingEntries", {
+ type: "object",
+ actor: "server1.conn1.child1/obj111",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 4,
+ entries: [
+ [
+ "key-a",
+ {
+ type: "null",
+ },
+ ],
+ [
+ "key-b",
+ {
+ type: "undefined",
+ },
+ ],
+ ["key-c", "value-c"],
+ ["key-d", 4],
+ ],
+ },
+});
+
+stubs.set("testDisconnectedNodeValuedMap", {
+ type: "object",
+ actor: "server1.conn1.child1/obj213",
+ class: "Map",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 3,
+ entries: [
+ [
+ "item-0",
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj214",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ [
+ "item-1",
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj215",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ [
+ "item-2",
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj216",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-3",
+ class: "btn btn-count",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ ],
+ },
+});
+
+stubs.set("testNodeValuedMap", {
+ type: "object",
+ actor: "server1.conn1.child1/obj213",
+ class: "Map",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 3,
+ entries: [
+ [
+ "item-0",
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj214",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ [
+ "item-1",
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj215",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ [
+ "item-2",
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj216",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-3",
+ class: "btn btn-count",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ ],
+ ],
+ },
+});
+
+stubs.set("testNodeKeyedMap", {
+ type: "object",
+ actor: "server1.conn1.child1/obj223",
+ class: "WeakMap",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 3,
+ entries: [
+ [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj224",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ "item-0",
+ ],
+ [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj225",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-3",
+ class: "btn btn-count",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ "item-2",
+ ],
+ [
+ {
+ type: "object",
+ actor: "server1.conn1.child1/obj226",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ "item-1",
+ ],
+ ],
+ },
+});
+
+stubs.set("20-entries Map", {
+ type: "object",
+ actor: "server4.conn2.child19/obj777",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 20,
+ entries: [
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj778",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "1",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj779",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "2",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj780",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "3",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj781",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "4",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj782",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "5",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj783",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "6",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj784",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "7",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj785",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "8",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj786",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "9",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj787",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "10",
+ },
+ ],
+ ],
+ },
+});
+
+stubs.set("234-entries Map", {
+ type: "object",
+ actor: "server4.conn2.child19/obj789",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 234,
+ entries: [
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj790",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "1",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj791",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "2",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj792",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "3",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj793",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "4",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj794",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "5",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj795",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "6",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj796",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "7",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj797",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "8",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj798",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "9",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj799",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "10",
+ },
+ ],
+ ],
+ },
+});
+
+stubs.set("23456-entries Map", {
+ type: "object",
+ actor: "server4.conn2.child19/obj803",
+ class: "Map",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "MapLike",
+ size: 23456,
+ entries: [
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj804",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "1",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj805",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "2",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj806",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "3",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj807",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "4",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj808",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "5",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj809",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "6",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj810",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "7",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj811",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "8",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj812",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "9",
+ },
+ ],
+ [
+ {
+ type: "object",
+ actor: "server4.conn2.child19/obj813",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ {
+ type: "symbol",
+ name: "10",
+ },
+ ],
+ ],
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/grip.js b/devtools/client/shared/components/test/node/stubs/reps/grip.js
new file mode 100644
index 0000000000..69a24013ef
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/grip.js
@@ -0,0 +1,1057 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ maxLengthMap,
+} = require("resource://devtools/client/shared/components/reps/reps/grip.js");
+
+const stubs = new Map();
+
+stubs.set("testBasic", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj304",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("testMaxProps", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj337",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 3,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ a: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "a",
+ },
+ b: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "b",
+ },
+ c: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "c",
+ },
+ },
+ ownPropertiesLength: 3,
+ safeGetterValues: {},
+ },
+});
+
+const longModeMaxLength = maxLengthMap.get(MODE.LONG);
+
+stubs.set("testMoreThanMaxProps", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj332",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: longModeMaxLength + 1,
+ preview: {
+ kind: "Object",
+ ownProperties: Array.from({ length: longModeMaxLength }).reduce(
+ (res, item, index) => ({
+ ...res,
+ [`p${index}`]: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: index.toString(),
+ },
+ }),
+ {}
+ ),
+ ownPropertiesLength: longModeMaxLength + 1,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("testUninterestingProps", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj342",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ a: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "undefined",
+ },
+ },
+ b: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "undefined",
+ },
+ },
+ c: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "c",
+ },
+ d: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 1,
+ },
+ },
+ ownPropertiesLength: 4,
+ safeGetterValues: {},
+ },
+});
+stubs.set("testNonEnumerableProps", {
+ type: "object",
+ actor: "server1.conn1.child1/obj30",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 1,
+ safeGetterValues: {},
+ },
+});
+stubs.set("testNestedObject", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj145",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ objProp: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj146",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ },
+ },
+ strProp: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "test string",
+ },
+ },
+ ownPropertiesLength: 2,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("testNestedArray", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj326",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ arrProp: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ class: "Array",
+ actor: "server1.conn0.obj327",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 1,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("testMoreProp", {
+ type: "object",
+ class: "Object",
+ actor: "server1.conn0.obj342",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ a: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "undefined",
+ },
+ },
+ b: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 1,
+ },
+ more: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 2,
+ },
+ d: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 3,
+ },
+ },
+ ownPropertiesLength: 4,
+ safeGetterValues: {},
+ },
+});
+stubs.set("testBooleanObject", {
+ type: "object",
+ actor: "server1.conn1.child1/obj57",
+ class: "Boolean",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {},
+ wrappedValue: true,
+ },
+});
+stubs.set("testNumberObject", {
+ type: "object",
+ actor: "server1.conn1.child1/obj59",
+ class: "Number",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {},
+ wrappedValue: 42,
+ },
+});
+stubs.set("testStringObject", {
+ type: "object",
+ actor: "server1.conn1.child1/obj61",
+ class: "String",
+ ownPropertyLength: 4,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 4,
+ safeGetterValues: {},
+ wrappedValue: "foo",
+ },
+});
+stubs.set("testProxy", {
+ type: "object",
+ actor: "server1.conn1.child1/obj47",
+ class: "Proxy",
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<target>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj48",
+ class: "Object",
+ ownPropertyLength: 1,
+ },
+ },
+ "<handler>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj49",
+ class: "Array",
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+stubs.set("testProxySlots", {
+ proxyTarget: {
+ type: "object",
+ actor: "server1.conn1.child1/obj48",
+ class: "Object",
+ ownPropertyLength: 1,
+ },
+ proxyHandler: {
+ type: "object",
+ actor: "server1.conn1.child1/obj49",
+ class: "Array",
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ },
+ },
+});
+stubs.set("testArrayBuffer", {
+ type: "object",
+ actor: "server1.conn1.child1/obj170",
+ class: "ArrayBuffer",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ byteLength: {
+ getterValue: 10,
+ getterPrototypeLevel: 1,
+ enumerable: false,
+ writable: true,
+ },
+ },
+ },
+});
+stubs.set("testSharedArrayBuffer", {
+ type: "object",
+ actor: "server1.conn1.child1/obj171",
+ class: "SharedArrayBuffer",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ byteLength: {
+ getterValue: 5,
+ getterPrototypeLevel: 1,
+ enumerable: false,
+ writable: true,
+ },
+ },
+ },
+});
+stubs.set("testApplicationCache", {
+ type: "object",
+ actor: "server2.conn1.child2/obj45",
+ class: "OfflineResourceList",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {
+ status: {
+ getterValue: 0,
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onchecking: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onerror: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onnoupdate: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ ondownloading: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onprogress: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onupdateready: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ oncached: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ onobsolete: {
+ getterValue: {
+ type: "null",
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ mozItems: {
+ getterValue: {
+ type: "object",
+ actor: "server2.conn1.child2/obj46",
+ class: "DOMStringList",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ArrayLike",
+ length: 0,
+ },
+ },
+ getterPrototypeLevel: 1,
+ enumerable: true,
+ writable: true,
+ },
+ },
+ },
+});
+stubs.set("testObjectWithNodes", {
+ type: "object",
+ actor: "server1.conn1.child1/obj214",
+ class: "Object",
+ ownPropertyLength: 2,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ foo: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj215",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ },
+ bar: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj216",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ safeGetterValues: {},
+ },
+});
+stubs.set("testObjectWithDisconnectedNodes", {
+ type: "object",
+ actor: "server1.conn1.child1/obj214",
+ class: "Object",
+ ownPropertyLength: 2,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ foo: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj215",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ },
+ bar: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj216",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ attributes: {
+ id: "btn-2",
+ class: "btn btn-err",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ safeGetterValues: {},
+ },
+});
+
+// Packet for `({get x(){}})`
+stubs.set("TestObjectWithGetter", {
+ type: "object",
+ actor: "server2.conn1.child1/obj105",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ x: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server2.conn1.child1/obj106",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get x",
+ displayName: "get x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ set: {
+ type: "undefined",
+ },
+ },
+ },
+ ownPropertiesLength: 1,
+ safeGetterValues: {},
+ },
+});
+
+// Packet for `({set x(s){}})`
+stubs.set("TestObjectWithSetter", {
+ type: "object",
+ actor: "server2.conn1.child1/obj115",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ x: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "undefined",
+ },
+ set: {
+ type: "object",
+ actor: "server2.conn1.child1/obj116",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "set x",
+ displayName: "set x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 1,
+ safeGetterValues: {},
+ },
+});
+
+// Packet for `({get x(){}, set x(s){}})`
+stubs.set("TestObjectWithGetterAndSetter", {
+ type: "object",
+ actor: "server2.conn1.child1/obj126",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ x: {
+ configurable: true,
+ enumerable: true,
+ get: {
+ type: "object",
+ actor: "server2.conn1.child1/obj127",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "get x",
+ displayName: "get x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ set: {
+ type: "object",
+ actor: "server2.conn1.child1/obj128",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ name: "set x",
+ displayName: "set x",
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 1,
+ safeGetterValues: {},
+ },
+});
+
+// Packet for :
+// ({
+// [Symbol()]: "first unnamed symbol",
+// [Symbol()]: "second unnamed symbol",
+// [Symbol("named")] : "named symbol",
+// [Symbol.iterator] : function* () {yield 1;yield 2;},
+// x: 10,
+// })
+stubs.set("TestObjectWithSymbolProperties", {
+ type: "object",
+ actor: "server2.conn1.child1/obj30",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ x: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 10,
+ },
+ },
+ ownSymbols: [
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "first unnamed symbol",
+ },
+ type: "symbol",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "second unnamed symbol",
+ },
+ type: "symbol",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "named symbol",
+ },
+ type: "symbol",
+ name: "named",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server2.conn1.child1/obj31",
+ class: "Function",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ location: {
+ url: "debugger eval code",
+ line: 1,
+ },
+ },
+ },
+ type: "symbol",
+ name: "Symbol.iterator",
+ },
+ ],
+ ownPropertiesLength: 1,
+ ownSymbolsLength: 4,
+ safeGetterValues: {},
+ },
+});
+
+// Packet for :
+// x = {};
+// for(let i = 0; i < 11; i++) {
+// x[Symbol(`i-${i}`)] = `value-${i}`
+// }
+// x;
+stubs.set("TestObjectWithMoreThanMaxSymbolProperties", {
+ type: "object",
+ actor: "server2.conn1.child1/obj39",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownSymbols: [
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-0",
+ },
+ type: "symbol",
+ name: "i-0",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-1",
+ },
+ type: "symbol",
+ name: "i-1",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-2",
+ },
+ type: "symbol",
+ name: "i-2",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-3",
+ },
+ type: "symbol",
+ name: "i-3",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-4",
+ },
+ type: "symbol",
+ name: "i-4",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-5",
+ },
+ type: "symbol",
+ name: "i-5",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-6",
+ },
+ type: "symbol",
+ name: "i-6",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-7",
+ },
+ type: "symbol",
+ name: "i-7",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-8",
+ },
+ type: "symbol",
+ name: "i-8",
+ },
+ {
+ descriptor: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "value-9",
+ },
+ type: "symbol",
+ name: "i-9",
+ },
+ ],
+ ownPropertiesLength: 0,
+ ownSymbolsLength: 11,
+ },
+});
+
+stubs.set('{test: "http://example.com/ some other text"}', {
+ type: "object",
+ actor: "server2.conn4.child17/obj30",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ test: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: "http://example.com/ some other text",
+ },
+ },
+ ownSymbols: [],
+ ownPropertiesLength: 1,
+ ownSymbolsLength: 0,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("Generator", {
+ type: "object",
+ actor: "server1.conn2.child1/obj33",
+ class: "Generator",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownSymbols: [],
+ ownPropertiesLength: 0,
+ ownSymbolsLength: 0,
+ safeGetterValues: {},
+ },
+});
+
+stubs.set("DeadObject", {
+ type: "object",
+ actor: "server1.conn7.child2/obj41",
+ class: "DeadObject",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+});
+
+// Packet for :
+// var obj = Object.create(null); obj.__proto__ = []; obj;
+stubs.set("ObjectWith__proto__Property", {
+ type: "object",
+ actor: "server1.conn1.child1/obj31",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ ["__proto__"]: {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj32",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ preview: {
+ kind: "ArrayLike",
+ length: 0,
+ },
+ },
+ },
+ },
+ ownSymbols: [],
+ ownPropertiesLength: 1,
+ ownSymbolsLength: 0,
+ safeGetterValues: {},
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/infinity.js b/devtools/client/shared/components/test/node/stubs/reps/infinity.js
new file mode 100644
index 0000000000..9948f218ef
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/infinity.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Infinity`, {
+ "type": "Infinity"
+});
+
+stubs.set(`NegativeInfinity`, {
+ "type": "-Infinity"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/long-string.js b/devtools/client/shared/components/test/node/stubs/reps/long-string.js
new file mode 100644
index 0000000000..26715c4017
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/long-string.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const multilineFullText = `a\n${Array(20000)
+ .fill("a")
+ .join("")}`;
+const fullTextLength = multilineFullText.length;
+const initialText = multilineFullText.substring(0, 10000);
+
+const stubs = new Map();
+
+stubs.set("testMultiline", {
+ type: "longString",
+ initial: initialText,
+ length: fullTextLength,
+ actor: "server1.conn1.child1/longString58",
+});
+
+stubs.set("testUnloadedFullText", {
+ type: "longString",
+ initial: Array(10000)
+ .fill("a")
+ .join(""),
+ length: 20000,
+ actor: "server1.conn1.child1/longString58",
+});
+
+stubs.set("testLoadedFullText", {
+ type: "longString",
+ fullText: multilineFullText,
+ initial: initialText,
+ length: fullTextLength,
+ actor: "server1.conn1.child1/longString58",
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/nan.js b/devtools/client/shared/components/test/node/stubs/reps/nan.js
new file mode 100644
index 0000000000..51b67dd128
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/nan.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`NaN`, {
+ "type": "NaN"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/null.js b/devtools/client/shared/components/test/node/stubs/reps/null.js
new file mode 100644
index 0000000000..5f7ccf5f0c
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/null.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Null`, {
+ "type": "null"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/number.js b/devtools/client/shared/components/test/node/stubs/reps/number.js
new file mode 100644
index 0000000000..217a3fa0da
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/number.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Int`, 5);
+
+stubs.set(`True`, true);
+
+stubs.set(`False`, false);
+
+stubs.set(`NegZeroGrip`, {
+ "type": "-0"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/object-with-text.js b/devtools/client/shared/components/test/node/stubs/reps/object-with-text.js
new file mode 100644
index 0000000000..ea17d01d7e
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/object-with-text.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("ShadowRule", {
+ type: "object",
+ class: "CSSStyleRule",
+ actor: "server1.conn3.obj273",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ObjectWithText",
+ text: ".Shadow",
+ },
+});
+
+stubs.set("CSSMediaRule", {
+ type: "object",
+ actor: "server2.conn8.child17/obj30",
+ class: "CSSMediaRule",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "ObjectWithText",
+ text: "(min-height: 680px), screen and (orientation: portrait)",
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/object-with-url.js b/devtools/client/shared/components/test/node/stubs/reps/object-with-url.js
new file mode 100644
index 0000000000..179faf0874
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/object-with-url.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("ObjectWithUrl", {
+ type: "object",
+ class: "Location",
+ actor: "server1.conn2.obj272",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 15,
+ preview: {
+ kind: "ObjectWithURL",
+ url: "https://www.mozilla.org/en-US/",
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/promise.js b/devtools/client/shared/components/test/node/stubs/reps/promise.js
new file mode 100644
index 0000000000..6dc71cd230
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/promise.js
@@ -0,0 +1,244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("Pending", {
+ type: "object",
+ actor: "server1.conn1.child1/obj54",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "pending",
+ },
+ },
+ ownPropertiesLength: 1,
+ },
+});
+
+stubs.set("FulfilledWithNumber", {
+ type: "object",
+ actor: "server1.conn1.child1/obj55",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: 42,
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("FulfilledWithString", {
+ type: "object",
+ actor: "server1.conn1.child1/obj56",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: "foo",
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("FulfilledWithObject", {
+ type: "object",
+ actor: "server1.conn1.child1/obj59",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj60",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 2,
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("FulfilledWithArray", {
+ type: "object",
+ actor: "server1.conn1.child1/obj57",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj58",
+ class: "Array",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 4,
+ preview: {
+ kind: "ArrayLike",
+ length: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("FulfilledWithNode", {
+ type: "object",
+ actor: "server1.conn1.child1/obj217",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj218",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: true,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("FulfilledWithDisconnectedNode", {
+ type: "object",
+ actor: "server1.conn1.child1/obj217",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "fulfilled",
+ },
+ "<value>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj218",
+ class: "HTMLButtonElement",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 0,
+ preview: {
+ kind: "DOMNode",
+ nodeType: 1,
+ nodeName: "button",
+ isConnected: false,
+ attributes: {
+ id: "btn-1",
+ class: "btn btn-log",
+ type: "button",
+ },
+ attributesLength: 3,
+ },
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("RejectedWithNumber", {
+ type: "object",
+ actor: "server0.conn0.child3/obj27",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "rejected",
+ },
+ "<reason>": {
+ value: 123,
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+stubs.set("RejectedWithObject", {
+ type: "object",
+ actor: "server0.conn0.child3/obj67",
+ class: "Promise",
+ ownPropertyLength: 0,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ "<state>": {
+ value: "rejected",
+ },
+ "<reason>": {
+ value: {
+ type: "object",
+ actor: "server1.conn1.child1/obj68",
+ class: "Object",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ },
+ },
+ },
+ ownPropertiesLength: 2,
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/regexp.js b/devtools/client/shared/components/test/node/stubs/reps/regexp.js
new file mode 100644
index 0000000000..0dd5b06e97
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/regexp.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const stubs = new Map();
+stubs.set("RegExp", {
+ type: "object",
+ class: "RegExp",
+ actor: "server1.conn22.obj39",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ ownPropertyLength: 1,
+ displayString: "/ab+c/i",
+});
+
+stubs.set("longString displayString RegExp", {
+ type: "object",
+ actor: "server0.conn0.child2/obj79",
+ class: "RegExp",
+ ownPropertyLength: 1,
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ displayString: {
+ type: "longString",
+ actor: "server0.conn0.child2/longstractor78",
+ length: 30002,
+ initial:
+ "/ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ",
+ },
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/stubs.ini b/devtools/client/shared/components/test/node/stubs/reps/stubs.ini
new file mode 100644
index 0000000000..636ca34885
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/stubs.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ attribute.js
+ comment-node.js
+ date-time.js
+ infinity.js
+ nan.js
+ null.js
+ number.js
+ stylesheet.js
+ symbol.js
+ text-node.js
+ undefined.js
+ window.js
+
+[browser_dummy.js]
+skip-if=true #This is only here so we can expose the support files in other ini files.
diff --git a/devtools/client/shared/components/test/node/stubs/reps/stylesheet.js b/devtools/client/shared/components/test/node/stubs/reps/stylesheet.js
new file mode 100644
index 0000000000..eeca646dd2
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/stylesheet.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`StyleSheet`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal4294967299/obj40",
+ "class": "CSSStyleSheet",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://example.com/styles.css"
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal4294967299/obj40"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/symbol.js b/devtools/client/shared/components/test/node/stubs/reps/symbol.js
new file mode 100644
index 0000000000..d7f0ace128
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/symbol.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Symbol`, {
+ "type": "symbol",
+ "actor": "server0.conn0.windowGlobal4294967299/symbol40",
+ "name": "foo"
+});
+
+stubs.set(`SymbolWithoutIdentifier`, {
+ "type": "symbol",
+ "actor": "server0.conn0.windowGlobal4294967299/symbol42"
+});
+
+stubs.set(`SymbolWithLongString`, {
+ "type": "symbol",
+ "actor": "server0.conn0.windowGlobal4294967299/symbol44",
+ "name": {
+ "type": "longString",
+ "actor": "server0.conn0.windowGlobal4294967299/longstractor45",
+ "length": 20000,
+ "initial": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ }
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/text-node.js b/devtools/client/shared/components/test/node/stubs/reps/text-node.js
new file mode 100644
index 0000000000..eaf893cdbb
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/text-node.js
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`testRendering`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal2147483651/obj40",
+ "class": "Text",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 3,
+ "nodeName": "#text",
+ "isConnected": true,
+ "textContent": "hello world"
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.5296372099388534
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal2147483651/obj40"
+});
+
+stubs.set(`testRenderingDisconnected`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal2147483651/obj42",
+ "class": "Text",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 3,
+ "nodeName": "#text",
+ "isConnected": false,
+ "textContent": "hello world"
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.6969799823627325
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal2147483651/obj42"
+});
+
+stubs.set(`testRenderingWithEOL`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal2147483651/obj44",
+ "class": "Text",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 3,
+ "nodeName": "#text",
+ "isConnected": false,
+ "textContent": "hello\nworld"
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.7640922670176581
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal2147483651/obj44"
+});
+
+stubs.set(`testRenderingWithDoubleQuote`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal2147483651/obj46",
+ "class": "Text",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 3,
+ "nodeName": "#text",
+ "isConnected": false,
+ "textContent": "hello\"world"
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.113013948491126
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal2147483651/obj46"
+});
+
+stubs.set(`testRenderingWithLongString`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal2147483651/obj48",
+ "class": "Text",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 3,
+ "nodeName": "#text",
+ "isConnected": false,
+ "textContent": {
+ "type": "longString",
+ "actor": "server0.conn0.windowGlobal2147483651/longstractor49",
+ "length": 20002,
+ "initial": "a\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ }
+ },
+ "contentDomReference": {
+ "browsingContextId": 51,
+ "id": 0.792316936882363
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal2147483651/obj48"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/undefined.js b/devtools/client/shared/components/test/node/stubs/reps/undefined.js
new file mode 100644
index 0000000000..7acd7be3cf
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/undefined.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Undefined`, {
+ "type": "undefined"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/window.js b/devtools/client/shared/components/test/node/stubs/reps/window.js
new file mode 100644
index 0000000000..67e76e5e32
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/window.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
+ */
+
+const stubs = new Map();
+stubs.set(`Window`, {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal2147483651/obj35",
+ "class": "Window",
+ "ownPropertyLength": 806,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "data:text/html;charset=utf-8,stub generation"
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal2147483651/obj35"
+});
+
+module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/yarn.lock b/devtools/client/shared/components/test/node/yarn.lock
new file mode 100644
index 0000000000..de7f467a56
--- /dev/null
+++ b/devtools/client/shared/components/test/node/yarn.lock
@@ -0,0 +1,4209 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
+ integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
+ dependencies:
+ "@babel/highlight" "^7.10.4"
+
+"@babel/core@^7.1.0":
+ version "7.11.6"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
+ integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/generator" "^7.11.6"
+ "@babel/helper-module-transforms" "^7.11.0"
+ "@babel/helpers" "^7.10.4"
+ "@babel/parser" "^7.11.5"
+ "@babel/template" "^7.10.4"
+ "@babel/traverse" "^7.11.5"
+ "@babel/types" "^7.11.5"
+ convert-source-map "^1.7.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.1"
+ json5 "^2.1.2"
+ lodash "^4.17.19"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.4.0":
+ version "7.11.6"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
+ integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
+ dependencies:
+ "@babel/types" "^7.11.5"
+ jsesc "^2.5.1"
+ source-map "^0.5.0"
+
+"@babel/helper-create-class-features-plugin@^7.10.4":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d"
+ integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==
+ dependencies:
+ "@babel/helper-function-name" "^7.10.4"
+ "@babel/helper-member-expression-to-functions" "^7.10.5"
+ "@babel/helper-optimise-call-expression" "^7.10.4"
+ "@babel/helper-plugin-utils" "^7.10.4"
+ "@babel/helper-replace-supers" "^7.10.4"
+ "@babel/helper-split-export-declaration" "^7.10.4"
+
+"@babel/helper-function-name@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
+ integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.10.4"
+ "@babel/template" "^7.10.4"
+ "@babel/types" "^7.10.4"
+
+"@babel/helper-get-function-arity@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
+ integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+ dependencies:
+ "@babel/types" "^7.10.4"
+
+"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5":
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df"
+ integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==
+ dependencies:
+ "@babel/types" "^7.11.0"
+
+"@babel/helper-module-imports@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
+ integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
+ dependencies:
+ "@babel/types" "^7.10.4"
+
+"@babel/helper-module-transforms@^7.11.0":
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
+ integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==
+ dependencies:
+ "@babel/helper-module-imports" "^7.10.4"
+ "@babel/helper-replace-supers" "^7.10.4"
+ "@babel/helper-simple-access" "^7.10.4"
+ "@babel/helper-split-export-declaration" "^7.11.0"
+ "@babel/template" "^7.10.4"
+ "@babel/types" "^7.11.0"
+ lodash "^4.17.19"
+
+"@babel/helper-optimise-call-expression@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
+ integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
+ dependencies:
+ "@babel/types" "^7.10.4"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
+ integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
+
+"@babel/helper-replace-supers@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf"
+ integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.10.4"
+ "@babel/helper-optimise-call-expression" "^7.10.4"
+ "@babel/traverse" "^7.10.4"
+ "@babel/types" "^7.10.4"
+
+"@babel/helper-simple-access@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
+ integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==
+ dependencies:
+ "@babel/template" "^7.10.4"
+ "@babel/types" "^7.10.4"
+
+"@babel/helper-skip-transparent-expression-wrappers@^7.11.0":
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729"
+ integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==
+ dependencies:
+ "@babel/types" "^7.11.0"
+
+"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
+ integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
+ dependencies:
+ "@babel/types" "^7.11.0"
+
+"@babel/helper-validator-identifier@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
+ integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+
+"@babel/helpers@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044"
+ integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==
+ dependencies:
+ "@babel/template" "^7.10.4"
+ "@babel/traverse" "^7.10.4"
+ "@babel/types" "^7.10.4"
+
+"@babel/highlight@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
+ integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.10.4"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.4.3":
+ version "7.11.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
+ integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
+
+"@babel/plugin-proposal-class-properties@7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807"
+ integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.10.4"
+ "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a"
+ integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.8.3":
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076"
+ integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+ integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.0.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+ integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+ integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/template@^7.10.4", "@babel/template@^7.4.0":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
+ integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/parser" "^7.10.4"
+ "@babel/types" "^7.10.4"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5", "@babel/traverse@^7.4.3":
+ version "7.11.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
+ integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/generator" "^7.11.5"
+ "@babel/helper-function-name" "^7.10.4"
+ "@babel/helper-split-export-declaration" "^7.11.0"
+ "@babel/parser" "^7.11.5"
+ "@babel/types" "^7.11.5"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.19"
+
+"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.4.0":
+ version "7.11.5"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
+ integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.10.4"
+ lodash "^4.17.19"
+ to-fast-properties "^2.0.0"
+
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
+ integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
+"@jest/console@^24.7.1", "@jest/console@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0"
+ integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==
+ dependencies:
+ "@jest/source-map" "^24.9.0"
+ chalk "^2.0.1"
+ slash "^2.0.0"
+
+"@jest/core@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4"
+ integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/reporters" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-changed-files "^24.9.0"
+ jest-config "^24.9.0"
+ jest-haste-map "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.9.0"
+ jest-resolve-dependencies "^24.9.0"
+ jest-runner "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-snapshot "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ jest-watcher "^24.9.0"
+ micromatch "^3.1.10"
+ p-each-series "^1.0.0"
+ realpath-native "^1.1.0"
+ rimraf "^2.5.4"
+ slash "^2.0.0"
+ strip-ansi "^5.0.0"
+
+"@jest/environment@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18"
+ integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==
+ dependencies:
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ jest-mock "^24.9.0"
+
+"@jest/fake-timers@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93"
+ integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-mock "^24.9.0"
+
+"@jest/reporters@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43"
+ integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==
+ dependencies:
+ "@jest/environment" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ istanbul-lib-coverage "^2.0.2"
+ istanbul-lib-instrument "^3.0.1"
+ istanbul-lib-report "^2.0.4"
+ istanbul-lib-source-maps "^3.0.1"
+ istanbul-reports "^2.2.6"
+ jest-haste-map "^24.9.0"
+ jest-resolve "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-util "^24.9.0"
+ jest-worker "^24.6.0"
+ node-notifier "^5.4.2"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+ string-length "^2.0.0"
+
+"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714"
+ integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==
+ dependencies:
+ callsites "^3.0.0"
+ graceful-fs "^4.1.15"
+ source-map "^0.6.0"
+
+"@jest/test-result@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca"
+ integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==
+ dependencies:
+ "@jest/console" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+
+"@jest/test-sequencer@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31"
+ integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==
+ dependencies:
+ "@jest/test-result" "^24.9.0"
+ jest-haste-map "^24.9.0"
+ jest-runner "^24.9.0"
+ jest-runtime "^24.9.0"
+
+"@jest/transform@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56"
+ integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^24.9.0"
+ babel-plugin-istanbul "^5.1.0"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.1.15"
+ jest-haste-map "^24.9.0"
+ jest-regex-util "^24.9.0"
+ jest-util "^24.9.0"
+ micromatch "^3.1.10"
+ pirates "^4.0.1"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "2.4.1"
+
+"@jest/types@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59"
+ integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^1.1.1"
+ "@types/yargs" "^13.0.0"
+
+"@tootallnate/once@2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
+ integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
+
+"@types/babel__core@^7.1.0":
+ version "7.1.10"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.10.tgz#ca58fc195dd9734e77e57c6f2df565623636ab40"
+ integrity sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8"
+ integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.3.tgz#b8aaeba0a45caca7b56a5de9459872dde3727214"
+ integrity sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03"
+ integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==
+ dependencies:
+ "@babel/types" "^7.3.0"
+
+"@types/cheerio@^0.22.22":
+ version "0.22.22"
+ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.22.tgz#ae71cf4ca59b8bbaf34c99af7a5d6c8894988f5f"
+ integrity sha512-05DYX4zU96IBfZFY+t3Mh88nlwSMtmmzSYaQkKN48T495VV1dkHSah6qYyDTN5ngaS0i0VonH37m+RuzSM0YiA==
+ dependencies:
+ "@types/node" "*"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
+ integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
+
+"@types/istanbul-lib-report@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+ integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^1.1.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2"
+ integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+ "@types/istanbul-lib-report" "*"
+
+"@types/node@*":
+ version "14.11.8"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.8.tgz#fe2012f2355e4ce08bca44aeb3abbb21cf88d33f"
+ integrity sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==
+
+"@types/stack-utils@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+
+"@types/yargs-parser@*":
+ version "15.0.0"
+ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
+ integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
+
+"@types/yargs@^13.0.0":
+ version "13.0.11"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.11.tgz#def2f0c93e4bdf2c61d7e34899b17e34be28d3b1"
+ integrity sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==
+ dependencies:
+ "@types/yargs-parser" "*"
+
+abab@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
+ integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
+
+abab@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
+ integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+
+acorn-globals@^4.1.0:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+ integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+ dependencies:
+ acorn "^6.0.1"
+ acorn-walk "^6.0.1"
+
+acorn-globals@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
+ integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
+ dependencies:
+ acorn "^7.1.1"
+ acorn-walk "^7.1.1"
+
+acorn-walk@^6.0.1:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
+ integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+
+acorn-walk@^7.1.1:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+ integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
+
+acorn@^5.5.3:
+ version "5.7.4"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
+ integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
+
+acorn@^6.0.1:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
+ integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
+
+acorn@^7.1.1:
+ version "7.4.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+ integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+acorn@^8.7.1:
+ version "8.8.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+ integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+
+agent-base@6:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+ integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+ dependencies:
+ debug "4"
+
+airbnb-prop-types@^2.16.0:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
+ integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
+ dependencies:
+ array.prototype.find "^2.1.1"
+ function.prototype.name "^1.1.2"
+ is-regex "^1.1.0"
+ object-is "^1.1.2"
+ object.assign "^4.1.0"
+ object.entries "^1.1.2"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.13.1"
+
+ajv@^6.12.3:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.0.0, ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+ integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+ integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+ integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+array.prototype.find@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c"
+ integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.4"
+
+array.prototype.flat@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
+ integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+ integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
+async-limiter@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+ integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428"
+ integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==
+
+babel-jest@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54"
+ integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==
+ dependencies:
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/babel__core" "^7.1.0"
+ babel-plugin-istanbul "^5.1.0"
+ babel-preset-jest "^24.9.0"
+ chalk "^2.4.2"
+ slash "^2.0.0"
+
+babel-plugin-istanbul@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
+ integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ find-up "^3.0.0"
+ istanbul-lib-instrument "^3.3.0"
+ test-exclude "^5.2.3"
+
+babel-plugin-jest-hoist@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756"
+ integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==
+ dependencies:
+ "@types/babel__traverse" "^7.0.6"
+
+babel-plugin-transform-amd-to-commonjs@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-amd-to-commonjs/-/babel-plugin-transform-amd-to-commonjs-1.4.0.tgz#d9bc5003eaa26dbdd4e854e453f84903852af2ca"
+ integrity sha512-Xx0kYPn0LPyms+8n2KLn9yd2R5XMb2P1sNe4qn64/UQY5F2KFYlhhhyYUNm/BThfODAzl7rbaOsEfpU2M8iDKQ==
+
+babel-preset-jest@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc"
+ integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==
+ dependencies:
+ "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
+ babel-plugin-jest-hoist "^24.9.0"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+bindings@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+ integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+ dependencies:
+ file-uri-to-path "1.0.0"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+browser-process-hrtime@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+ integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
+ dependencies:
+ resolve "1.1.7"
+
+bser@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
+ integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@^5.0.0, camelcase@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+capture-exit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+ integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==
+ dependencies:
+ rsvp "^4.8.4"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+cheerio@^1.0.0-rc.3:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+ integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+ integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+ dependencies:
+ string-width "^3.1.0"
+ strip-ansi "^5.2.0"
+ wrap-ansi "^5.1.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^2.19.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+component-emitter@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+convert-source-map@^1.4.0, convert-source-map@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+ integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+ dependencies:
+ safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+ integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
+core-util-is@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6:
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+ integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssom@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
+ integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
+
+cssstyle@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
+ integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==
+ dependencies:
+ cssom "0.3.x"
+
+cssstyle@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+ integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
+ dependencies:
+ cssom "~0.3.6"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+data-urls@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
+data-urls@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
+ integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
+ dependencies:
+ abab "^2.0.6"
+ whatwg-mimetype "^3.0.0"
+ whatwg-url "^11.0.0"
+
+debug@4:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+debug@^2.2.0, debug@^2.3.3:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^4.1.0, debug@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
+ integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
+ dependencies:
+ ms "2.1.2"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decimal.js@^10.3.1:
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.0.tgz#97a7448873b01e92e5ff9117d89a7bca8e63e0fe"
+ integrity sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg==
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+ integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+ integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+
+diff-sequences@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
+ integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+ integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
+
+dom-serializer@0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+ integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+ dependencies:
+ domelementtype "^2.0.1"
+ entities "^2.0.0"
+
+dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971"
+ integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==
+
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+domexception@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
+ integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
+ dependencies:
+ webidl-conversions "^7.0.0"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+emoji-regex@^7.0.1:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+ integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+encoding@^0.1.11:
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+ integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+ dependencies:
+ iconv-lite "^0.6.2"
+
+end-of-stream@^1.1.0:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ dependencies:
+ once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+entities@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
+ integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
+
+entities@^4.3.0:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4"
+ integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==
+
+enzyme-adapter-react-16@^1.13.2:
+ version "1.15.5"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz#7a6f0093d3edd2f7025b36e7fbf290695473ee04"
+ integrity sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==
+ dependencies:
+ enzyme-adapter-utils "^1.13.1"
+ enzyme-shallow-equal "^1.0.4"
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ object.values "^1.1.1"
+ prop-types "^15.7.2"
+ react-is "^16.13.1"
+ react-test-renderer "^16.0.0-0"
+ semver "^5.7.0"
+
+enzyme-adapter-utils@^1.13.1:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz#59c1b734b0927543e3d8dc477299ec957feb312d"
+ integrity sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==
+ dependencies:
+ airbnb-prop-types "^2.16.0"
+ function.prototype.name "^1.1.2"
+ object.assign "^4.1.0"
+ object.fromentries "^2.0.2"
+ prop-types "^15.7.2"
+ semver "^5.7.1"
+
+enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e"
+ integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==
+ dependencies:
+ has "^1.0.3"
+ object-is "^1.1.2"
+
+enzyme-to-json@^3.3.5:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz#d60740950bc7ca6384dfe6fe405494ec5df996bc"
+ integrity sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==
+ dependencies:
+ "@types/cheerio" "^0.22.22"
+ lodash "^4.17.15"
+ react-is "^16.12.0"
+
+enzyme@^3.9.0:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28"
+ integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==
+ dependencies:
+ array.prototype.flat "^1.2.3"
+ cheerio "^1.0.0-rc.3"
+ enzyme-shallow-equal "^1.0.1"
+ function.prototype.name "^1.1.2"
+ has "^1.0.3"
+ html-element-map "^1.2.0"
+ is-boolean-object "^1.0.1"
+ is-callable "^1.1.5"
+ is-number-object "^1.0.4"
+ is-regex "^1.0.5"
+ is-string "^1.0.5"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.7.0"
+ object-is "^1.0.2"
+ object.assign "^4.1.0"
+ object.entries "^1.1.1"
+ object.values "^1.1.1"
+ raf "^3.4.1"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.2.1"
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5:
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
+ integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.2.2"
+ is-regex "^1.1.1"
+ object-inspect "^1.8.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.1"
+ string.prototype.trimend "^1.0.1"
+ string.prototype.trimstart "^1.0.1"
+
+es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
+ version "1.18.0-next.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
+ integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.2.2"
+ is-negative-zero "^2.0.0"
+ is-regex "^1.1.1"
+ object-inspect "^1.8.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.1"
+ string.prototype.trimend "^1.0.1"
+ string.prototype.trimstart "^1.0.1"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^1.9.1:
+ version "1.14.3"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
+ integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+escodegen@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
+ integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^5.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+exec-sh@^0.3.2:
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
+ integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+ integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expect@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
+ integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ ansi-styles "^3.2.0"
+ jest-get-type "^24.9.0"
+ jest-matcher-utils "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-regex-util "^24.9.0"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@~2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fb-watchman@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
+ integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==
+ dependencies:
+ bser "2.1.1"
+
+fbjs@^0.8.16:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
+file-uri-to-path@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+ integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+ integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
+ dependencies:
+ bindings "^1.5.0"
+ nan "^2.12.1"
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+function.prototype.name@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45"
+ integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ functions-have-names "^1.2.0"
+
+functions-have-names@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91"
+ integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==
+
+gensync@^1.0.0-beta.1:
+ version "1.0.0-beta.1"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+ integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+ integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+ integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+ integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+ version "5.1.5"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
+ integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
+ dependencies:
+ ajv "^6.12.3"
+ har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-symbols@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+ integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hosted-git-info@^2.1.4:
+ version "2.8.8"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
+ integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
+
+html-element-map@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22"
+ integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==
+ dependencies:
+ array-filter "^1.0.0"
+
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+html-encoding-sniffer@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
+ integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
+ dependencies:
+ whatwg-encoding "^2.0.0"
+
+html-escaper@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+ integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
+http-proxy-agent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
+ integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
+ dependencies:
+ "@tootallnate/once" "2"
+ agent-base "6"
+ debug "4"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-proxy-agent@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
+ integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+iconv-lite@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
+ integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-boolean-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
+ integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
+ integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
+
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+ dependencies:
+ ci-info "^2.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+ integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-generator-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+ integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+
+is-negative-zero@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
+ integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
+
+is-number-object@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
+ integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-potential-custom-element-name@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
+ integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+
+is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
+ integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
+ dependencies:
+ has-symbols "^1.0.1"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-string@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+ integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+ integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
+
+is-symbol@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+ integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+ dependencies:
+ has-symbols "^1.0.1"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+isarray@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
+ integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+
+istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
+ integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
+ dependencies:
+ "@babel/generator" "^7.4.0"
+ "@babel/parser" "^7.4.3"
+ "@babel/template" "^7.4.0"
+ "@babel/traverse" "^7.4.3"
+ "@babel/types" "^7.4.0"
+ istanbul-lib-coverage "^2.0.5"
+ semver "^6.0.0"
+
+istanbul-lib-report@^2.0.4:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33"
+ integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==
+ dependencies:
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ supports-color "^6.1.0"
+
+istanbul-lib-source-maps@^3.0.1:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8"
+ integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ rimraf "^2.6.3"
+ source-map "^0.6.1"
+
+istanbul-reports@^2.2.6:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931"
+ integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==
+ dependencies:
+ html-escaper "^2.0.0"
+
+jest-changed-files@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
+ integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ execa "^1.0.0"
+ throat "^4.0.0"
+
+jest-cli@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af"
+ integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==
+ dependencies:
+ "@jest/core" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ import-local "^2.0.0"
+ is-ci "^2.0.0"
+ jest-config "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ prompts "^2.0.1"
+ realpath-native "^1.1.0"
+ yargs "^13.3.0"
+
+jest-config@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5"
+ integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/test-sequencer" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ babel-jest "^24.9.0"
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^24.9.0"
+ jest-environment-node "^24.9.0"
+ jest-get-type "^24.9.0"
+ jest-jasmine2 "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ micromatch "^3.1.10"
+ pretty-format "^24.9.0"
+ realpath-native "^1.1.0"
+
+jest-diff@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
+ integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
+ dependencies:
+ chalk "^2.0.1"
+ diff-sequences "^24.9.0"
+ jest-get-type "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-docblock@^24.3.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
+ integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-each@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05"
+ integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.9.0"
+ jest-util "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-environment-jsdom@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b"
+ integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==
+ dependencies:
+ "@jest/environment" "^24.9.0"
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ jest-mock "^24.9.0"
+ jest-util "^24.9.0"
+ jsdom "^11.5.1"
+
+jest-environment-node@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3"
+ integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==
+ dependencies:
+ "@jest/environment" "^24.9.0"
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ jest-mock "^24.9.0"
+ jest-util "^24.9.0"
+
+jest-get-type@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
+ integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
+
+jest-haste-map@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
+ integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ anymatch "^2.0.0"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.15"
+ invariant "^2.2.4"
+ jest-serializer "^24.9.0"
+ jest-util "^24.9.0"
+ jest-worker "^24.9.0"
+ micromatch "^3.1.10"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+jest-jasmine2@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0"
+ integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@jest/environment" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^24.9.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^24.9.0"
+ jest-matcher-utils "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-snapshot "^24.9.0"
+ jest-util "^24.9.0"
+ pretty-format "^24.9.0"
+ throat "^4.0.0"
+
+jest-leak-detector@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a"
+ integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==
+ dependencies:
+ jest-get-type "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-matcher-utils@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
+ integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^24.9.0"
+ jest-get-type "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-message-util@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3"
+ integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/stack-utils" "^1.0.1"
+ chalk "^2.0.1"
+ micromatch "^3.1.10"
+ slash "^2.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6"
+ integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==
+ dependencies:
+ "@jest/types" "^24.9.0"
+
+jest-pnp-resolver@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
+ integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
+
+jest-regex-util@^24.3.0, jest-regex-util@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636"
+ integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==
+
+jest-resolve-dependencies@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab"
+ integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-snapshot "^24.9.0"
+
+jest-resolve@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321"
+ integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ browser-resolve "^1.11.3"
+ chalk "^2.0.1"
+ jest-pnp-resolver "^1.2.1"
+ realpath-native "^1.1.0"
+
+jest-runner@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42"
+ integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.4.2"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-config "^24.9.0"
+ jest-docblock "^24.3.0"
+ jest-haste-map "^24.9.0"
+ jest-jasmine2 "^24.9.0"
+ jest-leak-detector "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-resolve "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-util "^24.9.0"
+ jest-worker "^24.6.0"
+ source-map-support "^0.5.6"
+ throat "^4.0.0"
+
+jest-runtime@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac"
+ integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.9.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/yargs" "^13.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ jest-config "^24.9.0"
+ jest-haste-map "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-mock "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.9.0"
+ jest-snapshot "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ strip-bom "^3.0.0"
+ yargs "^13.3.0"
+
+jest-serializer@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73"
+ integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==
+
+jest-snapshot@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba"
+ integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==
+ dependencies:
+ "@babel/types" "^7.0.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ expect "^24.9.0"
+ jest-diff "^24.9.0"
+ jest-get-type "^24.9.0"
+ jest-matcher-utils "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-resolve "^24.9.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^24.9.0"
+ semver "^6.2.0"
+
+jest-util@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162"
+ integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==
+ dependencies:
+ "@jest/console" "^24.9.0"
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/source-map" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ callsites "^3.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.15"
+ is-ci "^2.0.0"
+ mkdirp "^0.5.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+
+jest-validate@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab"
+ integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ camelcase "^5.3.1"
+ chalk "^2.0.1"
+ jest-get-type "^24.9.0"
+ leven "^3.1.0"
+ pretty-format "^24.9.0"
+
+jest-watcher@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b"
+ integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==
+ dependencies:
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/yargs" "^13.0.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ jest-util "^24.9.0"
+ string-length "^2.0.0"
+
+jest-worker@^24.6.0, jest-worker@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
+ integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==
+ dependencies:
+ merge-stream "^2.0.0"
+ supports-color "^6.1.0"
+
+jest@^24.6.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171"
+ integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==
+ dependencies:
+ import-local "^2.0.0"
+ jest-cli "^24.9.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@20.0.0:
+ version "20.0.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.0.tgz#882825ac9cc5e5bbee704ba16143e1fa78361ebf"
+ integrity sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==
+ dependencies:
+ abab "^2.0.6"
+ acorn "^8.7.1"
+ acorn-globals "^6.0.0"
+ cssom "^0.5.0"
+ cssstyle "^2.3.0"
+ data-urls "^3.0.2"
+ decimal.js "^10.3.1"
+ domexception "^4.0.0"
+ escodegen "^2.0.0"
+ form-data "^4.0.0"
+ html-encoding-sniffer "^3.0.0"
+ http-proxy-agent "^5.0.0"
+ https-proxy-agent "^5.0.1"
+ is-potential-custom-element-name "^1.0.1"
+ nwsapi "^2.2.0"
+ parse5 "^7.0.0"
+ saxes "^6.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^4.0.0"
+ w3c-hr-time "^1.0.2"
+ w3c-xmlserializer "^3.0.0"
+ webidl-conversions "^7.0.0"
+ whatwg-encoding "^2.0.0"
+ whatwg-mimetype "^3.0.0"
+ whatwg-url "^11.0.0"
+ ws "^8.8.0"
+ xml-name-validator "^4.0.0"
+
+jsdom@^11.5.1:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
+ integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+ dependencies:
+ abab "^2.0.0"
+ acorn "^5.5.3"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle "^1.0.0"
+ data-urls "^1.0.0"
+ domexception "^1.0.1"
+ escodegen "^1.9.1"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.3.0"
+ nwsapi "^2.0.7"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.87.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.4"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-mimetype "^2.1.0"
+ whatwg-url "^6.4.1"
+ ws "^5.2.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@^2.1.2:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+ integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+ dependencies:
+ minimist "^1.2.5"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+kleur@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+ integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
+left-pad@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
+ integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
+
+leven@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+ integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+ integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+ integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+ integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+ integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+ integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash@^4.15.0, lodash@^4.17.15, lodash@^4.17.19:
+ version "4.17.20"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+ integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+make-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+ integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+ dependencies:
+ tmpl "1.0.x"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+mime-db@1.44.0:
+ version "1.44.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+ integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.27"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+ integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+ dependencies:
+ mime-db "1.44.0"
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+ integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+mixin-deep@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.1:
+ version "0.5.5"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
+ integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
+ dependencies:
+ minimist "^1.2.5"
+
+moo@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
+ integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nan@^2.12.1:
+ version "2.14.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
+ integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+ integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+nearley@^2.7.10:
+ version "2.19.7"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.7.tgz#eafbe3e2d8ccfe70adaa5c026ab1f9709c116218"
+ integrity sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.5.0"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+ integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+node-modules-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
+ integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
+
+node-notifier@^5.4.2:
+ version "5.4.3"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50"
+ integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==
+ dependencies:
+ growly "^1.3.0"
+ is-wsl "^1.1.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+normalize-package-data@^2.3.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+ integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+ dependencies:
+ hosted-git-info "^2.1.4"
+ resolve "^1.10.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
+nwsapi@^2.0.7:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+ integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
+nwsapi@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.1.tgz#10a9f268fbf4c461249ebcfe38e359aa36e2577c"
+ integrity sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-inspect@^1.7.0, object-inspect@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
+ integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+
+object-is@^1.0.2, object-is@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
+ integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.18.0-next.1"
+
+object-keys@^1.0.12, object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.1.0, object.assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
+ integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.18.0-next.0"
+ has-symbols "^1.0.1"
+ object-keys "^1.1.1"
+
+object.entries@^1.1.1, object.entries@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
+ integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+ has "^1.0.3"
+
+object.fromentries@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9"
+ integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.getownpropertydescriptors@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+ integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+ integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+optionator@^0.8.1:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+ integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.6"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ word-wrap "~1.2.3"
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=
+ dependencies:
+ p-reduce "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-limit@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+ integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+ integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+ dependencies:
+ "@types/node" "*"
+
+parse5@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
+ integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
+ dependencies:
+ entities "^4.3.0"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+ dependencies:
+ pify "^3.0.0"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pirates@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+ integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
+ dependencies:
+ node-modules-regexp "^1.0.0"
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+ integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+ integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+pretty-format@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
+ integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ ansi-regex "^4.0.0"
+ ansi-styles "^3.2.0"
+ react-is "^16.8.4"
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
+prompts@^2.0.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068"
+ integrity sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==
+ dependencies:
+ kleur "^3.0.3"
+ sisteransi "^1.0.4"
+
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
+prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
+psl@^1.1.28:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+ integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+psl@^1.1.33:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
+ integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystringify@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
+ integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
+raf@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+ integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+react-dom-factories@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.2.tgz#eb7705c4db36fb501b3aa38ff759616aa0ff96e0"
+ integrity sha1-63cFxNs2+1AbOqOP91lhaqD/luA=
+
+react-dom@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+ integrity sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A==
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+react-is@^16.12.0, react-is@^16.13.1, react-is@^16.4.1, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-test-renderer@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
+ integrity sha512-wyyiPxRZOTpKnNIgUBOB6xPLTpIzwcQMIURhZvzUqZzezvHjaGNsDPBhMac5fIY3Jf5NuKxoGvV64zDSOECPPQ==
+ dependencies:
+ fbjs "^0.8.16"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+ react-is "^16.4.1"
+
+react-test-renderer@^16.0.0-0:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
+ integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.19.1"
+
+react@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+ integrity sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg==
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+read-pkg-up@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
+ integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
+ dependencies:
+ find-up "^3.0.0"
+ read-pkg "^3.0.0"
+
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+ integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+ dependencies:
+ load-json-file "^4.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^3.0.0"
+
+readable-stream@^3.1.1:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+realpath-native@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
+ integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==
+ dependencies:
+ util.promisify "^1.0.0"
+
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+ integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+ integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+ integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
+ dependencies:
+ lodash "^4.17.19"
+
+request-promise-native@^1.0.5:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+ integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
+ dependencies:
+ request-promise-core "1.1.4"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
+request@^2.87.0:
+ version "2.88.2"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+ integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.5.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+ integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+ integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
+
+resolve@^1.10.0, resolve@^1.3.2:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
+ integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+ dependencies:
+ path-parse "^1.0.6"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rimraf@^2.5.4, rimraf@^2.6.3:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+ integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ dependencies:
+ glob "^7.1.3"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+rsvp@^4.8.4:
+ version "4.8.5"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+ integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sane@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+ integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==
+ dependencies:
+ "@cnakazawa/watch" "^1.0.3"
+ anymatch "^2.0.0"
+ capture-exit "^2.0.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+saxes@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
+ integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
+ dependencies:
+ xmlchars "^2.2.0"
+
+scheduler@^0.19.1:
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
+ integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+ integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.0.0, semver@^6.2.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^2.0.0, set-value@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+ integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+ integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+ integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
+sisteransi@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
+ integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+ integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+ dependencies:
+ atob "^2.1.2"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.5.6:
+ version "0.5.19"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+ integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+ integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@^0.5.0, source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+ integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+ integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+ integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
+ integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+stack-utils@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
+ integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+stealthy-require@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+ integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+ integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+ dependencies:
+ emoji-regex "^7.0.1"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^5.1.0"
+
+string.prototype.trim@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz#f538d0bacd98fc4297f0bef645226d5aaebf59f3"
+ integrity sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.18.0-next.0"
+
+string.prototype.trimend@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+ integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+string.prototype.trimstart@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+ integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+ dependencies:
+ ansi-regex "^4.1.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.0.0"
+
+symbol-tree@^3.2.2, symbol-tree@^3.2.4:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+ integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+test-exclude@^5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
+ integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
+ dependencies:
+ glob "^7.1.3"
+ minimatch "^3.0.4"
+ read-pkg-up "^4.0.0"
+ require-main-filename "^2.0.0"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+ integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+ integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
+ integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
+ dependencies:
+ psl "^1.1.33"
+ punycode "^2.1.1"
+ universalify "^0.2.0"
+ url-parse "^1.5.3"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+ dependencies:
+ punycode "^2.1.0"
+
+tr46@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
+ integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
+ dependencies:
+ punycode "^2.1.1"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+ dependencies:
+ prelude-ls "~1.1.2"
+
+ua-parser-js@^0.7.18:
+ version "0.7.22"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"
+ integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==
+
+union-value@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+ integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^2.0.1"
+
+universalify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
+ integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+uri-js@^4.2.2:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602"
+ integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url-parse@^1.5.3:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
+ integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+ integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.2"
+ has-symbols "^1.0.1"
+ object.getownpropertydescriptors "^2.1.0"
+
+uuid@^3.3.2:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+ integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+w3c-hr-time@^1.0.1, w3c-hr-time@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+ integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+ dependencies:
+ browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923"
+ integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==
+ dependencies:
+ xml-name-validator "^4.0.0"
+
+walker@^1.0.7, walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+ dependencies:
+ makeerror "1.0.x"
+
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+ integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+webidl-conversions@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
+ integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-encoding@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
+ integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
+ dependencies:
+ iconv-lite "0.6.3"
+
+whatwg-fetch@>=0.10.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3"
+ integrity sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==
+
+whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+ integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-mimetype@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
+ integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
+
+whatwg-url@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
+ integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+ dependencies:
+ tr46 "^3.0.0"
+ webidl-conversions "^7.0.0"
+
+whatwg-url@^6.4.1:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
+ integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+whatwg-url@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+ integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+word-wrap@~1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+ integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wrap-ansi@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+ integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+ dependencies:
+ ansi-styles "^3.2.0"
+ string-width "^3.0.0"
+ strip-ansi "^5.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write-file-atomic@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529"
+ integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+ws@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+ integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+ dependencies:
+ async-limiter "~1.0.0"
+
+ws@^8.8.0:
+ version "8.8.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
+ integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
+
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+ integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xml-name-validator@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
+ integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
+
+xmlchars@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+ integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
+y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yargs-parser@^13.1.2:
+ version "13.1.2"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+ integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^13.3.0:
+ version "13.3.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+ integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+ dependencies:
+ cliui "^5.0.0"
+ find-up "^3.0.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^3.0.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^13.1.2"
diff --git a/devtools/client/shared/components/throttling/NetworkThrottlingMenu.js b/devtools/client/shared/components/throttling/NetworkThrottlingMenu.js
new file mode 100644
index 0000000000..b4be5ad037
--- /dev/null
+++ b/devtools/client/shared/components/throttling/NetworkThrottlingMenu.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const throttlingProfiles = require("resource://devtools/client/shared/components/throttling/profiles.js");
+const Types = require("resource://devtools/client/shared/components/throttling/types.js");
+
+// Localization
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/network-throttling.properties"
+);
+const NO_THROTTLING_LABEL = L10N.getStr("responsive.noThrottling");
+
+loader.lazyRequireGetter(
+ this,
+ "showMenu",
+ "resource://devtools/client/shared/components/menu/utils.js",
+ true
+);
+
+/**
+ * This component represents selector button that can be used
+ * to throttle network bandwidth.
+ */
+class NetworkThrottlingMenu extends PureComponent {
+ static get propTypes() {
+ return {
+ networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+ onChangeNetworkThrottling: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onShowThrottlingMenu = this.onShowThrottlingMenu.bind(this);
+ }
+
+ onShowThrottlingMenu(event) {
+ const { networkThrottling, onChangeNetworkThrottling } = this.props;
+
+ const menuItems = throttlingProfiles.map(profile => {
+ return {
+ label: profile.id,
+ type: "checkbox",
+ checked:
+ networkThrottling.enabled && profile.id == networkThrottling.profile,
+ click: () => onChangeNetworkThrottling(true, profile.id),
+ };
+ });
+
+ menuItems.unshift("-");
+
+ menuItems.unshift({
+ label: NO_THROTTLING_LABEL,
+ type: "checkbox",
+ checked: !networkThrottling.enabled,
+ click: () => onChangeNetworkThrottling(false, ""),
+ });
+
+ showMenu(menuItems, { button: event.target });
+ }
+
+ render() {
+ const { networkThrottling } = this.props;
+ const label = networkThrottling.enabled
+ ? networkThrottling.profile
+ : NO_THROTTLING_LABEL;
+
+ let title = NO_THROTTLING_LABEL;
+
+ if (networkThrottling.enabled) {
+ const id = networkThrottling.profile;
+ const selectedProfile = throttlingProfiles.find(
+ profile => profile.id === id
+ );
+ title = selectedProfile.description;
+ }
+
+ return dom.button(
+ {
+ id: "network-throttling-menu",
+ className: "devtools-button devtools-dropdown-button",
+ title,
+ onClick: this.onShowThrottlingMenu,
+ },
+ dom.span({ className: "title" }, label)
+ );
+ }
+}
+
+module.exports = NetworkThrottlingMenu;
diff --git a/devtools/client/shared/components/throttling/actions.js b/devtools/client/shared/components/throttling/actions.js
new file mode 100644
index 0000000000..b6a686ab22
--- /dev/null
+++ b/devtools/client/shared/components/throttling/actions.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const actionTypes = {
+ CHANGE_NETWORK_THROTTLING: "CHANGE_NETWORK_THROTTLING",
+};
+
+function changeNetworkThrottling(enabled, profile) {
+ return {
+ type: actionTypes.CHANGE_NETWORK_THROTTLING,
+ enabled,
+ profile,
+ };
+}
+
+module.exports = {
+ ...actionTypes,
+ changeNetworkThrottling,
+};
diff --git a/devtools/client/shared/components/throttling/moz.build b/devtools/client/shared/components/throttling/moz.build
new file mode 100644
index 0000000000..2c178219fc
--- /dev/null
+++ b/devtools/client/shared/components/throttling/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "actions.js",
+ "NetworkThrottlingMenu.js",
+ "profiles.js",
+ "reducer.js",
+ "types.js",
+)
diff --git a/devtools/client/shared/components/throttling/profiles.js b/devtools/client/shared/components/throttling/profiles.js
new file mode 100644
index 0000000000..85dc3b1158
--- /dev/null
+++ b/devtools/client/shared/components/throttling/profiles.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const K = 1024;
+const M = 1024 * 1024;
+const Bps = 1 / 8;
+const KBps = K * Bps;
+const MBps = M * Bps;
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/network-throttling.properties"
+);
+
+/**
+ * Predefined network throttling profiles.
+ * Speeds are in bytes per second. Latency is in ms.
+ */
+
+class ThrottlingProfile {
+ constructor({ id, download, upload, latency }) {
+ this.id = id;
+ this.download = download;
+ this.upload = upload;
+ this.latency = latency;
+ }
+
+ get description() {
+ const download = this.#toDescriptionData(this.download);
+ const upload = this.#toDescriptionData(this.upload);
+ return L10N.getFormatStr(
+ "throttling.profile.description",
+ download.value,
+ download.unit,
+ upload.value,
+ upload.unit,
+ this.latency
+ );
+ }
+
+ #toDescriptionData(val) {
+ if (val % MBps === 0) {
+ return { value: val / MBps, unit: "Mbps" };
+ }
+ return { value: val / KBps, unit: "Kbps" };
+ }
+}
+
+// Should be synced with devtools/docs/user/network_monitor/throttling/index.rst
+const profiles = [
+ {
+ id: "GPRS",
+ download: 50 * KBps,
+ upload: 20 * KBps,
+ latency: 500,
+ },
+ {
+ id: "Regular 2G",
+ download: 250 * KBps,
+ upload: 50 * KBps,
+ latency: 300,
+ },
+ {
+ id: "Good 2G",
+ download: 450 * KBps,
+ upload: 150 * KBps,
+ latency: 150,
+ },
+ {
+ id: "Regular 3G",
+ download: 750 * KBps,
+ upload: 250 * KBps,
+ latency: 100,
+ },
+ {
+ id: "Good 3G",
+ download: 1.5 * MBps,
+ upload: 750 * KBps,
+ latency: 40,
+ },
+ {
+ id: "Regular 4G / LTE",
+ download: 4 * MBps,
+ upload: 3 * MBps,
+ latency: 20,
+ },
+ {
+ id: "DSL",
+ download: 2 * MBps,
+ upload: 1 * MBps,
+ latency: 5,
+ },
+ {
+ id: "Wi-Fi",
+ download: 30 * MBps,
+ upload: 15 * MBps,
+ latency: 2,
+ },
+].map(profile => new ThrottlingProfile(profile));
+
+module.exports = profiles;
diff --git a/devtools/client/shared/components/throttling/reducer.js b/devtools/client/shared/components/throttling/reducer.js
new file mode 100644
index 0000000000..ea6408fe9f
--- /dev/null
+++ b/devtools/client/shared/components/throttling/reducer.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ CHANGE_NETWORK_THROTTLING,
+} = require("resource://devtools/client/shared/components/throttling/actions.js");
+
+const INITIAL_STATE = {
+ enabled: false,
+ profile: "",
+};
+
+function throttlingReducer(state = INITIAL_STATE, action) {
+ switch (action.type) {
+ case CHANGE_NETWORK_THROTTLING: {
+ return {
+ enabled: action.enabled,
+ profile: action.profile,
+ };
+ }
+ default:
+ return state;
+ }
+}
+
+module.exports = throttlingReducer;
diff --git a/devtools/client/shared/components/throttling/types.js b/devtools/client/shared/components/throttling/types.js
new file mode 100644
index 0000000000..a54797057d
--- /dev/null
+++ b/devtools/client/shared/components/throttling/types.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+/**
+ * Network throttling state.
+ */
+exports.networkThrottling = {
+ // Whether or not network throttling is enabled
+ enabled: PropTypes.bool,
+ // Name of the selected throttling profile
+ profile: PropTypes.string,
+};
diff --git a/devtools/client/shared/components/tree/LabelCell.js b/devtools/client/shared/components/tree/LabelCell.js
new file mode 100644
index 0000000000..e42a9dfd1c
--- /dev/null
+++ b/devtools/client/shared/components/tree/LabelCell.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const { Component } = require("devtools/client/shared/vendor/react");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ /**
+ * Render the default cell used for toggle buttons
+ */
+ class LabelCell extends Component {
+ // See the TreeView component for details related
+ // to the 'member' object.
+ static get propTypes() {
+ return {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ member: PropTypes.object.isRequired,
+ renderSuffix: PropTypes.func,
+ };
+ }
+
+ render() {
+ const id = this.props.id;
+ const title = this.props.title;
+ const member = this.props.member;
+ const level = member.level || 0;
+ const renderSuffix = this.props.renderSuffix;
+
+ const iconClassList = ["treeIcon"];
+ if (member.hasChildren && member.loading) {
+ iconClassList.push("devtools-throbber");
+ } else if (member.hasChildren) {
+ iconClassList.push("theme-twisty");
+ }
+ if (member.open) {
+ iconClassList.push("open");
+ }
+
+ return dom.td(
+ {
+ className: "treeLabelCell",
+ title,
+ style: {
+ // Compute indentation dynamically. The deeper the item is
+ // inside the hierarchy, the bigger is the left padding.
+ "--tree-label-cell-indent": `${level * 16}px`,
+ },
+ key: "default",
+ role: "presentation",
+ },
+ dom.span({
+ className: iconClassList.join(" "),
+ role: "presentation",
+ }),
+ dom.span(
+ {
+ className: "treeLabel " + member.type + "Label",
+ title,
+ "aria-labelledby": id,
+ "data-level": level,
+ },
+ member.name
+ ),
+ renderSuffix && renderSuffix(member)
+ );
+ }
+ }
+
+ // Exports from this module
+ module.exports = LabelCell;
+});
diff --git a/devtools/client/shared/components/tree/ObjectProvider.js b/devtools/client/shared/components/tree/ObjectProvider.js
new file mode 100644
index 0000000000..48d577ff4d
--- /dev/null
+++ b/devtools/client/shared/components/tree/ObjectProvider.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ /**
+ * Implementation of the default data provider. A provider is state less
+ * object responsible for transformation data (usually a state) to
+ * a structure that can be directly consumed by the tree-view component.
+ */
+ const ObjectProvider = {
+ getChildren(object) {
+ const children = [];
+
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return [];
+ }
+
+ if (typeof object == "string") {
+ return [];
+ }
+
+ for (const prop in object) {
+ try {
+ children.push(new ObjectProperty(prop, object[prop]));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return children;
+ },
+
+ hasChildren(object) {
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return false;
+ }
+
+ if (typeof object == "string") {
+ return false;
+ }
+
+ if (typeof object !== "object") {
+ return false;
+ }
+
+ return !!Object.keys(object).length;
+ },
+
+ getLabel(object) {
+ return object instanceof ObjectProperty ? object.name : null;
+ },
+
+ getValue(object) {
+ return object instanceof ObjectProperty ? object.value : null;
+ },
+
+ getKey(object) {
+ return object instanceof ObjectProperty ? object.name : null;
+ },
+
+ getType(object) {
+ return object instanceof ObjectProperty
+ ? typeof object.value
+ : typeof object;
+ },
+ };
+
+ function ObjectProperty(name, value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ // Exports from this module
+ exports.ObjectProperty = ObjectProperty;
+ exports.ObjectProvider = ObjectProvider;
+});
diff --git a/devtools/client/shared/components/tree/TreeCell.js b/devtools/client/shared/components/tree/TreeCell.js
new file mode 100644
index 0000000000..2fec6ad29d
--- /dev/null
+++ b/devtools/client/shared/components/tree/TreeCell.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const { Component } = require("devtools/client/shared/vendor/react");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { input, span, td } = dom;
+
+ /**
+ * This template represents a cell in TreeView row. It's rendered
+ * using <td> element (the row is <tr> and the entire tree is <table>).
+ */
+ class TreeCell extends Component {
+ // See TreeView component for detailed property explanation.
+ static get propTypes() {
+ return {
+ value: PropTypes.any,
+ decorator: PropTypes.object,
+ id: PropTypes.string.isRequired,
+ member: PropTypes.object.isRequired,
+ renderValue: PropTypes.func.isRequired,
+ enableInput: PropTypes.bool,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ inputEnabled: false,
+ };
+
+ this.getCellClass = this.getCellClass.bind(this);
+ this.updateInputEnabled = this.updateInputEnabled.bind(this);
+ }
+
+ /**
+ * Optimize cell rendering. Rerender cell content only if
+ * the value or expanded state changes.
+ */
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.value != nextProps.value ||
+ this.state !== nextState ||
+ this.props.member.open != nextProps.member.open
+ );
+ }
+
+ getCellClass(object, id) {
+ const decorator = this.props.decorator;
+ if (!decorator || !decorator.getCellClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getCellClass(object, id);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ }
+
+ updateInputEnabled(evt) {
+ this.setState(
+ Object.assign({}, this.state, {
+ inputEnabled: evt.target.nodeName.toLowerCase() !== "input",
+ })
+ );
+ }
+
+ render() {
+ let { member, id, value, decorator, renderValue, enableInput } =
+ this.props;
+ const type = member.type || "";
+
+ // Compute class name list for the <td> element.
+ const classNames = this.getCellClass(member.object, id) || [];
+ classNames.push("treeValueCell");
+ classNames.push(type + "Cell");
+
+ // Render value using a default render function or custom
+ // provided function from props or a decorator.
+ renderValue = renderValue || defaultRenderValue;
+ if (decorator?.renderValue) {
+ renderValue = decorator.renderValue(member.object, id) || renderValue;
+ }
+
+ const props = Object.assign({}, this.props, {
+ object: value,
+ });
+
+ let cellElement;
+ if (enableInput && this.state.inputEnabled && type !== "object") {
+ classNames.push("inputEnabled");
+ cellElement = input({
+ autoFocus: true,
+ onBlur: this.updateInputEnabled,
+ readOnly: true,
+ value,
+ "aria-labelledby": id,
+ });
+ } else {
+ cellElement = span(
+ {
+ onClick: type !== "object" ? this.updateInputEnabled : null,
+ "aria-labelledby": id,
+ },
+ renderValue(props)
+ );
+ }
+
+ // Render me!
+ return td(
+ {
+ className: classNames.join(" "),
+ role: "presentation",
+ },
+ cellElement
+ );
+ }
+ }
+
+ // Default value rendering.
+ const defaultRenderValue = props => {
+ return props.object + "";
+ };
+
+ // Exports from this module
+ module.exports = TreeCell;
+});
diff --git a/devtools/client/shared/components/tree/TreeHeader.js b/devtools/client/shared/components/tree/TreeHeader.js
new file mode 100644
index 0000000000..41986210fc
--- /dev/null
+++ b/devtools/client/shared/components/tree/TreeHeader.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const { Component } = require("devtools/client/shared/vendor/react");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { thead, tr, td, div } = dom;
+
+ /**
+ * This component is responsible for rendering tree header.
+ * It's based on <thead> element.
+ */
+ class TreeHeader extends Component {
+ // See also TreeView component for detailed info about properties.
+ static get propTypes() {
+ return {
+ // Custom tree decorator
+ decorator: PropTypes.object,
+ // True if the header should be visible
+ header: PropTypes.bool,
+ // Array with column definition
+ columns: PropTypes.array,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ columns: [
+ {
+ id: "default",
+ },
+ ],
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.getHeaderClass = this.getHeaderClass.bind(this);
+ }
+
+ getHeaderClass(colId) {
+ const decorator = this.props.decorator;
+ if (!decorator || !decorator.getHeaderClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getHeaderClass(colId);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ }
+
+ render() {
+ const cells = [];
+ const visible = this.props.header;
+
+ // Render the rest of the columns (if any)
+ this.props.columns.forEach(col => {
+ const cellStyle = {
+ width: col.width ? col.width : "",
+ };
+
+ let classNames = [];
+
+ if (visible) {
+ classNames = this.getHeaderClass(col.id);
+ classNames.push("treeHeaderCell");
+ }
+
+ cells.push(
+ td(
+ {
+ className: classNames.join(" "),
+ style: cellStyle,
+ role: "presentation",
+ id: col.id,
+ key: col.id,
+ },
+ visible
+ ? div(
+ {
+ className: "treeHeaderCellBox",
+ role: "presentation",
+ },
+ col.title
+ )
+ : null
+ )
+ );
+ });
+
+ return thead(
+ {
+ role: "presentation",
+ },
+ tr(
+ {
+ className: visible ? "treeHeaderRow" : "",
+ role: "presentation",
+ },
+ cells
+ )
+ );
+ }
+ }
+
+ // Exports from this module
+ module.exports = TreeHeader;
+});
diff --git a/devtools/client/shared/components/tree/TreeRow.js b/devtools/client/shared/components/tree/TreeRow.js
new file mode 100644
index 0000000000..7288d3bdfa
--- /dev/null
+++ b/devtools/client/shared/components/tree/TreeRow.js
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const {
+ Component,
+ createFactory,
+ createRef,
+ } = require("devtools/client/shared/vendor/react");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+ const { tr } = dom;
+
+ // Tree
+ const TreeCell = createFactory(
+ require("devtools/client/shared/components/tree/TreeCell")
+ );
+ const LabelCell = createFactory(
+ require("devtools/client/shared/components/tree/LabelCell")
+ );
+
+ const {
+ wrapMoveFocus,
+ getFocusableElements,
+ } = require("devtools/client/shared/focus");
+
+ const UPDATE_ON_PROPS = [
+ "name",
+ "open",
+ "value",
+ "loading",
+ "level",
+ "selected",
+ "active",
+ "hasChildren",
+ ];
+
+ /**
+ * This template represents a node in TreeView component. It's rendered
+ * using <tr> element (the entire tree is one big <table>).
+ */
+ class TreeRow extends Component {
+ // See TreeView component for more details about the props and
+ // the 'member' object.
+ static get propTypes() {
+ return {
+ member: PropTypes.shape({
+ object: PropTypes.object,
+ name: PropTypes.string,
+ type: PropTypes.string.isRequired,
+ rowClass: PropTypes.string.isRequired,
+ level: PropTypes.number.isRequired,
+ hasChildren: PropTypes.bool,
+ value: PropTypes.any,
+ open: PropTypes.bool.isRequired,
+ path: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
+ selected: PropTypes.bool,
+ active: PropTypes.bool,
+ loading: PropTypes.bool,
+ }),
+ decorator: PropTypes.object,
+ renderCell: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ columns: PropTypes.array.isRequired,
+ id: PropTypes.string.isRequired,
+ provider: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired,
+ onContextMenu: PropTypes.func,
+ onMouseOver: PropTypes.func,
+ onMouseOut: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.treeRowRef = createRef();
+
+ this.getRowClass = this.getRowClass.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ this._setTabbableState();
+
+ // Child components might add/remove new focusable elements, watch for the
+ // additions/removals of descendant nodes and update focusable state.
+ const win = this.treeRowRef.current.ownerDocument.defaultView;
+ const { MutationObserver } = win;
+ this.observer = new MutationObserver(() => {
+ this._setTabbableState();
+ });
+ this.observer.observe(this.treeRowRef.current, {
+ childList: true,
+ subtree: true,
+ });
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ // I don't like accessing the underlying DOM elements directly,
+ // but this optimization makes the filtering so damn fast!
+ // The row doesn't have to be re-rendered, all we really need
+ // to do is toggling a class name.
+ // The important part is that DOM elements don't need to be
+ // re-created when they should appear again.
+ if (nextProps.member.hidden != this.props.member.hidden) {
+ const row = findDOMNode(this);
+ row.classList.toggle("hidden");
+ }
+ }
+
+ /**
+ * Optimize row rendering. If props are the same do not render.
+ * This makes the rendering a lot faster!
+ */
+ shouldComponentUpdate(nextProps) {
+ for (const prop of UPDATE_ON_PROPS) {
+ if (nextProps.member[prop] != this.props.member[prop]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ componentWillUnmount() {
+ this.observer.disconnect();
+ this.observer = null;
+ }
+
+ /**
+ * Makes sure that none of the focusable elements inside the row container
+ * are tabbable if the row is not active. If the row is active and focus
+ * is outside its container, focus on the first focusable element inside.
+ */
+ _setTabbableState() {
+ const elms = getFocusableElements(this.treeRowRef.current);
+ if (elms.length === 0) {
+ return;
+ }
+
+ const { active } = this.props.member;
+ if (!active) {
+ elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
+ return;
+ }
+
+ if (!elms.includes(document.activeElement)) {
+ elms[0].focus();
+ }
+ }
+
+ _onKeyDown(e) {
+ const { target, key, shiftKey } = e;
+
+ if (key !== "Tab") {
+ return;
+ }
+
+ const focusMoved = !!wrapMoveFocus(
+ getFocusableElements(this.treeRowRef.current),
+ target,
+ shiftKey
+ );
+ if (focusMoved) {
+ // Focus was moved to the begining/end of the list, so we need to
+ // prevent the default focus change that would happen here.
+ e.preventDefault();
+ }
+
+ e.stopPropagation();
+ }
+
+ getRowClass(object) {
+ const decorator = this.props.decorator;
+ if (!decorator || !decorator.getRowClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getRowClass(object);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ }
+
+ render() {
+ const member = this.props.member;
+ const decorator = this.props.decorator;
+
+ const props = {
+ id: this.props.id,
+ ref: this.treeRowRef,
+ role: "treeitem",
+ "aria-level": member.level + 1,
+ "aria-selected": !!member.selected,
+ onClick: this.props.onClick,
+ onContextMenu: this.props.onContextMenu,
+ onKeyDownCapture: member.active ? this._onKeyDown : undefined,
+ onMouseOver: this.props.onMouseOver,
+ onMouseOut: this.props.onMouseOut,
+ };
+
+ // Compute class name list for the <tr> element.
+ const classNames = this.getRowClass(member.object) || [];
+ classNames.push("treeRow");
+ classNames.push(member.type + "Row");
+
+ if (member.hasChildren) {
+ classNames.push("hasChildren");
+
+ // There are 2 situations where hasChildren is true:
+ // 1. it is an object with children. Only set aria-expanded in this situation
+ // 2. It is a long string (> 50 chars) that can be expanded to fully display it
+ if (member.type !== "string") {
+ props["aria-expanded"] = member.open;
+ }
+ }
+
+ if (member.open) {
+ classNames.push("opened");
+ }
+
+ if (member.loading) {
+ classNames.push("loading");
+ }
+
+ if (member.selected) {
+ classNames.push("selected");
+ }
+
+ if (member.hidden) {
+ classNames.push("hidden");
+ }
+
+ props.className = classNames.join(" ");
+
+ // The label column (with toggle buttons) is usually
+ // the first one, but there might be cases (like in
+ // the Memory panel) where the toggling is done
+ // in the last column.
+ const cells = [];
+
+ // Get components for rendering cells.
+ let renderCell = this.props.renderCell || RenderCell;
+ let renderLabelCell = this.props.renderLabelCell || RenderLabelCell;
+ if (decorator?.renderLabelCell) {
+ renderLabelCell =
+ decorator.renderLabelCell(member.object) || renderLabelCell;
+ }
+
+ // Render a cell for every column.
+ this.props.columns.forEach(col => {
+ const cellProps = Object.assign({}, this.props, {
+ key: col.id,
+ id: col.id,
+ value: this.props.provider.getValue(member.object, col.id),
+ });
+
+ if (decorator?.renderCell) {
+ renderCell = decorator.renderCell(member.object, col.id);
+ }
+
+ const render = col.id == "default" ? renderLabelCell : renderCell;
+
+ // Some cells don't have to be rendered. This happens when some
+ // other cells span more columns. Note that the label cells contains
+ // toggle buttons and should be usually there unless we are rendering
+ // a simple non-expandable table.
+ if (render) {
+ cells.push(render(cellProps));
+ }
+ });
+
+ // Render tree row
+ return tr(props, cells);
+ }
+ }
+
+ // Helpers
+
+ const RenderCell = props => {
+ return TreeCell(props);
+ };
+
+ const RenderLabelCell = props => {
+ return LabelCell(props);
+ };
+
+ // Exports from this module
+ module.exports = TreeRow;
+});
diff --git a/devtools/client/shared/components/tree/TreeView.css b/devtools/client/shared/components/tree/TreeView.css
new file mode 100644
index 0000000000..e244ff3da9
--- /dev/null
+++ b/devtools/client/shared/components/tree/TreeView.css
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url('chrome://devtools/content/shared/components/reps/reps.css');
+
+/******************************************************************************/
+/* TreeView Colors */
+
+:root {
+ --tree-header-background: #C8D2DC;
+ --tree-header-sorted-background: #AAC3DC;
+}
+
+/******************************************************************************/
+/* TreeView Table*/
+
+.treeTable {
+ color: var(--theme-highlight-blue);
+}
+
+.treeTable .treeLabelCell,
+.treeTable .treeValueCell {
+ padding: 2px 0;
+ padding-inline-start: 4px;
+ line-height: 16px; /* make rows 20px tall */
+ vertical-align: top;
+ overflow: hidden;
+}
+
+.treeTable .treeLabelCell {
+ white-space: nowrap;
+ cursor: default;
+ padding-inline-start: var(--tree-label-cell-indent);
+}
+
+.treeTable .treeLabelCell::after {
+ content: ":";
+ color: var(--object-color);
+}
+
+.treeTable .treeValueCell.inputEnabled {
+ padding-block: 0;
+}
+
+.treeTable .treeValueCell.inputEnabled input {
+ width: 100%;
+ height: 20px;
+ margin: 0;
+ margin-inline-start: -2px;
+ border: solid 1px transparent;
+ outline: none;
+ box-shadow: none;
+ padding: 0 1px;
+ color: var(--theme-text-color-strong);
+ background: var(--theme-sidebar-background);
+}
+
+.treeTable .treeValueCell.inputEnabled input:focus {
+ border-color: var(--theme-textbox-box-shadow);
+ transition: all 150ms ease-in-out;
+}
+
+.treeTable .treeValueCell > [aria-labelledby],
+.treeTable .treeLabelCell > .treeLabel {
+ unicode-bidi: plaintext;
+ text-align: match-parent;
+}
+
+/* No padding if there is actually no label */
+.treeTable .treeLabel:empty {
+ padding-inline-start: 0;
+}
+
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+/* :not(.selected) is used because row selection styles should have
+ more precedence than row hovering. */
+.treeTable .treeRow:not(.selected):hover {
+ background-color: var(--theme-selection-background-hover) !important;
+}
+
+.treeTable .treeRow.selected {
+ background-color: var(--theme-selection-background);
+}
+
+.treeTable .treeRow.selected :where(:not(.objectBox-jsonml)),
+.treeTable .treeRow.selected .treeLabelCell::after {
+ color: var(--theme-selection-color);
+ fill: currentColor;
+}
+
+/* Invert text selection color in selected rows */
+.treeTable .treeRow.selected :not(input, textarea)::selection {
+ color: var(--theme-selection-background);
+ background-color: var(--theme-selection-color);
+}
+
+/* Filtering */
+.treeTable .treeRow.hidden {
+ display: none !important;
+}
+
+.treeTable .treeValueCellDivider {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+/* Learn More link */
+.treeTable .treeValueCell .learn-more-link {
+ user-select: none;
+ color: var(--theme-highlight-blue);
+ cursor: pointer;
+ margin: 0 5px;
+}
+
+.treeTable .treeValueCell .learn-more-link:hover {
+ text-decoration: underline;
+}
+
+/******************************************************************************/
+/* Toggle Icon */
+
+.treeTable .treeRow .treeIcon {
+ box-sizing: content-box;
+ height: 14px;
+ width: 14px;
+ padding: 1px;
+ /* Set the size of loading spinner (see .devtools-throbber) */
+ font-size: 10px;
+ line-height: 14px;
+ display: inline-block;
+ vertical-align: bottom;
+ /* Use a total width of 20px (margins + padding + width) */
+ margin-inline: 3px 1px;
+}
+
+/* All expanded/collapsed styles need to apply on immediate children
+ since there might be nested trees within a tree. */
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
+ cursor: pointer;
+ background-repeat: no-repeat;
+}
+
+/******************************************************************************/
+/* Header */
+
+.treeTable .treeHeaderRow {
+ height: 18px;
+}
+
+.treeTable .treeHeaderCell {
+ cursor: pointer;
+ user-select: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 0 !important;
+ background: linear-gradient(
+ rgba(255, 255, 255, 0.05),
+ rgba(0, 0, 0, 0.05)),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%) repeat-x var(--tree-header-background);
+ color: var(--theme-body-color);
+ white-space: nowrap;
+}
+
+.treeTable .treeHeaderCellBox {
+ padding-block: 2px;
+ padding-inline: 10px 14px;
+}
+
+.treeTable .treeHeaderRow > .treeHeaderCell:first-child > .treeHeaderCellBox {
+ padding: 0;
+}
+
+.treeTable .treeHeaderSorted {
+ background-color: var(--tree-header-sorted-background);
+}
+
+.treeTable .treeHeaderSorted > .treeHeaderCellBox {
+ background: url(chrome://devtools/skin/images/sort-descending-arrow.svg) no-repeat calc(100% - 4px);
+}
+
+.treeTable .treeHeaderSorted.sortedAscending > .treeHeaderCellBox {
+ background-image: url(chrome://devtools/skin/images/sort-ascending-arrow.svg);
+}
+
+.treeTable .treeHeaderCell:hover:active {
+ background-image: linear-gradient(
+ rgba(0, 0, 0, 0.1),
+ transparent),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%);
+}
diff --git a/devtools/client/shared/components/tree/TreeView.js b/devtools/client/shared/components/tree/TreeView.js
new file mode 100644
index 0000000000..d9ef7c0088
--- /dev/null
+++ b/devtools/client/shared/components/tree/TreeView.js
@@ -0,0 +1,799 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const {
+ cloneElement,
+ Component,
+ createFactory,
+ createRef,
+ } = require("devtools/client/shared/vendor/react");
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ ObjectProvider,
+ } = require("devtools/client/shared/components/tree/ObjectProvider");
+ const TreeRow = createFactory(
+ require("devtools/client/shared/components/tree/TreeRow")
+ );
+ const TreeHeader = createFactory(
+ require("devtools/client/shared/components/tree/TreeHeader")
+ );
+
+ const { scrollIntoView } = require("devtools/client/shared/scroll");
+
+ const SUPPORTED_KEYS = [
+ "ArrowUp",
+ "ArrowDown",
+ "ArrowLeft",
+ "ArrowRight",
+ "End",
+ "Home",
+ "Enter",
+ " ",
+ "Escape",
+ ];
+
+ const defaultProps = {
+ object: null,
+ renderRow: null,
+ provider: ObjectProvider,
+ expandedNodes: new Set(),
+ selected: null,
+ defaultSelectFirstNode: true,
+ active: null,
+ expandableStrings: true,
+ columns: [],
+ };
+
+ /**
+ * This component represents a tree view with expandable/collapsible nodes.
+ * The tree is rendered using <table> element where every node is represented
+ * by <tr> element. The tree is one big table where nodes (rows) are properly
+ * indented from the left to mimic hierarchical structure of the data.
+ *
+ * The tree can have arbitrary number of columns and so, might be use
+ * as an expandable tree-table UI widget as well. By default, there is
+ * one column for node label and one for node value.
+ *
+ * The tree is maintaining its (presentation) state, which consists
+ * from list of expanded nodes and list of columns.
+ *
+ * Complete data provider interface:
+ * var TreeProvider = {
+ * getChildren: function(object);
+ * hasChildren: function(object);
+ * getLabel: function(object, colId);
+ * getLevel: function(object); // optional
+ * getValue: function(object, colId);
+ * getKey: function(object);
+ * getType: function(object);
+ * }
+ *
+ * Complete tree decorator interface:
+ * var TreeDecorator = {
+ * getRowClass: function(object);
+ * getCellClass: function(object, colId);
+ * getHeaderClass: function(colId);
+ * renderValue: function(object, colId);
+ * renderRow: function(object);
+ * renderCell: function(object, colId);
+ * renderLabelCell: function(object);
+ * }
+ */
+ class TreeView extends Component {
+ // The only required property (not set by default) is the input data
+ // object that is used to populate the tree.
+ static get propTypes() {
+ return {
+ // The input data object.
+ object: PropTypes.any,
+ className: PropTypes.string,
+ label: PropTypes.string,
+ // Data provider (see also the interface above)
+ provider: PropTypes.shape({
+ getChildren: PropTypes.func,
+ hasChildren: PropTypes.func,
+ getLabel: PropTypes.func,
+ getValue: PropTypes.func,
+ getKey: PropTypes.func,
+ getLevel: PropTypes.func,
+ getType: PropTypes.func,
+ }).isRequired,
+ // Tree decorator (see also the interface above)
+ decorator: PropTypes.shape({
+ getRowClass: PropTypes.func,
+ getCellClass: PropTypes.func,
+ getHeaderClass: PropTypes.func,
+ renderValue: PropTypes.func,
+ renderRow: PropTypes.func,
+ renderCell: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ }),
+ // Custom tree row (node) renderer
+ renderRow: PropTypes.func,
+ // Custom cell renderer
+ renderCell: PropTypes.func,
+ // Custom value renderer
+ renderValue: PropTypes.func,
+ // Custom tree label (including a toggle button) renderer
+ renderLabelCell: PropTypes.func,
+ // Set of expanded nodes
+ expandedNodes: PropTypes.object,
+ // Selected node
+ selected: PropTypes.string,
+ // Select first node by default
+ defaultSelectFirstNode: PropTypes.bool,
+ // The currently active (keyboard) item, if any such item exists.
+ active: PropTypes.string,
+ // Custom filtering callback
+ onFilter: PropTypes.func,
+ // Custom sorting callback
+ onSort: PropTypes.func,
+ // Custom row click callback
+ onClickRow: PropTypes.func,
+ // Row context menu event handler
+ onContextMenuRow: PropTypes.func,
+ // Tree context menu event handler
+ onContextMenuTree: PropTypes.func,
+ // A header is displayed if set to true
+ header: PropTypes.bool,
+ // Long string is expandable by a toggle button
+ expandableStrings: PropTypes.bool,
+ // Array of columns
+ columns: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ width: PropTypes.string,
+ })
+ ),
+ };
+ }
+
+ static get defaultProps() {
+ return defaultProps;
+ }
+
+ static subPath(path, subKey) {
+ return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
+ }
+
+ /**
+ * Creates a set with the paths of the nodes that should be expanded by default
+ * according to the passed options.
+ * @param {Object} The root node of the tree.
+ * @param {Object} [optional] An object with the following optional parameters:
+ * - maxLevel: nodes nested deeper than this level won't be expanded.
+ * - maxNodes: maximum number of nodes that can be expanded. The traversal is
+ breadth-first, so expanding nodes nearer to the root will be preferred.
+ Sibling nodes will either be all expanded or none expanded.
+ * }
+ */
+ static getExpandedNodes(
+ rootObj,
+ { maxLevel = Infinity, maxNodes = Infinity } = {}
+ ) {
+ const expandedNodes = new Set();
+ const queue = [
+ {
+ object: rootObj,
+ level: 1,
+ path: "",
+ },
+ ];
+ while (queue.length) {
+ const { object, level, path } = queue.shift();
+ if (Object(object) !== object) {
+ continue;
+ }
+ const keys = Object.keys(object);
+ if (expandedNodes.size + keys.length > maxNodes) {
+ // Avoid having children half expanded.
+ break;
+ }
+ for (const key of keys) {
+ const nodePath = TreeView.subPath(path, key);
+ expandedNodes.add(nodePath);
+ if (level < maxLevel) {
+ queue.push({
+ object: object[key],
+ level: level + 1,
+ path: nodePath,
+ });
+ }
+ }
+ }
+ return expandedNodes;
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ expandedNodes: props.expandedNodes,
+ columns: ensureDefaultColumn(props.columns),
+ selected: props.selected,
+ active: props.active,
+ lastSelectedIndex: props.defaultSelectFirstNode ? 0 : null,
+ mouseDown: false,
+ };
+
+ this.treeRef = createRef();
+
+ this.toggle = this.toggle.bind(this);
+ this.isExpanded = this.isExpanded.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onClickRow = this.onClickRow.bind(this);
+ this.getSelectedRow = this.getSelectedRow.bind(this);
+ this.selectRow = this.selectRow.bind(this);
+ this.activateRow = this.activateRow.bind(this);
+ this.isSelected = this.isSelected.bind(this);
+ this.onFilter = this.onFilter.bind(this);
+ this.onSort = this.onSort.bind(this);
+ this.getMembers = this.getMembers.bind(this);
+ this.renderRows = this.renderRows.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { expandedNodes, selected } = nextProps;
+ const state = {
+ expandedNodes,
+ lastSelectedIndex: this.getSelectedRowIndex(),
+ };
+
+ if (selected) {
+ state.selected = selected;
+ }
+
+ this.setState(Object.assign({}, this.state, state));
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const {
+ expandedNodes,
+ columns,
+ selected,
+ active,
+ lastSelectedIndex,
+ mouseDown,
+ } = this.state;
+
+ return (
+ expandedNodes !== nextState.expandedNodes ||
+ columns !== nextState.columns ||
+ selected !== nextState.selected ||
+ active !== nextState.active ||
+ lastSelectedIndex !== nextState.lastSelectedIndex ||
+ mouseDown === nextState.mouseDown
+ );
+ }
+
+ componentDidUpdate() {
+ const selected = this.getSelectedRow();
+ if (selected || this.state.active) {
+ return;
+ }
+
+ const rows = this.visibleRows;
+ if (rows.length === 0) {
+ return;
+ }
+
+ // Only select a row if there is a previous lastSelected Index
+ // This mostly happens when the treeview is loaded the first time
+ if (this.state.lastSelectedIndex !== null) {
+ this.selectRow(
+ rows[Math.min(this.state.lastSelectedIndex, rows.length - 1)],
+ { alignTo: "top" }
+ );
+ }
+ }
+
+ /**
+ * Get rows that are currently visible. Some rows can be filtered and made
+ * invisible, in which case, when navigating around the tree we need to
+ * ignore the ones that are not reachable by the user.
+ */
+ get visibleRows() {
+ return this.rows.filter(row => {
+ const rowEl = findDOMNode(row);
+ return rowEl?.offsetParent;
+ });
+ }
+
+ // Node expand/collapse
+
+ toggle(nodePath) {
+ const nodes = this.state.expandedNodes;
+ if (this.isExpanded(nodePath)) {
+ nodes.delete(nodePath);
+ } else {
+ nodes.add(nodePath);
+ }
+
+ // Compute new state and update the tree.
+ this.setState(
+ Object.assign({}, this.state, {
+ expandedNodes: nodes,
+ })
+ );
+ }
+
+ isExpanded(nodePath) {
+ return this.state.expandedNodes.has(nodePath);
+ }
+
+ // Event Handlers
+
+ onFocus(_event) {
+ if (this.state.mouseDown) {
+ return;
+ }
+ // Set focus to the first element, if none is selected or activated
+ // This is needed because keyboard navigation won't work without an element being selected
+ this.componentDidUpdate();
+ }
+
+ // eslint-disable-next-line complexity
+ onKeyDown(event) {
+ const keyEligibleForFirstLetterNavigation = event.key.length === 1;
+ if (
+ (!SUPPORTED_KEYS.includes(event.key) &&
+ !keyEligibleForFirstLetterNavigation) ||
+ event.shiftKey ||
+ event.ctrlKey ||
+ event.metaKey ||
+ event.altKey
+ ) {
+ return;
+ }
+
+ const row = this.getSelectedRow();
+ if (!row) {
+ return;
+ }
+
+ const rows = this.visibleRows;
+ const index = rows.indexOf(row);
+ const { hasChildren, open } = row.props.member;
+
+ switch (event.key) {
+ case "ArrowRight":
+ if (hasChildren) {
+ if (open) {
+ const firstChildRow = this.rows
+ .slice(index + 1)
+ .find(r => r.props.member.level > row.props.member.level);
+ if (firstChildRow) {
+ this.selectRow(firstChildRow, { alignTo: "bottom" });
+ }
+ } else {
+ this.toggle(this.state.selected);
+ }
+ }
+ break;
+ case "ArrowLeft":
+ if (hasChildren && open) {
+ this.toggle(this.state.selected);
+ } else {
+ const parentRow = rows
+ .slice(0, index)
+ .reverse()
+ .find(r => r.props.member.level < row.props.member.level);
+ if (parentRow) {
+ this.selectRow(parentRow, { alignTo: "top" });
+ }
+ }
+ break;
+ case "ArrowDown":
+ const nextRow = rows[index + 1];
+ if (nextRow) {
+ this.selectRow(nextRow, { alignTo: "bottom" });
+ }
+ break;
+ case "ArrowUp":
+ const previousRow = rows[index - 1];
+ if (previousRow) {
+ this.selectRow(previousRow, { alignTo: "top" });
+ }
+ break;
+ case "Home":
+ const firstRow = rows[0];
+
+ if (firstRow) {
+ this.selectRow(firstRow, { alignTo: "top" });
+ }
+ break;
+ case "End":
+ const lastRow = rows[rows.length - 1];
+ if (lastRow) {
+ this.selectRow(lastRow, { alignTo: "bottom" });
+ }
+ break;
+ case "Enter":
+ case " ":
+ // On space or enter make selected row active. This means keyboard
+ // focus handling is passed on to the tree row itself.
+ if (this.treeRef.current === document.activeElement) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (this.state.active !== this.state.selected) {
+ this.activateRow(this.state.selected);
+ }
+
+ return;
+ }
+ break;
+ case "Escape":
+ event.stopPropagation();
+ if (this.state.active != null) {
+ this.activateRow(null);
+ }
+ break;
+ }
+
+ if (keyEligibleForFirstLetterNavigation) {
+ const next = rows
+ .slice(index + 1)
+ .find(r => r.props.member.name.startsWith(event.key));
+ if (next) {
+ this.selectRow(next, { alignTo: "bottom" });
+ }
+ }
+
+ // Focus should always remain on the tree container itself.
+ this.treeRef.current.focus();
+ event.preventDefault();
+ }
+
+ onClickRow(nodePath, event) {
+ const onClickRow = this.props.onClickRow;
+ const row = this.visibleRows.find(r => r.props.member.path === nodePath);
+
+ // Call custom click handler and bail out if it returns true.
+ if (
+ onClickRow &&
+ onClickRow.call(this, nodePath, event, row.props.member)
+ ) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ const cell = event.target.closest("td");
+ if (cell && cell.classList.contains("treeLabelCell")) {
+ this.toggle(nodePath);
+ }
+
+ this.selectRow(row, { preventAutoScroll: true });
+ }
+
+ onContextMenu(member, event) {
+ const onContextMenuRow = this.props.onContextMenuRow;
+ if (onContextMenuRow) {
+ onContextMenuRow.call(this, member, event);
+ }
+ }
+
+ getSelectedRow() {
+ const rows = this.visibleRows;
+ if (!this.state.selected || rows.length === 0) {
+ return null;
+ }
+ return rows.find(row => this.isSelected(row.props.member.path));
+ }
+
+ getSelectedRowIndex() {
+ const row = this.getSelectedRow();
+ if (!row) {
+ return this.props.defaultSelectFirstNode ? 0 : null;
+ }
+
+ return this.visibleRows.indexOf(row);
+ }
+
+ _scrollIntoView(row, options = {}) {
+ const treeEl = this.treeRef.current;
+ if (!treeEl || !row) {
+ return;
+ }
+
+ const { props: { member: { path } = {} } = {} } = row;
+ if (!path) {
+ return;
+ }
+
+ const element = treeEl.ownerDocument.getElementById(path);
+ if (!element) {
+ return;
+ }
+
+ scrollIntoView(element, { ...options });
+ }
+
+ selectRow(row, options = {}) {
+ const { props: { member: { path } = {} } = {} } = row;
+ if (this.isSelected(path)) {
+ return;
+ }
+
+ if (this.state.active != null) {
+ const treeEl = this.treeRef.current;
+ if (treeEl && treeEl !== treeEl.ownerDocument.activeElement) {
+ treeEl.focus();
+ }
+ }
+
+ if (!options.preventAutoScroll) {
+ this._scrollIntoView(row, options);
+ }
+
+ this.setState({
+ ...this.state,
+ selected: path,
+ active: null,
+ });
+ }
+
+ activateRow(active) {
+ this.setState({
+ ...this.state,
+ active,
+ });
+ }
+
+ isSelected(nodePath) {
+ return nodePath === this.state.selected;
+ }
+
+ isActive(nodePath) {
+ return nodePath === this.state.active;
+ }
+
+ // Filtering & Sorting
+
+ /**
+ * Filter out nodes that don't correspond to the current filter.
+ * @return {Boolean} true if the node should be visible otherwise false.
+ */
+ onFilter(object) {
+ const onFilter = this.props.onFilter;
+ return onFilter ? onFilter(object) : true;
+ }
+
+ onSort(parent, children) {
+ const onSort = this.props.onSort;
+ return onSort ? onSort(parent, children) : children;
+ }
+
+ // Members
+
+ /**
+ * Return children node objects (so called 'members') for given
+ * parent object.
+ */
+ getMembers(parent, level, path) {
+ // Strings don't have children. Note that 'long' strings are using
+ // the expander icon (+/-) to display the entire original value,
+ // but there are no child items.
+ if (typeof parent == "string") {
+ return [];
+ }
+
+ const { expandableStrings, provider } = this.props;
+ let children = provider.getChildren(parent) || [];
+
+ // If the return value is non-array, the children
+ // are being loaded asynchronously.
+ if (!Array.isArray(children)) {
+ return children;
+ }
+
+ children = this.onSort(parent, children) || children;
+
+ return children.map(child => {
+ const key = provider.getKey(child);
+ const nodePath = TreeView.subPath(path, key);
+ const type = provider.getType(child);
+ let hasChildren = provider.hasChildren(child);
+
+ // Value with no column specified is used for optimization.
+ // The row is re-rendered only if this value changes.
+ // Value for actual column is get when a cell is rendered.
+ const value = provider.getValue(child);
+
+ if (expandableStrings && isLongString(value)) {
+ hasChildren = true;
+ }
+
+ // Return value is a 'member' object containing meta-data about
+ // tree node. It describes node label, value, type, etc.
+ return {
+ // An object associated with this node.
+ object: child,
+ // A label for the child node
+ name: provider.getLabel(child),
+ // Data type of the child node (used for CSS customization)
+ type,
+ // Class attribute computed from the type.
+ rowClass: "treeRow-" + type,
+ // Level of the child within the hierarchy (top == 0)
+ level: provider.getLevel ? provider.getLevel(child, level) : level,
+ // True if this node has children.
+ hasChildren,
+ // Value associated with this node (as provided by the data provider)
+ value,
+ // True if the node is expanded.
+ open: this.isExpanded(nodePath),
+ // Node path
+ path: nodePath,
+ // True if the node is hidden (used for filtering)
+ hidden: !this.onFilter(child),
+ // True if the node is selected with keyboard
+ selected: this.isSelected(nodePath),
+ // True if the node is activated with keyboard
+ active: this.isActive(nodePath),
+ };
+ });
+ }
+
+ /**
+ * Render tree rows/nodes.
+ */
+ renderRows(parent, level = 0, path = "") {
+ let rows = [];
+ const decorator = this.props.decorator;
+ let renderRow = this.props.renderRow || TreeRow;
+
+ // Get children for given parent node, iterate over them and render
+ // a row for every one. Use row template (a component) from properties.
+ // If the return value is non-array, the children are being loaded
+ // asynchronously.
+ const members = this.getMembers(parent, level, path);
+ if (!Array.isArray(members)) {
+ return members;
+ }
+
+ members.forEach(member => {
+ if (decorator?.renderRow) {
+ renderRow = decorator.renderRow(member.object) || renderRow;
+ }
+
+ const props = Object.assign({}, this.props, {
+ key: `${member.path}-${member.active ? "active" : "inactive"}`,
+ member,
+ columns: this.state.columns,
+ id: member.path,
+ ref: row => row && this.rows.push(row),
+ onClick: this.onClickRow.bind(this, member.path),
+ onContextMenu: this.onContextMenu.bind(this, member),
+ });
+
+ // Render single row.
+ rows.push(renderRow(props));
+
+ // If a child node is expanded render its rows too.
+ if (member.hasChildren && member.open) {
+ const childRows = this.renderRows(
+ member.object,
+ level + 1,
+ member.path
+ );
+
+ // If children needs to be asynchronously fetched first,
+ // set 'loading' property to the parent row. Otherwise
+ // just append children rows to the array of all rows.
+ if (!Array.isArray(childRows)) {
+ const lastIndex = rows.length - 1;
+ props.member.loading = true;
+ rows[lastIndex] = cloneElement(rows[lastIndex], props);
+ } else {
+ rows = rows.concat(childRows);
+ }
+ }
+ });
+
+ return rows;
+ }
+
+ render() {
+ const root = this.props.object;
+ const classNames = ["treeTable"];
+ this.rows = [];
+
+ const { className, onContextMenuTree } = this.props;
+ // Use custom class name from props.
+ if (className) {
+ classNames.push(...className.split(" "));
+ }
+
+ // Alright, let's render all tree rows. The tree is one big <table>.
+ let rows = this.renderRows(root, 0, "");
+
+ // This happens when the view needs to do initial asynchronous
+ // fetch for the root object. The tree might provide a hook API
+ // for rendering animated spinner (just like for tree nodes).
+ if (!Array.isArray(rows)) {
+ rows = [];
+ }
+
+ const props = Object.assign({}, this.props, {
+ columns: this.state.columns,
+ });
+
+ return dom.table(
+ {
+ className: classNames.join(" "),
+ role: "tree",
+ ref: this.treeRef,
+ tabIndex: 0,
+ onFocus: this.onFocus,
+ onKeyDown: this.onKeyDown,
+ onContextMenu: onContextMenuTree && onContextMenuTree.bind(this),
+ onMouseDown: () => this.setState({ mouseDown: true }),
+ onMouseUp: () => this.setState({ mouseDown: false }),
+ onClick: () => {
+ // Focus should always remain on the tree container itself.
+ this.treeRef.current.focus();
+ },
+ onBlur: event => {
+ if (this.state.active != null) {
+ const { relatedTarget } = event;
+ if (!this.treeRef.current.contains(relatedTarget)) {
+ this.activateRow(null);
+ }
+ }
+ },
+ "aria-label": this.props.label || "",
+ "aria-activedescendant": this.state.selected,
+ cellPadding: 0,
+ cellSpacing: 0,
+ },
+ TreeHeader(props),
+ dom.tbody(
+ {
+ role: "presentation",
+ tabIndex: -1,
+ },
+ rows
+ )
+ );
+ }
+ }
+
+ // Helpers
+
+ /**
+ * There should always be at least one column (the one with toggle buttons)
+ * and this function ensures that it's true.
+ */
+ function ensureDefaultColumn(columns) {
+ if (!columns) {
+ columns = [];
+ }
+
+ const defaultColumn = columns.filter(col => col.id == "default");
+ if (defaultColumn.length) {
+ return columns;
+ }
+
+ // The default column is usually the first one.
+ return [{ id: "default" }, ...columns];
+ }
+
+ function isLongString(value) {
+ return typeof value == "string" && value.length > 50;
+ }
+
+ // Exports from this module
+ module.exports = TreeView;
+});
diff --git a/devtools/client/shared/components/tree/moz.build b/devtools/client/shared/components/tree/moz.build
new file mode 100644
index 0000000000..0700575f17
--- /dev/null
+++ b/devtools/client/shared/components/tree/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "LabelCell.js",
+ "ObjectProvider.js",
+ "TreeCell.js",
+ "TreeHeader.js",
+ "TreeRow.js",
+ "TreeView.js",
+)