summaryrefslogtreecommitdiffstats
path: root/devtools/shared
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/shared
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared')
-rw-r--r--devtools/shared/.eslintrc.js13
-rw-r--r--devtools/shared/DevToolsInfaillibleUtils.sys.mjs101
-rw-r--r--devtools/shared/DevToolsUtils.js1027
-rw-r--r--devtools/shared/ThreadSafeDevToolsUtils.js363
-rw-r--r--devtools/shared/accessibility.js192
-rw-r--r--devtools/shared/async-storage.js226
-rw-r--r--devtools/shared/async-utils.js71
-rw-r--r--devtools/shared/commands/README.md51
-rw-r--r--devtools/shared/commands/commands-factory.js245
-rwxr-xr-xdevtools/shared/commands/create-command.sh129
-rw-r--r--devtools/shared/commands/index.js135
-rw-r--r--devtools/shared/commands/inspected-window/inspected-window-command.js145
-rw-r--r--devtools/shared/commands/inspected-window/moz.build10
-rw-r--r--devtools/shared/commands/inspected-window/tests/browser.toml20
-rw-r--r--devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js523
-rw-r--r--devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js315
-rw-r--r--devtools/shared/commands/inspected-window/tests/head.js12
-rw-r--r--devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs87
-rw-r--r--devtools/shared/commands/inspector/inspector-command.js483
-rw-r--r--devtools/shared/commands/inspector/moz.build10
-rw-r--r--devtools/shared/commands/inspector/tests/browser.toml22
-rw-r--r--devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js140
-rw-r--r--devtools/shared/commands/inspector/tests/browser_inspector_command_getNodeFrontSelectorsFromTopDocument.js119
-rw-r--r--devtools/shared/commands/inspector/tests/browser_inspector_command_getSuggestionsForQuery.js124
-rw-r--r--devtools/shared/commands/inspector/tests/browser_inspector_command_search.js98
-rw-r--r--devtools/shared/commands/inspector/tests/head.js14
-rw-r--r--devtools/shared/commands/moz.build22
-rw-r--r--devtools/shared/commands/network/moz.build10
-rw-r--r--devtools/shared/commands/network/network-command.js96
-rw-r--r--devtools/shared/commands/network/tests/browser.toml13
-rw-r--r--devtools/shared/commands/network/tests/browser_network_command_request_blocking.js61
-rw-r--r--devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js78
-rw-r--r--devtools/shared/commands/network/tests/head.js12
-rw-r--r--devtools/shared/commands/object/moz.build10
-rw-r--r--devtools/shared/commands/object/object-command.js63
-rw-r--r--devtools/shared/commands/object/tests/browser.toml9
-rw-r--r--devtools/shared/commands/object/tests/browser_object.js125
-rw-r--r--devtools/shared/commands/object/tests/head.js12
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/console-messages.js59
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/css-changes.js28
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/error-messages.js62
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/moz.build14
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/platform-messages.js44
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/reflow.js24
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/root-node.js61
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/source.js88
-rw-r--r--devtools/shared/commands/resource/legacy-listeners/thread-states.js81
-rw-r--r--devtools/shared/commands/resource/moz.build15
-rw-r--r--devtools/shared/commands/resource/resource-command.js1367
-rw-r--r--devtools/shared/commands/resource/tests/breakpoint_document.html21
-rw-r--r--devtools/shared/commands/resource/tests/browser.toml128
-rw-r--r--devtools/shared/commands/resource/tests/browser_browser_resources_console_messages.js87
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_clear_resources.js90
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_client_caching.js380
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_console_messages.js623
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_console_messages_navigation.js190
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_console_messages_workers.js257
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_css_changes.js151
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_css_messages.js212
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_css_registered_properties.js384
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_document_events.js720
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_error_messages.js877
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_getAllResources.js128
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_invalid_api_usage.js76
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_last_private_context_exit.js94
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_network_event_stacktraces.js100
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_network_events.js318
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_network_events_cache.js236
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_network_events_navigation.js137
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_network_events_parent_process.js249
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_platform_messages.js158
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_reflows.js115
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_root_node.js129
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_scope_flag.js128
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_server_sent_events.js107
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_several_resources.js111
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_sources.js456
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_stylesheets.js713
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_stylesheets_header.js82
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_stylesheets_import.js60
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_stylesheets_navigation.js254
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_stylesheets_nested_iframes.js34
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_target_destroy.js104
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_target_resources_race.js70
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_target_switching.js94
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_thread_states.js557
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_unwatch_early.js113
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_watch_unwatch_multiple.js88
-rw-r--r--devtools/shared/commands/resource/tests/browser_resources_websocket.js245
-rw-r--r--devtools/shared/commands/resource/tests/doc_console.html18
-rw-r--r--devtools/shared/commands/resource/tests/doc_console_iframe.html16
-rw-r--r--devtools/shared/commands/resource/tests/early_console_document.html14
-rw-r--r--devtools/shared/commands/resource/tests/empty.html11
-rw-r--r--devtools/shared/commands/resource/tests/fission_document.html23
-rw-r--r--devtools/shared/commands/resource/tests/fission_document_workers.html47
-rw-r--r--devtools/shared/commands/resource/tests/fission_iframe.html12
-rw-r--r--devtools/shared/commands/resource/tests/fission_iframe_workers.html29
-rw-r--r--devtools/shared/commands/resource/tests/head.js151
-rw-r--r--devtools/shared/commands/resource/tests/network_document.html13
-rw-r--r--devtools/shared/commands/resource/tests/network_document_navigation.html14
-rw-r--r--devtools/shared/commands/resource/tests/network_navigation.js1
-rw-r--r--devtools/shared/commands/resource/tests/service-worker-sources.js2
-rw-r--r--devtools/shared/commands/resource/tests/sources.html53
-rw-r--r--devtools/shared/commands/resource/tests/sources.js2
-rw-r--r--devtools/shared/commands/resource/tests/sse_backend.sjs8
-rw-r--r--devtools/shared/commands/resource/tests/sse_frontend.html31
-rw-r--r--devtools/shared/commands/resource/tests/sse_frontend_iframe.html29
-rw-r--r--devtools/shared/commands/resource/tests/style_document.css1
-rw-r--r--devtools/shared/commands/resource/tests/style_document.html22
-rw-r--r--devtools/shared/commands/resource/tests/style_iframe.css1
-rw-r--r--devtools/shared/commands/resource/tests/style_iframe.html15
-rw-r--r--devtools/shared/commands/resource/tests/stylesheets-nested-iframes.html27
-rw-r--r--devtools/shared/commands/resource/tests/test_image.pngbin0 -> 580 bytes
-rw-r--r--devtools/shared/commands/resource/tests/test_service_worker.js11
-rw-r--r--devtools/shared/commands/resource/tests/test_worker.js15
-rw-r--r--devtools/shared/commands/resource/tests/websocket_backend_wsh.py20
-rw-r--r--devtools/shared/commands/resource/tests/websocket_frontend.html45
-rw-r--r--devtools/shared/commands/resource/tests/websocket_frontend_iframe.html41
-rw-r--r--devtools/shared/commands/resource/tests/worker-sources.js2
-rw-r--r--devtools/shared/commands/resource/transformers/console-messages.js23
-rw-r--r--devtools/shared/commands/resource/transformers/error-messages.js31
-rw-r--r--devtools/shared/commands/resource/transformers/moz.build16
-rw-r--r--devtools/shared/commands/resource/transformers/network-events.js16
-rw-r--r--devtools/shared/commands/resource/transformers/storage-cache.js22
-rw-r--r--devtools/shared/commands/resource/transformers/storage-cookie.js26
-rw-r--r--devtools/shared/commands/resource/transformers/storage-extension.js26
-rw-r--r--devtools/shared/commands/resource/transformers/storage-indexed-db.js26
-rw-r--r--devtools/shared/commands/resource/transformers/storage-local-storage.js22
-rw-r--r--devtools/shared/commands/resource/transformers/storage-session-storage.js22
-rw-r--r--devtools/shared/commands/resource/transformers/thread-states.js32
-rw-r--r--devtools/shared/commands/root-resource/moz.build7
-rw-r--r--devtools/shared/commands/root-resource/root-resource-command.js348
-rw-r--r--devtools/shared/commands/script/moz.build10
-rw-r--r--devtools/shared/commands/script/script-command.js157
-rw-r--r--devtools/shared/commands/script/tests/browser.toml15
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_basic.js1050
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js41
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js85
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_throw.js75
-rw-r--r--devtools/shared/commands/script/tests/head.js51
-rw-r--r--devtools/shared/commands/target-configuration/moz.build10
-rw-r--r--devtools/shared/commands/target-configuration/target-configuration-command.js124
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser.toml34
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js107
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js183
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js309
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js187
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js264
-rw-r--r--devtools/shared/commands/target-configuration/tests/head.js12
-rw-r--r--devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs100
-rw-r--r--devtools/shared/commands/target/actions/moz.build7
-rw-r--r--devtools/shared/commands/target/actions/targets.js33
-rw-r--r--devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js72
-rw-r--r--devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js302
-rw-r--r--devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js19
-rw-r--r--devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js234
-rw-r--r--devtools/shared/commands/target/legacy-target-watchers/moz.build10
-rw-r--r--devtools/shared/commands/target/moz.build17
-rw-r--r--devtools/shared/commands/target/reducers/moz.build7
-rw-r--r--devtools/shared/commands/target/reducers/targets.js70
-rw-r--r--devtools/shared/commands/target/selectors/moz.build7
-rw-r--r--devtools/shared/commands/target/selectors/targets.js20
-rw-r--r--devtools/shared/commands/target/target-command.js1167
-rw-r--r--devtools/shared/commands/target/tests/browser.toml67
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_bfcache.js505
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_browser_workers.js246
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_detach.js59
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_frames.js649
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_frames_popups.js168
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js104
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js119
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js78
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_processes.js242
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_reload.js115
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_scope_flag.js190
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_service_workers.js77
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js358
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js138
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_tab_workers.js322
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js134
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js284
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_watchTargets.js214
-rw-r--r--devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js87
-rw-r--r--devtools/shared/commands/target/tests/fission_document.html47
-rw-r--r--devtools/shared/commands/target/tests/fission_iframe.html29
-rw-r--r--devtools/shared/commands/target/tests/head.js32
-rw-r--r--devtools/shared/commands/target/tests/incremental-js-value-script.sjs23
-rw-r--r--devtools/shared/commands/target/tests/simple_document.html12
-rw-r--r--devtools/shared/commands/target/tests/test_service_worker.js11
-rw-r--r--devtools/shared/commands/target/tests/test_sw_page.html19
-rw-r--r--devtools/shared/commands/target/tests/test_sw_page_worker.js5
-rw-r--r--devtools/shared/commands/target/tests/test_worker.js13
-rw-r--r--devtools/shared/commands/thread-configuration/moz.build7
-rw-r--r--devtools/shared/commands/thread-configuration/tests/browser.toml7
-rw-r--r--devtools/shared/commands/thread-configuration/tests/head.js12
-rw-r--r--devtools/shared/commands/thread-configuration/thread-configuration-command.js72
-rw-r--r--devtools/shared/commands/tracer/moz.build7
-rw-r--r--devtools/shared/commands/tracer/tracer-command.js85
-rw-r--r--devtools/shared/compatibility/README.md27
-rw-r--r--devtools/shared/compatibility/bin/update.js202
-rw-r--r--devtools/shared/compatibility/compatibility-user-settings.js127
-rw-r--r--devtools/shared/compatibility/constants.js27
-rw-r--r--devtools/shared/compatibility/dataset/css-properties.json1
-rw-r--r--devtools/shared/compatibility/dataset/moz.build9
-rw-r--r--devtools/shared/compatibility/helpers.js112
-rw-r--r--devtools/shared/compatibility/moz.build18
-rw-r--r--devtools/shared/compatibility/package.json13
-rw-r--r--devtools/shared/constants.js166
-rw-r--r--devtools/shared/content-observer.js73
-rw-r--r--devtools/shared/css/color-db.js323
-rw-r--r--devtools/shared/css/color.js766
-rw-r--r--devtools/shared/css/constants.js38
-rw-r--r--devtools/shared/css/lexer.js1522
-rw-r--r--devtools/shared/css/moz.build13
-rw-r--r--devtools/shared/css/parsing-utils.js783
-rw-r--r--devtools/shared/debounce.js45
-rw-r--r--devtools/shared/discovery/discovery.js427
-rw-r--r--devtools/shared/discovery/moz.build11
-rw-r--r--devtools/shared/discovery/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/discovery/tests/xpcshell/test_discovery.js158
-rw-r--r--devtools/shared/discovery/tests/xpcshell/xpcshell.toml6
-rw-r--r--devtools/shared/dom-helpers.js51
-rw-r--r--devtools/shared/dom-node-constants.js27
-rw-r--r--devtools/shared/dom-node-filter-constants.js25
-rw-r--r--devtools/shared/event-emitter.js470
-rw-r--r--devtools/shared/extend.js15
-rw-r--r--devtools/shared/flags.js69
-rw-r--r--devtools/shared/generate-uuid.js14
-rw-r--r--devtools/shared/heapsnapshot/AutoMemMap.cpp67
-rw-r--r--devtools/shared/heapsnapshot/AutoMemMap.h77
-rw-r--r--devtools/shared/heapsnapshot/CensusUtils.js502
-rw-r--r--devtools/shared/heapsnapshot/CoreDump.pb.cc2242
-rw-r--r--devtools/shared/heapsnapshot/CoreDump.pb.h2883
-rw-r--r--devtools/shared/heapsnapshot/CoreDump.proto152
-rw-r--r--devtools/shared/heapsnapshot/DeserializedNode.cpp124
-rw-r--r--devtools/shared/heapsnapshot/DeserializedNode.h308
-rw-r--r--devtools/shared/heapsnapshot/DominatorTree.cpp133
-rw-r--r--devtools/shared/heapsnapshot/DominatorTree.h66
-rw-r--r--devtools/shared/heapsnapshot/DominatorTreeNode.js378
-rw-r--r--devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp81
-rw-r--r--devtools/shared/heapsnapshot/FileDescriptorOutputStream.h38
-rw-r--r--devtools/shared/heapsnapshot/HeapAnalyses.worker.js336
-rw-r--r--devtools/shared/heapsnapshot/HeapAnalysesClient.js285
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshot.cpp1581
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshot.h216
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js85
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h31
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp56
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h36
-rw-r--r--devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl37
-rw-r--r--devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp79
-rw-r--r--devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h69
-rw-r--r--devtools/shared/heapsnapshot/census-tree-node.js764
-rwxr-xr-xdevtools/shared/heapsnapshot/generate-core-dump-sources.sh26
-rw-r--r--devtools/shared/heapsnapshot/moz.build59
-rw-r--r--devtools/shared/heapsnapshot/shortest-paths.js93
-rw-r--r--devtools/shared/heapsnapshot/tests/browser/browser.toml9
-rw-r--r--devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js30
-rw-r--r--devtools/shared/heapsnapshot/tests/chrome/chrome.toml7
-rw-r--r--devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html40
-rw-r--r--devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html27
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp95
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp98
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp7
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DevTools.h217
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp70
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp61
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp49
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp34
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp27
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/moz.build32
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs176
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs218
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js54
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js554
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js52
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js49
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js48
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js55
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js141
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js49
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js119
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js37
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js124
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js150
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js24
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js41
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js30
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js97
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js62
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js20
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js56
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js19
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js111
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js57
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js28
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js91
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js15
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js62
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js64
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js24
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js26
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js53
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js133
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js114
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js57
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js93
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js33
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js88
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js43
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js61
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js33
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js61
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js35
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js43
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js31
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js127
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js124
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js85
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js111
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js74
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js131
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js51
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js27
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js41
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js121
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js77
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js139
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js97
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js160
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js150
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js199
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js206
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js143
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js44
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js51
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js75
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js26
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js64
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js35
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js139
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js111
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js125
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js67
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js105
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js81
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js37
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js135
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js9
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml184
-rw-r--r--devtools/shared/images/command-pick-remote-touch.svg7
-rw-r--r--devtools/shared/images/command-pick.svg7
-rw-r--r--devtools/shared/images/error-small.svg6
-rw-r--r--devtools/shared/images/moz.build13
-rw-r--r--devtools/shared/images/resume.svg6
-rw-r--r--devtools/shared/images/stepOver.svg9
-rw-r--r--devtools/shared/indentation.js170
-rw-r--r--devtools/shared/indexed-db.js49
-rw-r--r--devtools/shared/inspector/css-logic.js844
-rw-r--r--devtools/shared/inspector/moz.build7
-rw-r--r--devtools/shared/inspector/utils.js22
-rw-r--r--devtools/shared/jar.mn15
-rw-r--r--devtools/shared/jsbeautify/UPGRADING.md36
-rw-r--r--devtools/shared/jsbeautify/beautify.js9
-rw-r--r--devtools/shared/jsbeautify/moz.build13
-rw-r--r--devtools/shared/jsbeautify/src/beautify-css.js1683
-rw-r--r--devtools/shared/jsbeautify/src/beautify-html.js3166
-rw-r--r--devtools/shared/jsbeautify/src/beautify-js.js4045
-rw-r--r--devtools/shared/jsbeautify/src/moz.build11
-rw-r--r--devtools/shared/l10n.js273
-rw-r--r--devtools/shared/layout/dom-matrix-2d.js297
-rw-r--r--devtools/shared/layout/moz.build7
-rw-r--r--devtools/shared/layout/utils.js927
-rw-r--r--devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs45
-rw-r--r--devtools/shared/loader/Loader.sys.mjs209
-rw-r--r--devtools/shared/loader/base-loader.sys.mjs638
-rw-r--r--devtools/shared/loader/browser-loader-mocks.js72
-rw-r--r--devtools/shared/loader/browser-loader.js239
-rw-r--r--devtools/shared/loader/builtin-modules.js203
-rw-r--r--devtools/shared/loader/loader-plugin-raw.sys.mjs39
-rw-r--r--devtools/shared/loader/moz.build21
-rw-r--r--devtools/shared/loader/worker-loader.js536
-rw-r--r--devtools/shared/locales/en-US/accessibility.properties142
-rw-r--r--devtools/shared/locales/en-US/debugger-paused-reasons.ftl85
-rw-r--r--devtools/shared/locales/en-US/debugger.properties59
-rw-r--r--devtools/shared/locales/en-US/eyedropper.properties14
-rw-r--r--devtools/shared/locales/en-US/highlighters.ftl66
-rw-r--r--devtools/shared/locales/en-US/screenshot.properties138
-rw-r--r--devtools/shared/locales/en-US/shared.properties6
-rw-r--r--devtools/shared/locales/en-US/styleinspector.properties267
-rw-r--r--devtools/shared/locales/en-US/webconsole-commands.ftl24
-rw-r--r--devtools/shared/locales/jar.mn11
-rw-r--r--devtools/shared/locales/l10n.toml12
-rw-r--r--devtools/shared/locales/moz.build7
-rw-r--r--devtools/shared/moz.build79
-rw-r--r--devtools/shared/natural-sort.js188
-rw-r--r--devtools/shared/network-observer/ChannelMap.sys.mjs129
-rw-r--r--devtools/shared/network-observer/NetworkAuthListener.sys.mjs185
-rw-r--r--devtools/shared/network-observer/NetworkHelper.sys.mjs913
-rw-r--r--devtools/shared/network-observer/NetworkObserver.sys.mjs1532
-rw-r--r--devtools/shared/network-observer/NetworkOverride.sys.mjs70
-rw-r--r--devtools/shared/network-observer/NetworkResponseListener.sys.mjs608
-rw-r--r--devtools/shared/network-observer/NetworkThrottleManager.sys.mjs495
-rw-r--r--devtools/shared/network-observer/NetworkUtils.sys.mjs693
-rw-r--r--devtools/shared/network-observer/README.md9
-rw-r--r--devtools/shared/network-observer/WildcardToRegexp.sys.mjs28
-rw-r--r--devtools/shared/network-observer/moz.build21
-rw-r--r--devtools/shared/network-observer/test/browser/browser.toml33
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver.js72
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js386
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js49
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_override.js179
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js113
-rw-r--r--devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html32
-rw-r--r--devtools/shared/network-observer/test/browser/doc_network-observer.html49
-rw-r--r--devtools/shared/network-observer/test/browser/gzipped.sjs44
-rw-r--r--devtools/shared/network-observer/test/browser/head.js123
-rw-r--r--devtools/shared/network-observer/test/browser/override.html1
-rw-r--r--devtools/shared/network-observer/test/browser/override.js2
-rw-r--r--devtools/shared/network-observer/test/browser/serviceworker.js23
-rw-r--r--devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs31
-rw-r--r--devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs196
-rw-r--r--devtools/shared/network-observer/test/xpcshell/head.js9
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_network_helper.js90
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js84
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js54
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js48
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-state.js122
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js41
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js39
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_throttle.js162
-rw-r--r--devtools/shared/network-observer/test/xpcshell/xpcshell.toml22
-rw-r--r--devtools/shared/node-properties/UPGRADING.md12
-rw-r--r--devtools/shared/node-properties/moz.build9
-rw-r--r--devtools/shared/node-properties/node-properties.js776
-rw-r--r--devtools/shared/path.js27
-rw-r--r--devtools/shared/performance-new/moz.build12
-rw-r--r--devtools/shared/performance-new/recording-utils.js39
-rw-r--r--devtools/shared/picker-constants.js14
-rw-r--r--devtools/shared/platform/CacheEntry.sys.mjs115
-rw-r--r--devtools/shared/platform/clipboard.js59
-rw-r--r--devtools/shared/platform/moz.build11
-rw-r--r--devtools/shared/platform/stack.js64
-rw-r--r--devtools/shared/plural-form.js203
-rw-r--r--devtools/shared/protocol.js36
-rw-r--r--devtools/shared/protocol/Actor.js260
-rw-r--r--devtools/shared/protocol/Actor/generateActorSpec.js62
-rw-r--r--devtools/shared/protocol/Actor/moz.build8
-rw-r--r--devtools/shared/protocol/Front.js410
-rw-r--r--devtools/shared/protocol/Front/FrontClassWithSpec.js118
-rw-r--r--devtools/shared/protocol/Front/moz.build8
-rw-r--r--devtools/shared/protocol/Pool.js220
-rw-r--r--devtools/shared/protocol/Request.js169
-rw-r--r--devtools/shared/protocol/Response.js119
-rw-r--r--devtools/shared/protocol/lazy-pool.js224
-rw-r--r--devtools/shared/protocol/moz.build22
-rw-r--r--devtools/shared/protocol/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/protocol/tests/xpcshell/head.js99
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js79
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_async.js192
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_children.js700
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_index.js52
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js52
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js27
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js310
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js316
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js98
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_types.js65
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js41
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js183
-rw-r--r--devtools/shared/protocol/tests/xpcshell/xpcshell.toml30
-rw-r--r--devtools/shared/protocol/types.js587
-rw-r--r--devtools/shared/protocol/utils.js44
-rw-r--r--devtools/shared/qrcode/decoder/LICENSE201
-rw-r--r--devtools/shared/qrcode/decoder/index.js2374
-rw-r--r--devtools/shared/qrcode/decoder/moz.build9
-rw-r--r--devtools/shared/qrcode/encoder/LICENSE19
-rw-r--r--devtools/shared/qrcode/encoder/index.js1674
-rw-r--r--devtools/shared/qrcode/encoder/moz.build9
-rw-r--r--devtools/shared/qrcode/index.js116
-rw-r--r--devtools/shared/qrcode/moz.build18
-rw-r--r--devtools/shared/qrcode/tests/chrome/chrome.toml5
-rw-r--r--devtools/shared/qrcode/tests/chrome/test_decode.html66
-rw-r--r--devtools/shared/qrcode/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/qrcode/tests/xpcshell/test_encode.js30
-rw-r--r--devtools/shared/qrcode/tests/xpcshell/xpcshell.toml6
-rw-r--r--devtools/shared/security/DevToolsSocketStatus.sys.mjs60
-rw-r--r--devtools/shared/security/auth.js220
-rw-r--r--devtools/shared/security/moz.build15
-rw-r--r--devtools/shared/security/prompt.js197
-rw-r--r--devtools/shared/security/socket.js685
-rw-r--r--devtools/shared/security/tests/chrome/chrome.toml4
-rw-r--r--devtools/shared/security/tests/chrome/test_websocket-transport.html72
-rw-r--r--devtools/shared/security/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/security/tests/xpcshell/head_dbg.js95
-rw-r--r--devtools/shared/security/tests/xpcshell/test_devtools_socket_status.js137
-rw-r--r--devtools/shared/security/tests/xpcshell/testactors.js16
-rw-r--r--devtools/shared/security/tests/xpcshell/xpcshell.toml8
-rw-r--r--devtools/shared/specs/accessibility.js299
-rw-r--r--devtools/shared/specs/addon/addons.js33
-rw-r--r--devtools/shared/specs/addon/moz.build10
-rw-r--r--devtools/shared/specs/addon/webextension-inspected-window.js119
-rw-r--r--devtools/shared/specs/animation.js110
-rw-r--r--devtools/shared/specs/array-buffer.js24
-rw-r--r--devtools/shared/specs/blackboxing.js42
-rw-r--r--devtools/shared/specs/breakpoint-list.js54
-rw-r--r--devtools/shared/specs/changes.js40
-rw-r--r--devtools/shared/specs/compatibility.js69
-rw-r--r--devtools/shared/specs/css-properties.js23
-rw-r--r--devtools/shared/specs/descriptors/moz.build12
-rw-r--r--devtools/shared/specs/descriptors/process.js41
-rw-r--r--devtools/shared/specs/descriptors/tab.js50
-rw-r--r--devtools/shared/specs/descriptors/webextension.js58
-rw-r--r--devtools/shared/specs/descriptors/worker.js32
-rw-r--r--devtools/shared/specs/device.js18
-rw-r--r--devtools/shared/specs/environment.js14
-rw-r--r--devtools/shared/specs/frame.js21
-rw-r--r--devtools/shared/specs/heap-snapshot-file.js23
-rw-r--r--devtools/shared/specs/highlighters.js44
-rw-r--r--devtools/shared/specs/index.js409
-rw-r--r--devtools/shared/specs/inspector.js79
-rw-r--r--devtools/shared/specs/layout.js75
-rw-r--r--devtools/shared/specs/manifest.js21
-rw-r--r--devtools/shared/specs/memory.js124
-rw-r--r--devtools/shared/specs/moz.build64
-rw-r--r--devtools/shared/specs/network-content.js32
-rw-r--r--devtools/shared/specs/network-event.js220
-rw-r--r--devtools/shared/specs/network-parent.js82
-rw-r--r--devtools/shared/specs/node.js160
-rw-r--r--devtools/shared/specs/object.js214
-rw-r--r--devtools/shared/specs/objects-manager.js25
-rw-r--r--devtools/shared/specs/page-style.js126
-rw-r--r--devtools/shared/specs/perf.js90
-rw-r--r--devtools/shared/specs/preference.js55
-rw-r--r--devtools/shared/specs/private-properties-iterator.js42
-rw-r--r--devtools/shared/specs/property-iterator.js45
-rw-r--r--devtools/shared/specs/reflow.js36
-rw-r--r--devtools/shared/specs/responsive.js40
-rw-r--r--devtools/shared/specs/root.js139
-rw-r--r--devtools/shared/specs/screenshot-content.js34
-rw-r--r--devtools/shared/specs/screenshot.js37
-rw-r--r--devtools/shared/specs/source.js87
-rw-r--r--devtools/shared/specs/storage.js308
-rw-r--r--devtools/shared/specs/string.js85
-rw-r--r--devtools/shared/specs/style-rule.js73
-rw-r--r--devtools/shared/specs/style-sheets.js49
-rw-r--r--devtools/shared/specs/style/moz.build9
-rw-r--r--devtools/shared/specs/style/style-types.js73
-rw-r--r--devtools/shared/specs/symbol-iterator.js42
-rw-r--r--devtools/shared/specs/symbol.js20
-rw-r--r--devtools/shared/specs/target-configuration.js51
-rw-r--r--devtools/shared/specs/targets/content-process.js55
-rw-r--r--devtools/shared/specs/targets/moz.build13
-rw-r--r--devtools/shared/specs/targets/parent-process.js24
-rw-r--r--devtools/shared/specs/targets/webextension.js18
-rw-r--r--devtools/shared/specs/targets/window-global.js156
-rw-r--r--devtools/shared/specs/targets/worker.js30
-rw-r--r--devtools/shared/specs/thread-configuration.js31
-rw-r--r--devtools/shared/specs/thread.js189
-rw-r--r--devtools/shared/specs/tracer.js35
-rw-r--r--devtools/shared/specs/walker.js393
-rw-r--r--devtools/shared/specs/watcher.js123
-rw-r--r--devtools/shared/specs/webconsole.js203
-rw-r--r--devtools/shared/specs/worker/moz.build11
-rw-r--r--devtools/shared/specs/worker/push-subscription.js12
-rw-r--r--devtools/shared/specs/worker/service-worker-registration.js48
-rw-r--r--devtools/shared/specs/worker/service-worker.js12
-rw-r--r--devtools/shared/sprintfjs/UPGRADING.md17
-rw-r--r--devtools/shared/sprintfjs/moz.build9
-rw-r--r--devtools/shared/sprintfjs/sprintf.js283
-rw-r--r--devtools/shared/storage/moz.build9
-rw-r--r--devtools/shared/storage/utils.js161
-rw-r--r--devtools/shared/storage/vendor/JSON5_LICENSE23
-rw-r--r--devtools/shared/storage/vendor/JSON5_UPGRADING.md36
-rw-r--r--devtools/shared/storage/vendor/json5.js1713
-rw-r--r--devtools/shared/storage/vendor/moz.build13
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/UPDATING.md142
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/moz.build15
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/head_stringvalidator.js15
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_sanitizers.js419
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_validators.js3762
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/xpcshell.toml9
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/util/assert.js215
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/util/moz.build9
-rw-r--r--devtools/shared/storage/vendor/stringvalidator/validator.js1489
-rw-r--r--devtools/shared/system.js198
-rw-r--r--devtools/shared/test-helpers/allocation-tracker.js637
-rw-r--r--devtools/shared/test-helpers/browser.toml11
-rw-r--r--devtools/shared/test-helpers/browser_allocation_tracker.js255
-rw-r--r--devtools/shared/test-helpers/moz.build13
-rw-r--r--devtools/shared/test-helpers/test_javascript_tracer.js72
-rw-r--r--devtools/shared/test-helpers/thread-helpers.sys.mjs143
-rw-r--r--devtools/shared/test-helpers/tracked-objects.sys.mjs47
-rw-r--r--devtools/shared/test-helpers/xpcshell.toml6
-rw-r--r--devtools/shared/tests/browser/browser.toml12
-rw-r--r--devtools/shared/tests/browser/browser_async_storage.js76
-rw-r--r--devtools/shared/tests/browser/browser_l10n_localizeMarkup.js93
-rw-r--r--devtools/shared/tests/chrome/chrome.toml10
-rw-r--r--devtools/shared/tests/chrome/test_css-logic-findCssSelector.html115
-rw-r--r--devtools/shared/tests/chrome/test_css-logic-getCssPath.html106
-rw-r--r--devtools/shared/tests/chrome/test_css-logic-getXPath.html95
-rw-r--r--devtools/shared/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/tests/xpcshell/exposeLoader.js10
-rw-r--r--devtools/shared/tests/xpcshell/head_devtools.js66
-rw-r--r--devtools/shared/tests/xpcshell/test_assert.js42
-rw-r--r--devtools/shared/tests/xpcshell/test_console_filtering.js156
-rw-r--r--devtools/shared/tests/xpcshell/test_csslexer.js203
-rw-r--r--devtools/shared/tests/xpcshell/test_debugger_client.js69
-rw-r--r--devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js68
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js181
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_basic.js345
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_destroy.js32
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_static.js378
-rw-r--r--devtools/shared/tests/xpcshell/test_executeSoon.js35
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-bom.js82
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-chrome.js36
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-file.js113
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-http.js69
-rw-r--r--devtools/shared/tests/xpcshell/test_fetch-resource.js43
-rw-r--r--devtools/shared/tests/xpcshell/test_flatten.js27
-rw-r--r--devtools/shared/tests/xpcshell/test_indentation.js150
-rw-r--r--devtools/shared/tests/xpcshell/test_independent_loaders.js22
-rw-r--r--devtools/shared/tests/xpcshell/test_invisible_loader.js80
-rw-r--r--devtools/shared/tests/xpcshell/test_isSet.js35
-rw-r--r--devtools/shared/tests/xpcshell/test_loader.js75
-rw-r--r--devtools/shared/tests/xpcshell/test_natural-sort.js959
-rw-r--r--devtools/shared/tests/xpcshell/test_pluralForm-english.js32
-rw-r--r--devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js38
-rw-r--r--devtools/shared/tests/xpcshell/test_prettifyCSS.js195
-rw-r--r--devtools/shared/tests/xpcshell/test_require.js100
-rw-r--r--devtools/shared/tests/xpcshell/test_require_lazy.js38
-rw-r--r--devtools/shared/tests/xpcshell/test_require_raw.js26
-rw-r--r--devtools/shared/tests/xpcshell/test_safeErrorString.js59
-rw-r--r--devtools/shared/tests/xpcshell/test_sprintfjs.js120
-rw-r--r--devtools/shared/tests/xpcshell/test_stack.js49
-rw-r--r--devtools/shared/tests/xpcshell/throwing-module-1.js7
-rw-r--r--devtools/shared/tests/xpcshell/throwing-module-2.js8
-rw-r--r--devtools/shared/tests/xpcshell/xpcshell.toml72
-rw-r--r--devtools/shared/throttle.js77
-rw-r--r--devtools/shared/transport/child-transport.js128
-rw-r--r--devtools/shared/transport/js-window-actor-transport.js66
-rw-r--r--devtools/shared/transport/local-transport.js204
-rw-r--r--devtools/shared/transport/moz.build18
-rw-r--r--devtools/shared/transport/packets.js440
-rw-r--r--devtools/shared/transport/stream-utils.js254
-rw-r--r--devtools/shared/transport/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/transport/tests/xpcshell/head_dbg.js179
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_bulk_error.js94
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_client_server_bulk.js312
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_dbgsocket.js163
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_dbgsocket_connection_drop.js86
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_delimited_read.js30
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_packet.js24
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_queue.js198
-rw-r--r--devtools/shared/transport/tests/xpcshell/test_transport_bulk.js169
-rw-r--r--devtools/shared/transport/tests/xpcshell/testactors.js16
-rw-r--r--devtools/shared/transport/tests/xpcshell/xpcshell.toml22
-rw-r--r--devtools/shared/transport/transport.js499
-rw-r--r--devtools/shared/transport/websocket-transport.js86
-rw-r--r--devtools/shared/transport/worker-transport.js113
-rw-r--r--devtools/shared/validate-breakpoint.jsm49
-rw-r--r--devtools/shared/wasm-source-map.js114
-rw-r--r--devtools/shared/webconsole/GenerateDataFromWebIdls.py176
-rw-r--r--devtools/shared/webconsole/GenerateReservedWordsJS.py40
-rw-r--r--devtools/shared/webconsole/analyze-input-string.js406
-rw-r--r--devtools/shared/webconsole/js-property-provider.js803
-rw-r--r--devtools/shared/webconsole/messages.js55
-rw-r--r--devtools/shared/webconsole/moz.build33
-rw-r--r--devtools/shared/webconsole/parser-helper.js66
-rw-r--r--devtools/shared/webconsole/test/browser/browser.toml15
-rw-r--r--devtools/shared/webconsole/test/browser/browser_commands_registration.js86
-rw-r--r--devtools/shared/webconsole/test/browser/browser_network_longstring.js203
-rw-r--r--devtools/shared/webconsole/test/browser/data.json3
-rw-r--r--devtools/shared/webconsole/test/browser/data.json^headers^3
-rw-r--r--devtools/shared/webconsole/test/browser/head.js61
-rw-r--r--devtools/shared/webconsole/test/browser/network_requests_iframe.html66
-rw-r--r--devtools/shared/webconsole/test/chrome/chrome.toml57
-rw-r--r--devtools/shared/webconsole/test/chrome/common.js274
-rw-r--r--devtools/shared/webconsole/test/chrome/console-test-worker.js21
-rw-r--r--devtools/shared/webconsole/test/chrome/data.json5
-rw-r--r--devtools/shared/webconsole/test/chrome/data.json^headers^3
-rw-r--r--devtools/shared/webconsole/test/chrome/helper_serviceworker.js22
-rw-r--r--devtools/shared/webconsole/test/chrome/network_requests_iframe.html66
-rw-r--r--devtools/shared/webconsole/test/chrome/sandboxed_iframe.html8
-rw-r--r--devtools/shared/webconsole/test/chrome/test_basics.html61
-rw-r--r--devtools/shared/webconsole/test/chrome/test_cached_messages.html217
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_assert.html106
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_group_styling.html121
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_serviceworker.html212
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_serviceworker_cached.html129
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_styling.html134
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_timestamp.html48
-rw-r--r--devtools/shared/webconsole/test/chrome/test_console_worker.html73
-rw-r--r--devtools/shared/webconsole/test/chrome/test_consoleapi.html225
-rw-r--r--devtools/shared/webconsole/test/chrome/test_consoleapi_innerID.html159
-rw-r--r--devtools/shared/webconsole/test/chrome/test_file_uri.html114
-rw-r--r--devtools/shared/webconsole/test/chrome/test_jsterm_autocomplete.html635
-rw-r--r--devtools/shared/webconsole/test/chrome/test_network_get.html132
-rw-r--r--devtools/shared/webconsole/test/chrome/test_network_post.html142
-rw-r--r--devtools/shared/webconsole/test/chrome/test_network_security-hsts.html87
-rw-r--r--devtools/shared/webconsole/test/chrome/test_nsiconsolemessage.html74
-rw-r--r--devtools/shared/webconsole/test/chrome/test_object_actor.html158
-rw-r--r--devtools/shared/webconsole/test/chrome/test_object_actor_native_getters.html75
-rw-r--r--devtools/shared/webconsole/test/chrome/test_object_actor_native_getters_lenient_this.html54
-rw-r--r--devtools/shared/webconsole/test/chrome/test_page_errors.html224
-rw-r--r--devtools/shared/webconsole/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/webconsole/test/xpcshell/head.js10
-rw-r--r--devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js225
-rw-r--r--devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js746
-rw-r--r--devtools/shared/webconsole/test/xpcshell/xpcshell.toml10
-rw-r--r--devtools/shared/webextension-fallback.html6
-rw-r--r--devtools/shared/worker/helper.js138
-rw-r--r--devtools/shared/worker/moz.build13
-rw-r--r--devtools/shared/worker/tests/browser/browser.toml10
-rw-r--r--devtools/shared/worker/tests/browser/browser_worker-01.js110
-rw-r--r--devtools/shared/worker/tests/browser/browser_worker-02.js83
-rw-r--r--devtools/shared/worker/tests/browser/browser_worker-03.js63
-rw-r--r--devtools/shared/worker/worker.js198
731 files changed, 115049 insertions, 0 deletions
diff --git a/devtools/shared/.eslintrc.js b/devtools/shared/.eslintrc.js
new file mode 100644
index 0000000000..1ec0251734
--- /dev/null
+++ b/devtools/shared/.eslintrc.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = {
+ rules: {
+ // See bug 1288147, the devtools front-end wants to be able to run in
+ // content privileged windows, where ownerGlobal doesn't exist.
+ "mozilla/use-ownerGlobal": "off",
+ },
+};
diff --git a/devtools/shared/DevToolsInfaillibleUtils.sys.mjs b/devtools/shared/DevToolsInfaillibleUtils.sys.mjs
new file mode 100644
index 0000000000..ddb2b170ee
--- /dev/null
+++ b/devtools/shared/DevToolsInfaillibleUtils.sys.mjs
@@ -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/. */
+
+/**
+ * The 3 methods here are duplicated from ThreadSafeDevToolsUtils.js
+ * The ones defined here are used from other sys.mjs files, mostly from the
+ * NetworkObserver codebase, while the ones remaining in ThreadSafeDevToolsUtils.js
+ * are used in commonjs modules, including modules which can be loaded in workers.
+ *
+ * sys.mjs modules are currently not supported in workers, see Bug 1247687.
+ */
+
+/**
+ * Report that |who| threw an exception, |exception|.
+ */
+function reportException(who, exception) {
+ const msg = `${who} threw an exception: ${safeErrorString(exception)}`;
+ dump(msg + "\n");
+
+ if (typeof console !== "undefined" && console && console.error) {
+ console.error(exception);
+ }
+}
+
+/**
+ * Given a handler function that may throw, return an infallible handler
+ * function that calls the fallible handler, and logs any exceptions it
+ * throws.
+ *
+ * @param handler function
+ * A handler function, which may throw.
+ * @param aName string
+ * A name for handler, for use in error messages. If omitted, we use
+ * handler.name.
+ *
+ * (SpiderMonkey does generate good names for anonymous functions, but we
+ * don't have a way to get at them from JavaScript at the moment.)
+ */
+function makeInfallible(handler, name = handler.name) {
+ return function () {
+ try {
+ return handler.apply(this, arguments);
+ } catch (ex) {
+ let who = "Handler function";
+ if (name) {
+ who += " " + name;
+ }
+ reportException(who, ex);
+ return undefined;
+ }
+ };
+}
+
+/**
+ * Turn the |error| into a string, without fail.
+ *
+ * @param {Error|any} error
+ */
+function safeErrorString(error) {
+ try {
+ let errorString = error.toString();
+ if (typeof errorString == "string") {
+ // Attempt to attach a stack to |errorString|. If it throws an error, or
+ // isn't a string, don't use it.
+ try {
+ if (error.stack) {
+ const stack = error.stack.toString();
+ if (typeof stack == "string") {
+ errorString += "\nStack: " + stack;
+ }
+ }
+ } catch (ee) {
+ // Ignore.
+ }
+
+ // Append additional line and column number information to the output,
+ // since it might not be part of the stringified error.
+ if (
+ typeof error.lineNumber == "number" &&
+ typeof error.columnNumber == "number"
+ ) {
+ errorString +=
+ "Line: " + error.lineNumber + ", column: " + error.columnNumber;
+ }
+
+ return errorString;
+ }
+ } catch (ee) {
+ // Ignore.
+ }
+
+ // We failed to find a good error description, so do the next best thing.
+ return Object.prototype.toString.call(error);
+}
+
+export const DevToolsInfaillibleUtils = {
+ makeInfallible,
+ reportException,
+ safeErrorString,
+};
diff --git a/devtools/shared/DevToolsUtils.js b/devtools/shared/DevToolsUtils.js
new file mode 100644
index 0000000000..77a6ec447c
--- /dev/null
+++ b/devtools/shared/DevToolsUtils.js
@@ -0,0 +1,1027 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals setImmediate, rpc */
+
+"use strict";
+
+/* General utilities used throughout devtools. */
+
+var flags = require("resource://devtools/shared/flags.js");
+var {
+ getStack,
+ callFunctionWithAsyncStack,
+} = require("resource://devtools/shared/platform/stack.js");
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+ ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
+});
+
+// Native getters which are considered to be side effect free.
+ChromeUtils.defineLazyGetter(lazy, "sideEffectFreeGetters", () => {
+ const {
+ getters,
+ } = require("resource://devtools/server/actors/webconsole/eager-ecma-allowlist.js");
+
+ const map = new Map();
+ for (const n of getters) {
+ if (!map.has(n.name)) {
+ map.set(n.name, []);
+ }
+ map.get(n.name).push(n);
+ }
+
+ return map;
+});
+
+// Using this name lets the eslint plugin know about lazy defines in
+// this file.
+var DevToolsUtils = exports;
+
+// Re-export the thread-safe utils.
+const ThreadSafeDevToolsUtils = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
+for (const key of Object.keys(ThreadSafeDevToolsUtils)) {
+ exports[key] = ThreadSafeDevToolsUtils[key];
+}
+
+/**
+ * Waits for the next tick in the event loop to execute a callback.
+ */
+exports.executeSoon = function (fn) {
+ if (isWorker) {
+ setImmediate(fn);
+ } else {
+ let executor;
+ // Only enable async stack reporting when DEBUG_JS_MODULES is set
+ // (customized local builds) to avoid a performance penalty.
+ if (AppConstants.DEBUG_JS_MODULES || flags.testing) {
+ const stack = getStack();
+ executor = () => {
+ callFunctionWithAsyncStack(fn, stack, "DevToolsUtils.executeSoon");
+ };
+ } else {
+ executor = fn;
+ }
+ Services.tm.dispatchToMainThread({
+ run: exports.makeInfallible(executor),
+ });
+ }
+};
+
+/**
+ * Similar to executeSoon, but enters microtask before executing the callback
+ * if this is called on the main thread.
+ */
+exports.executeSoonWithMicroTask = function (fn) {
+ if (isWorker) {
+ setImmediate(fn);
+ } else {
+ let executor;
+ // Only enable async stack reporting when DEBUG_JS_MODULES is set
+ // (customized local builds) to avoid a performance penalty.
+ if (AppConstants.DEBUG_JS_MODULES || flags.testing) {
+ const stack = getStack();
+ executor = () => {
+ callFunctionWithAsyncStack(
+ fn,
+ stack,
+ "DevToolsUtils.executeSoonWithMicroTask"
+ );
+ };
+ } else {
+ executor = fn;
+ }
+ Services.tm.dispatchToMainThreadWithMicroTask({
+ run: exports.makeInfallible(executor),
+ });
+ }
+};
+
+/**
+ * Waits for the next tick in the event loop.
+ *
+ * @return Promise
+ * A promise that is resolved after the next tick in the event loop.
+ */
+exports.waitForTick = function () {
+ return new Promise(resolve => {
+ exports.executeSoon(resolve);
+ });
+};
+
+/**
+ * Waits for the specified amount of time to pass.
+ *
+ * @param number delay
+ * The amount of time to wait, in milliseconds.
+ * @return Promise
+ * A promise that is resolved after the specified amount of time passes.
+ */
+exports.waitForTime = function (delay) {
+ return new Promise(resolve => setTimeout(resolve, delay));
+};
+
+/**
+ * Like ChromeUtils.defineLazyGetter, but with a |this| sensitive getter that
+ * allows the lazy getter to be defined on a prototype and work correctly with
+ * instances.
+ *
+ * @param Object object
+ * The prototype object to define the lazy getter on.
+ * @param String key
+ * The key to define the lazy getter on.
+ * @param Function callback
+ * The callback that will be called to determine the value. Will be
+ * called with the |this| value of the current instance.
+ */
+exports.defineLazyPrototypeGetter = function (object, key, callback) {
+ Object.defineProperty(object, key, {
+ configurable: true,
+ get() {
+ const value = callback.call(this);
+
+ Object.defineProperty(this, key, {
+ configurable: true,
+ writable: true,
+ value,
+ });
+
+ return value;
+ },
+ });
+};
+
+/**
+ * Safely get the property value from a Debugger.Object for a given key. Walks
+ * the prototype chain until the property is found.
+ *
+ * @param {Debugger.Object} object
+ * The Debugger.Object to get the value from.
+ * @param {String} key
+ * The key to look for.
+ * @param {Boolean} invokeUnsafeGetter (defaults to false).
+ * Optional boolean to indicate if the function should execute unsafe getter
+ * in order to retrieve its result's properties.
+ * ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects
+ * in the content page ⚠️
+ * @return Any
+ */
+exports.getProperty = function (object, key, invokeUnsafeGetters = false) {
+ const root = object;
+ while (object && exports.isSafeDebuggerObject(object)) {
+ let desc;
+ try {
+ desc = object.getOwnPropertyDescriptor(key);
+ } catch (e) {
+ // The above can throw when the debuggee does not subsume the object's
+ // compartment, or for some WrappedNatives like Cu.Sandbox.
+ return undefined;
+ }
+ if (desc) {
+ if ("value" in desc) {
+ return desc.value;
+ }
+ // Call the getter if it's safe.
+ if (exports.hasSafeGetter(desc) || invokeUnsafeGetters === true) {
+ try {
+ return desc.get.call(root).return;
+ } catch (e) {
+ // If anything goes wrong report the error and return undefined.
+ exports.reportException("getProperty", e);
+ }
+ }
+ return undefined;
+ }
+ object = object.proto;
+ }
+ return undefined;
+};
+
+/**
+ * Removes all the non-opaque security wrappers of a debuggee object.
+ *
+ * @param obj Debugger.Object
+ * The debuggee object to be unwrapped.
+ * @return Debugger.Object|null|undefined
+ * - If the object has no wrapper, the same `obj` is returned. Note DeadObject
+ * objects belong to this case.
+ * - Otherwise, if the debuggee doesn't subsume object's compartment, returns `null`.
+ * - Otherwise, if the object belongs to an invisible-to-debugger compartment,
+ * returns `undefined`.
+ * - Otherwise, returns the unwrapped object.
+ */
+exports.unwrap = function unwrap(obj) {
+ // Check if `obj` has an opaque wrapper.
+ if (obj.class === "Opaque") {
+ return obj;
+ }
+
+ // Attempt to unwrap via `obj.unwrap()`. Note that:
+ // - This will return `null` if the debuggee does not subsume object's compartment.
+ // - This will throw if the object belongs to an invisible-to-debugger compartment.
+ // - This will return `obj` if there is no wrapper.
+ let unwrapped;
+ try {
+ unwrapped = obj.unwrap();
+ } catch (err) {
+ return undefined;
+ }
+
+ // Check if further unwrapping is not possible.
+ if (!unwrapped || unwrapped === obj) {
+ return unwrapped;
+ }
+
+ // Recursively remove additional security wrappers.
+ return unwrap(unwrapped);
+};
+
+/**
+ * Checks whether a debuggee object is safe. Unsafe objects may run proxy traps or throw
+ * when using `proto`, `isExtensible`, `isFrozen` or `isSealed`. Note that safe objects
+ * may still throw when calling `getOwnPropertyNames`, `getOwnPropertyDescriptor`, etc.
+ * Also note DeadObject objects are considered safe.
+ *
+ * @param obj Debugger.Object
+ * The debuggee object to be checked.
+ * @return boolean
+ */
+exports.isSafeDebuggerObject = function (obj) {
+ const unwrapped = exports.unwrap(obj);
+
+ // Objects belonging to an invisible-to-debugger compartment might be proxies,
+ // so just in case consider them unsafe.
+ if (unwrapped === undefined) {
+ return false;
+ }
+
+ // If the debuggee does not subsume the object's compartment, most properties won't
+ // be accessible. Cross-origin Window and Location objects might expose some, though.
+ // Therefore, it must be considered safe. Note that proxy objects have fully opaque
+ // security wrappers, so proxy traps won't run in this case.
+ if (unwrapped === null) {
+ return true;
+ }
+
+ // Proxy objects can run traps when accessed. `isProxy` getter is called on `unwrapped`
+ // instead of on `obj` in order to detect proxies behind transparent wrappers.
+ if (unwrapped.isProxy) {
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Determines if a descriptor has a getter which doesn't call into JavaScript.
+ *
+ * @param Object desc
+ * The descriptor to check for a safe getter.
+ * @return Boolean
+ * Whether a safe getter was found.
+ */
+exports.hasSafeGetter = function (desc) {
+ // Scripted functions that are CCWs will not appear scripted until after
+ // unwrapping.
+ let fn = desc.get;
+ fn = fn && exports.unwrap(fn);
+ if (!fn) {
+ return false;
+ }
+ if (!fn.callable || fn.class !== "Function") {
+ return false;
+ }
+ if (fn.script !== undefined) {
+ // This is scripted function.
+ return false;
+ }
+
+ // This is a getter with native function.
+
+ // We assume all DOM getters have no major side effect, and they are
+ // eagerly-evaluateable.
+ //
+ // JitInfo is used only by methods/accessors in WebIDL, and being
+ // "a getter with JitInfo" can be used as a condition to check if given
+ // function is DOM getter.
+ //
+ // This includes privileged interfaces in addition to standard web APIs.
+ if (fn.isNativeGetterWithJitInfo()) {
+ return true;
+ }
+
+ // Apply explicit allowlist.
+ const natives = lazy.sideEffectFreeGetters.get(fn.name);
+ return natives && natives.some(n => fn.isSameNative(n));
+};
+
+/**
+ * Check that the property value from a Debugger.Object for a given key is an unsafe
+ * getter or not. Walks the prototype chain until the property is found.
+ *
+ * @param {Debugger.Object} object
+ * The Debugger.Object to check on.
+ * @param {String} key
+ * The key to look for.
+ * @param {Boolean} invokeUnsafeGetter (defaults to false).
+ * Optional boolean to indicate if the function should execute unsafe getter
+ * in order to retrieve its result's properties.
+ * @return Boolean
+ */
+exports.isUnsafeGetter = function (object, key) {
+ while (object && exports.isSafeDebuggerObject(object)) {
+ let desc;
+ try {
+ desc = object.getOwnPropertyDescriptor(key);
+ } catch (e) {
+ // The above can throw when the debuggee does not subsume the object's
+ // compartment, or for some WrappedNatives like Cu.Sandbox.
+ return false;
+ }
+ if (desc) {
+ if (Object.getOwnPropertyNames(desc).includes("get")) {
+ return !exports.hasSafeGetter(desc);
+ }
+ }
+ object = object.proto;
+ }
+
+ return false;
+};
+
+/**
+ * Check if it is safe to read properties and execute methods from the given JS
+ * object. Safety is defined as being protected from unintended code execution
+ * from content scripts (or cross-compartment code).
+ *
+ * See bugs 945920 and 946752 for discussion.
+ *
+ * @type Object obj
+ * The object to check.
+ * @return Boolean
+ * True if it is safe to read properties from obj, or false otherwise.
+ */
+exports.isSafeJSObject = function (obj) {
+ // If we are running on a worker thread, Cu is not available. In this case,
+ // we always return false, just to be on the safe side.
+ if (isWorker) {
+ return false;
+ }
+
+ if (
+ Cu.getGlobalForObject(obj) == Cu.getGlobalForObject(exports.isSafeJSObject)
+ ) {
+ // obj is not a cross-compartment wrapper.
+ return true;
+ }
+
+ // Xray wrappers protect against unintended code execution.
+ if (Cu.isXrayWrapper(obj)) {
+ return true;
+ }
+
+ // If there aren't Xrays, only allow chrome objects.
+ const principal = Cu.getObjectPrincipal(obj);
+ if (!principal.isSystemPrincipal) {
+ return false;
+ }
+
+ // Scripted proxy objects without Xrays can run their proxy traps.
+ if (Cu.isProxy(obj)) {
+ return false;
+ }
+
+ // Even if `obj` looks safe, an unsafe object in its prototype chain may still
+ // run unintended code, e.g. when using the `instanceof` operator.
+ const proto = Object.getPrototypeOf(obj);
+ if (proto && !exports.isSafeJSObject(proto)) {
+ return false;
+ }
+
+ // Allow non-problematic chrome objects.
+ return true;
+};
+
+/**
+ * Dump with newline - This is a logging function that will only output when
+ * the preference "devtools.debugger.log" is set to true. Typically it is used
+ * for logging the remote debugging protocol calls.
+ */
+exports.dumpn = function (str) {
+ if (flags.wantLogging) {
+ dump("DBG-SERVER: " + str + "\n");
+ }
+};
+
+/**
+ * Dump verbose - This is a verbose logger for low-level tracing, that is typically
+ * used to provide information about the remote debugging protocol's transport
+ * mechanisms. The logging can be enabled by changing the preferences
+ * "devtools.debugger.log" and "devtools.debugger.log.verbose" to true.
+ */
+exports.dumpv = function (msg) {
+ if (flags.wantVerbose) {
+ exports.dumpn(msg);
+ }
+};
+
+/**
+ * Defines a getter on a specified object that will be created upon first use.
+ *
+ * @param object
+ * The object to define the lazy getter on.
+ * @param name
+ * The name of the getter to define on object.
+ * @param lambda
+ * A function that returns what the getter should return. This will
+ * only ever be called once.
+ */
+exports.defineLazyGetter = function (object, name, lambda) {
+ Object.defineProperty(object, name, {
+ get() {
+ delete object[name];
+ object[name] = lambda.apply(object);
+ return object[name];
+ },
+ configurable: true,
+ enumerable: true,
+ });
+};
+
+DevToolsUtils.defineLazyGetter(this, "AppConstants", () => {
+ if (isWorker) {
+ return {};
+ }
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ ).AppConstants;
+});
+
+/**
+ * No operation. The empty function.
+ */
+exports.noop = function () {};
+
+let assertionFailureCount = 0;
+
+Object.defineProperty(exports, "assertionFailureCount", {
+ get() {
+ return assertionFailureCount;
+ },
+});
+
+function reallyAssert(condition, message) {
+ if (!condition) {
+ assertionFailureCount++;
+ const err = new Error("Assertion failure: " + message);
+ exports.reportException("DevToolsUtils.assert", err);
+ throw err;
+ }
+}
+
+/**
+ * DevToolsUtils.assert(condition, message)
+ *
+ * @param Boolean condition
+ * @param String message
+ *
+ * Assertions are enabled when any of the following are true:
+ * - This is a DEBUG_JS_MODULES build
+ * - flags.testing is set to true
+ *
+ * If assertions are enabled, then `condition` is checked and if false-y, the
+ * assertion failure is logged and then an error is thrown.
+ *
+ * If assertions are not enabled, then this function is a no-op.
+ */
+Object.defineProperty(exports, "assert", {
+ get: () =>
+ AppConstants.DEBUG_JS_MODULES || flags.testing
+ ? reallyAssert
+ : exports.noop,
+});
+
+DevToolsUtils.defineLazyGetter(this, "NetUtil", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs")
+ .NetUtil;
+});
+
+/**
+ * Performs a request to load the desired URL and returns a promise.
+ *
+ * @param urlIn String
+ * The URL we will request.
+ * @param aOptions Object
+ * An object with the following optional properties:
+ * - loadFromCache: if false, will bypass the cache and
+ * always load fresh from the network (default: true)
+ * - policy: the nsIContentPolicy type to apply when fetching the URL
+ * (only works when loading from system principal)
+ * - window: the window to get the loadGroup from
+ * - charset: the charset to use if the channel doesn't provide one
+ * - principal: the principal to use, if omitted, the request is loaded
+ * with a content principal corresponding to the url being
+ * loaded, using the origin attributes of the window, if any.
+ * - headers: extra headers
+ * - cacheKey: when loading from cache, use this key to retrieve a cache
+ * specific to a given SHEntry. (Allows loading POST
+ * requests from cache)
+ * @returns Promise that resolves with an object with the following members on
+ * success:
+ * - content: the document at that URL, as a string,
+ * - contentType: the content type of the document
+ *
+ * If an error occurs, the promise is rejected with that error.
+ *
+ * XXX: It may be better to use nsITraceableChannel to get to the sources
+ * without relying on caching when we can (not for eval, etc.):
+ * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
+ */
+function mainThreadFetch(
+ urlIn,
+ aOptions = {
+ loadFromCache: true,
+ policy: Ci.nsIContentPolicy.TYPE_OTHER,
+ window: null,
+ charset: null,
+ principal: null,
+ headers: null,
+ cacheKey: 0,
+ }
+) {
+ return new Promise((resolve, reject) => {
+ // Create a channel.
+ const url = urlIn.split(" -> ").pop();
+ let channel;
+ try {
+ channel = newChannelForURL(url, aOptions);
+ } catch (ex) {
+ reject(ex);
+ return;
+ }
+
+ channel.loadInfo.isInDevToolsContext = true;
+
+ // Set the channel options.
+ channel.loadFlags = aOptions.loadFromCache
+ ? channel.LOAD_FROM_CACHE
+ : channel.LOAD_BYPASS_CACHE;
+
+ if (aOptions.loadFromCache && channel instanceof Ci.nsICacheInfoChannel) {
+ // If DevTools intents to load the content from the cache,
+ // we make the LOAD_FROM_CACHE flag preferred over LOAD_BYPASS_CACHE.
+ channel.preferCacheLoadOverBypass = true;
+
+ // When loading from cache, the cacheKey allows us to target a specific
+ // SHEntry and offer ways to restore POST requests from cache.
+ if (aOptions.cacheKey != 0) {
+ channel.cacheKey = aOptions.cacheKey;
+ }
+ }
+
+ if (aOptions.headers && channel instanceof Ci.nsIHttpChannel) {
+ for (const h in aOptions.headers) {
+ channel.setRequestHeader(h, aOptions.headers[h], /* aMerge = */ false);
+ }
+ }
+
+ if (aOptions.window) {
+ // Respect private browsing.
+ channel.loadGroup = aOptions.window.docShell.QueryInterface(
+ Ci.nsIDocumentLoader
+ ).loadGroup;
+ }
+
+ // eslint-disable-next-line complexity
+ const onResponse = (stream, status, request) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error(`Failed to fetch ${url}. Code ${status}.`));
+ return;
+ }
+
+ try {
+ // We cannot use NetUtil to do the charset conversion as if charset
+ // information is not available and our default guess is wrong the method
+ // might fail and we lose the stream data. This means we can't fall back
+ // to using the locale default encoding (bug 1181345).
+
+ // Read and decode the data according to the locale default encoding.
+
+ let available;
+ try {
+ available = stream.available();
+ } catch (ex) {
+ if (ex.name === "NS_BASE_STREAM_CLOSED") {
+ // Empty files cause NS_BASE_STREAM_CLOSED exception.
+ // If there was a real stream error, we would have already rejected above.
+ resolve({
+ content: "",
+ contentType: "text/plain",
+ });
+ return;
+ }
+
+ reject(ex);
+ }
+ let source = NetUtil.readInputStreamToString(stream, available);
+ stream.close();
+
+ // We do our own BOM sniffing here because there's no convenient
+ // implementation of the "decode" algorithm
+ // (https://encoding.spec.whatwg.org/#decode) exposed to JS.
+ let bomCharset = null;
+ if (
+ available >= 3 &&
+ source.codePointAt(0) == 0xef &&
+ source.codePointAt(1) == 0xbb &&
+ source.codePointAt(2) == 0xbf
+ ) {
+ bomCharset = "UTF-8";
+ source = source.slice(3);
+ } else if (
+ available >= 2 &&
+ source.codePointAt(0) == 0xfe &&
+ source.codePointAt(1) == 0xff
+ ) {
+ bomCharset = "UTF-16BE";
+ source = source.slice(2);
+ } else if (
+ available >= 2 &&
+ source.codePointAt(0) == 0xff &&
+ source.codePointAt(1) == 0xfe
+ ) {
+ bomCharset = "UTF-16LE";
+ source = source.slice(2);
+ }
+
+ // If the channel or the caller has correct charset information, the
+ // content will be decoded correctly. If we have to fall back to UTF-8 and
+ // the guess is wrong, the conversion fails and convertToUnicode returns
+ // the input unmodified. Essentially we try to decode the data as UTF-8
+ // and if that fails, we use the locale specific default encoding. This is
+ // the best we can do if the source does not provide charset info.
+ let charset = bomCharset;
+ if (!charset) {
+ try {
+ charset = channel.contentCharset;
+ } catch (e) {
+ // Accessing `contentCharset` on content served by a service worker in
+ // non-e10s may throw.
+ }
+ }
+ if (!charset) {
+ charset = aOptions.charset || "UTF-8";
+ }
+ const unicodeSource = lazy.NetworkHelper.convertToUnicode(
+ source,
+ charset
+ );
+
+ // Look for any source map URL in the response.
+ let sourceMapURL;
+ if (request instanceof Ci.nsIHttpChannel) {
+ try {
+ sourceMapURL = request.getResponseHeader("SourceMap");
+ } catch (e) {}
+ if (!sourceMapURL) {
+ try {
+ sourceMapURL = request.getResponseHeader("X-SourceMap");
+ } catch (e) {}
+ }
+ }
+
+ resolve({
+ content: unicodeSource,
+ contentType: request.contentType,
+ sourceMapURL,
+ });
+ } catch (ex) {
+ reject(ex);
+ }
+ };
+
+ // Open the channel
+ try {
+ NetUtil.asyncFetch(channel, onResponse);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+}
+
+/**
+ * Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel.
+ *
+ * @param {String} url - The URL to open a channel for.
+ * @param {Object} options - The options object passed to @method fetch.
+ * @return {nsIChannel} - The newly created channel. Throws on failure.
+ */
+function newChannelForURL(
+ url,
+ { policy, window, principal },
+ recursing = false
+) {
+ const securityFlags =
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+
+ let uri;
+ try {
+ uri = Services.io.newURI(url);
+ } catch (e) {
+ // In the xpcshell tests, the script url is the absolute path of the test
+ // file, which will make a malformed URI error be thrown. Add the file
+ // scheme to see if it helps.
+ uri = Services.io.newURI("file://" + url);
+ }
+ const channelOptions = {
+ contentPolicyType: policy,
+ securityFlags,
+ uri,
+ };
+
+ // Ensure that we have some contentPolicyType type set if one was
+ // not provided.
+ if (!channelOptions.contentPolicyType) {
+ channelOptions.contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER;
+ }
+
+ // If a window is provided, always use it's document as the loadingNode.
+ // This will provide the correct principal, origin attributes, service
+ // worker controller, etc.
+ if (window) {
+ channelOptions.loadingNode = window.document;
+ } else {
+ // If a window is not provided, then we must set a loading principal.
+
+ // If the caller did not provide a principal, then we use the URI
+ // to create one. Note, it's not clear what use cases require this
+ // and it may not be correct.
+ let prin = principal;
+ if (!prin) {
+ prin = Services.scriptSecurityManager.createContentPrincipal(uri, {});
+ }
+
+ channelOptions.loadingPrincipal = prin;
+ }
+
+ try {
+ return NetUtil.newChannel(channelOptions);
+ } catch (e) {
+ // Don't infinitely recurse if newChannel keeps throwing.
+ if (recursing) {
+ throw e;
+ }
+
+ // In xpcshell tests on Windows, nsExternalProtocolHandler::NewChannel()
+ // can throw NS_ERROR_UNKNOWN_PROTOCOL if the external protocol isn't
+ // supported by Windows, so we also need to handle the exception here if
+ // parsing the URL above doesn't throw.
+ return newChannelForURL(
+ "file://" + url,
+ { policy, window, principal },
+ /* recursing */ true
+ );
+ }
+}
+
+// Fetch is defined differently depending on whether we are on the main thread
+// or a worker thread.
+if (this.isWorker) {
+ // Services is not available in worker threads, nor is there any other way
+ // to fetch a URL. We need to enlist the help from the main thread here, by
+ // issuing an rpc request, to fetch the URL on our behalf.
+ exports.fetch = function (url, options) {
+ return rpc("fetch", url, options);
+ };
+} else {
+ exports.fetch = mainThreadFetch;
+}
+
+/**
+ * Open the file at the given path for reading.
+ *
+ * @param {String} filePath
+ *
+ * @returns Promise<nsIInputStream>
+ */
+exports.openFileStream = function (filePath) {
+ return new Promise((resolve, reject) => {
+ const uri = NetUtil.newURI(new lazy.FileUtils.File(filePath));
+ NetUtil.asyncFetch(
+ { uri, loadUsingSystemPrincipal: true },
+ (stream, result) => {
+ if (!Components.isSuccessCode(result)) {
+ reject(new Error(`Could not open "${filePath}": result = ${result}`));
+ return;
+ }
+
+ resolve(stream);
+ }
+ );
+ });
+};
+
+/**
+ * Save the given data to disk after asking the user where to do so.
+ *
+ * @param {Window} parentWindow
+ * The parent window to use to display the filepicker.
+ * @param {UInt8Array} dataArray
+ * The data to write to the file.
+ * @param {String} fileName
+ * The suggested filename.
+ * @param {Array} filters
+ * An array of object of the following shape:
+ * - pattern: A pattern for accepted files (example: "*.js")
+ * - label: The label that will be displayed in the save file dialog.
+ * @return {String|null}
+ * The path to the local saved file, if saved.
+ */
+exports.saveAs = async function (
+ parentWindow,
+ dataArray,
+ fileName = "",
+ filters = []
+) {
+ let returnFile;
+ try {
+ returnFile = await exports.showSaveFileDialog(
+ parentWindow,
+ fileName,
+ filters
+ );
+ } catch (ex) {
+ return null;
+ }
+
+ await IOUtils.write(returnFile.path, dataArray, {
+ tmpPath: returnFile.path + ".tmp",
+ });
+
+ return returnFile.path;
+};
+
+/**
+ * Show file picker and return the file user selected.
+ *
+ * @param {nsIWindow} parentWindow
+ * Optional parent window. If null the parent window of the file picker
+ * will be the window of the attached input element.
+ * @param {String} suggestedFilename
+ * The suggested filename.
+ * @param {Array} filters
+ * An array of object of the following shape:
+ * - pattern: A pattern for accepted files (example: "*.js")
+ * - label: The label that will be displayed in the save file dialog.
+ * @return {Promise}
+ * A promise that is resolved after the file is selected by the file picker
+ */
+exports.showSaveFileDialog = function (
+ parentWindow,
+ suggestedFilename,
+ filters = []
+) {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ if (suggestedFilename) {
+ fp.defaultString = suggestedFilename;
+ }
+
+ fp.init(parentWindow, null, fp.modeSave);
+ if (Array.isArray(filters) && filters.length) {
+ for (const { pattern, label } of filters) {
+ fp.appendFilter(label, pattern);
+ }
+ } else {
+ fp.appendFilters(fp.filterAll);
+ }
+
+ return new Promise((resolve, reject) => {
+ fp.open(result => {
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ reject();
+ } else {
+ resolve(fp.file);
+ }
+ });
+ });
+};
+
+/*
+ * All of the flags have been moved to a different module. Make sure
+ * nobody is accessing them anymore, and don't write new code using
+ * them. We can remove this code after a while.
+ */
+function errorOnFlag(exports, name) {
+ Object.defineProperty(exports, name, {
+ get: () => {
+ const msg =
+ `Cannot get the flag ${name}. ` +
+ `Use the "devtools/shared/flags" module instead`;
+ console.error(msg);
+ throw new Error(msg);
+ },
+ set: () => {
+ const msg =
+ `Cannot set the flag ${name}. ` +
+ `Use the "devtools/shared/flags" module instead`;
+ console.error(msg);
+ throw new Error(msg);
+ },
+ });
+}
+
+errorOnFlag(exports, "testing");
+errorOnFlag(exports, "wantLogging");
+errorOnFlag(exports, "wantVerbose");
+
+// Calls the property with the given `name` on the given `object`, where
+// `name` is a string, and `object` a Debugger.Object instance.
+//
+// This function uses only the Debugger.Object API to call the property. It
+// avoids the use of unsafeDeference. This is useful for example in workers,
+// where unsafeDereference will return an opaque security wrapper to the
+// referent.
+function callPropertyOnObject(object, name, ...args) {
+ // Find the property.
+ let descriptor;
+ let proto = object;
+ do {
+ descriptor = proto.getOwnPropertyDescriptor(name);
+ if (descriptor !== undefined) {
+ break;
+ }
+ proto = proto.proto;
+ } while (proto !== null);
+ if (descriptor === undefined) {
+ throw new Error("No such property");
+ }
+ const value = descriptor.value;
+ if (typeof value !== "object" || value === null || !("callable" in value)) {
+ throw new Error("Not a callable object.");
+ }
+
+ // Call the property.
+ const result = value.call(object, ...args);
+ if (result === null) {
+ throw new Error("Code was terminated.");
+ }
+ if ("throw" in result) {
+ throw result.throw;
+ }
+ return result.return;
+}
+
+exports.callPropertyOnObject = callPropertyOnObject;
+
+// Convert a Debugger.Object wrapping an iterator into an iterator in the
+// debugger's realm.
+function* makeDebuggeeIterator(object) {
+ while (true) {
+ const nextValue = callPropertyOnObject(object, "next");
+ if (exports.getProperty(nextValue, "done")) {
+ break;
+ }
+ yield exports.getProperty(nextValue, "value");
+ }
+}
+
+exports.makeDebuggeeIterator = makeDebuggeeIterator;
+
+/**
+ * Shared helper to retrieve the topmost window. This can be used to retrieve the chrome
+ * window embedding the DevTools frame.
+ */
+function getTopWindow(win) {
+ return win.windowRoot ? win.windowRoot.ownerGlobal : win.top;
+}
+
+exports.getTopWindow = getTopWindow;
+
+/**
+ * Check whether two objects are identical by performing
+ * a deep equality check on their properties and values.
+ * See toolkit/modules/ObjectUtils.jsm for implementation.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Boolean}
+ */
+exports.deepEqual = (a, b) => {
+ return lazy.ObjectUtils.deepEqual(a, b);
+};
+
+function isWorkerDebuggerAlive(dbg) {
+ // Some workers are zombies. `isClosed` is false, but nothing works.
+ // `postMessage` is a noop, `addListener`'s `onClosed` doesn't work.
+ // (Ignore dbg without `window` as they aren't related to docShell
+ // and probably do not suffer form this issue)
+ return !dbg.isClosed && (!dbg.window || dbg.window.docShell);
+}
+exports.isWorkerDebuggerAlive = isWorkerDebuggerAlive;
diff --git a/devtools/shared/ThreadSafeDevToolsUtils.js b/devtools/shared/ThreadSafeDevToolsUtils.js
new file mode 100644
index 0000000000..fba93a9dcc
--- /dev/null
+++ b/devtools/shared/ThreadSafeDevToolsUtils.js
@@ -0,0 +1,363 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * General utilities used throughout devtools that can also be used in
+ * workers.
+ */
+
+/**
+ * Immutably reduce the given `...objs` into one object. The reduction is
+ * applied from left to right, so `immutableUpdate({ a: 1 }, { a: 2 })` will
+ * result in `{ a: 2 }`. The resulting object is frozen.
+ *
+ * Example usage:
+ *
+ * const original = { foo: 1, bar: 2, baz: 3 };
+ * const modified = immutableUpdate(original, { baz: 0, bang: 4 });
+ *
+ * // We get the new object that we expect...
+ * assert(modified.baz === 0);
+ * assert(modified.bang === 4);
+ *
+ * // However, the original is not modified.
+ * assert(original.baz === 2);
+ * assert(original.bang === undefined);
+ *
+ * @param {...Object} ...objs
+ * @returns {Object}
+ */
+exports.immutableUpdate = function (...objs) {
+ return Object.freeze(Object.assign({}, ...objs));
+};
+
+/**
+ * Utility function for updating an object with the properties of
+ * other objects.
+ *
+ * DEPRECATED: Just use Object.assign() instead!
+ *
+ * @param aTarget Object
+ * The object being updated.
+ * @param aNewAttrs Object
+ * The rest params are objects to update aTarget with. You
+ * can pass as many as you like.
+ */
+exports.update = function update(target, ...args) {
+ for (const attrs of args) {
+ for (const key in attrs) {
+ const desc = Object.getOwnPropertyDescriptor(attrs, key);
+
+ if (desc) {
+ Object.defineProperty(target, key, desc);
+ }
+ }
+ }
+ return target;
+};
+
+/**
+ * Utility function for getting the values from an object as an array
+ *
+ * @param object Object
+ * The object to iterate over
+ */
+exports.values = function values(object) {
+ return Object.keys(object).map(k => object[k]);
+};
+
+/**
+ * Report that |who| threw an exception, |exception|.
+ */
+exports.reportException = function reportException(who, exception) {
+ const msg = `${who} threw an exception: ${exports.safeErrorString(
+ exception
+ )}`;
+ dump(msg + "\n");
+
+ if (typeof console !== "undefined" && console && console.error) {
+ console.error(exception);
+ }
+};
+
+/**
+ * Given a handler function that may throw, return an infallible handler
+ * function that calls the fallible handler, and logs any exceptions it
+ * throws.
+ *
+ * @param handler function
+ * A handler function, which may throw.
+ * @param aName string
+ * A name for handler, for use in error messages. If omitted, we use
+ * handler.name.
+ *
+ * (SpiderMonkey does generate good names for anonymous functions, but we
+ * don't have a way to get at them from JavaScript at the moment.)
+ */
+exports.makeInfallible = function (handler, name = handler.name) {
+ return function () {
+ try {
+ return handler.apply(this, arguments);
+ } catch (ex) {
+ let who = "Handler function";
+ if (name) {
+ who += " " + name;
+ }
+ exports.reportException(who, ex);
+ return undefined;
+ }
+ };
+};
+
+/**
+ * Turn the |error| into a string, without fail.
+ *
+ * @param {Error|any} error
+ */
+exports.safeErrorString = function (error) {
+ try {
+ let errorString = error.toString();
+ if (typeof errorString == "string") {
+ // Attempt to attach a stack to |errorString|. If it throws an error, or
+ // isn't a string, don't use it.
+ try {
+ if (error.stack) {
+ const stack = error.stack.toString();
+ if (typeof stack == "string") {
+ errorString += "\nStack: " + stack;
+ }
+ }
+ } catch (ee) {
+ // Ignore.
+ }
+
+ // Append additional line and column number information to the output,
+ // since it might not be part of the stringified error.
+ if (
+ typeof error.lineNumber == "number" &&
+ typeof error.columnNumber == "number"
+ ) {
+ errorString +=
+ "Line: " + error.lineNumber + ", column: " + error.columnNumber;
+ }
+
+ return errorString;
+ }
+ } catch (ee) {
+ // Ignore.
+ }
+
+ // We failed to find a good error description, so do the next best thing.
+ return Object.prototype.toString.call(error);
+};
+
+/**
+ * Interleaves two arrays element by element, returning the combined array, like
+ * a zip. In the case of arrays with different sizes, undefined values will be
+ * interleaved at the end along with the extra values of the larger array.
+ *
+ * @param Array a
+ * @param Array b
+ * @returns Array
+ * The combined array, in the form [a1, b1, a2, b2, ...]
+ */
+exports.zip = function (a, b) {
+ if (!b) {
+ return a;
+ }
+ if (!a) {
+ return b;
+ }
+ const pairs = [];
+ for (
+ let i = 0, aLength = a.length, bLength = b.length;
+ i < aLength || i < bLength;
+ i++
+ ) {
+ pairs.push([a[i], b[i]]);
+ }
+ return pairs;
+};
+
+/**
+ * Converts an object into an array with 2-element arrays as key/value
+ * pairs of the object. `{ foo: 1, bar: 2}` would become
+ * `[[foo, 1], [bar 2]]` (order not guaranteed).
+ *
+ * @param object obj
+ * @returns array
+ */
+exports.entries = function entries(obj) {
+ return Object.keys(obj).map(k => [k, obj[k]]);
+};
+
+/*
+ * Takes an array of 2-element arrays as key/values pairs and
+ * constructs an object using them.
+ */
+exports.toObject = function (arr) {
+ const obj = {};
+ for (const [k, v] of arr) {
+ obj[k] = v;
+ }
+ return obj;
+};
+
+/**
+ * Composes the given functions into a single function, which will
+ * apply the results of each function right-to-left, starting with
+ * applying the given arguments to the right-most function.
+ * `compose(foo, bar, baz)` === `args => foo(bar(baz(args)))`
+ *
+ * @param ...function funcs
+ * @returns function
+ */
+exports.compose = function compose(...funcs) {
+ return (...args) => {
+ const initialValue = funcs[funcs.length - 1](...args);
+ const leftFuncs = funcs.slice(0, -1);
+ return leftFuncs.reduceRight((composed, f) => f(composed), initialValue);
+ };
+};
+
+/**
+ * Return true if `thing` is a generator function, false otherwise.
+ */
+exports.isGenerator = function (fn) {
+ if (typeof fn !== "function") {
+ return false;
+ }
+ const proto = Object.getPrototypeOf(fn);
+ if (!proto) {
+ return false;
+ }
+ const ctor = proto.constructor;
+ if (!ctor) {
+ return false;
+ }
+ return ctor.name == "GeneratorFunction";
+};
+
+/**
+ * Return true if `thing` is an async function, false otherwise.
+ */
+exports.isAsyncFunction = function (fn) {
+ if (typeof fn !== "function") {
+ return false;
+ }
+ const proto = Object.getPrototypeOf(fn);
+ if (!proto) {
+ return false;
+ }
+ const ctor = proto.constructor;
+ if (!ctor) {
+ return false;
+ }
+ return ctor.name == "AsyncFunction";
+};
+
+/**
+ * Return true if `thing` is a Promise or then-able, false otherwise.
+ */
+exports.isPromise = function (p) {
+ return p && typeof p.then === "function";
+};
+
+/**
+ * Return true if `thing` is a SavedFrame, false otherwise.
+ */
+exports.isSavedFrame = function (thing) {
+ return Object.prototype.toString.call(thing) === "[object SavedFrame]";
+};
+
+/**
+ * Return true iff `thing` is a `Set` object (possibly from another global).
+ */
+exports.isSet = function (thing) {
+ return Object.prototype.toString.call(thing) === "[object Set]";
+};
+
+/**
+ * Given a list of lists, flatten it. Only flattens one level; does not
+ * recursively flatten all levels.
+ *
+ * @param {Array<Array<Any>>} lists
+ * @return {Array<Any>}
+ */
+exports.flatten = function (lists) {
+ return Array.prototype.concat.apply([], lists);
+};
+
+/**
+ * Returns a promise that is resolved or rejected when all promises have settled
+ * (resolved or rejected).
+ *
+ * This differs from Promise.all, which will reject immediately after the first
+ * rejection, instead of waiting for the remaining promises to settle.
+ *
+ * @param values
+ * Iterable of promises that may be pending, resolved, or rejected. When
+ * when all promises have settled (resolved or rejected), the returned
+ * promise will be resolved or rejected as well.
+ *
+ * @return A new promise that is fulfilled when all values have settled
+ * (resolved or rejected). Its resolution value will be an array of all
+ * resolved values in the given order, or undefined if values is an
+ * empty array. The reject reason will be forwarded from the first
+ * promise in the list of given promises to be rejected.
+ */
+exports.settleAll = values => {
+ if (values === null || typeof values[Symbol.iterator] != "function") {
+ throw new Error("settleAll() expects an iterable.");
+ }
+
+ return new Promise((resolve, reject) => {
+ values = Array.isArray(values) ? values : [...values];
+ let countdown = values.length;
+ const resolutionValues = new Array(countdown);
+ let rejectionValue;
+ let rejectionOccurred = false;
+
+ if (!countdown) {
+ resolve(resolutionValues);
+ return;
+ }
+
+ function checkForCompletion() {
+ if (--countdown > 0) {
+ return;
+ }
+ if (!rejectionOccurred) {
+ resolve(resolutionValues);
+ } else {
+ reject(rejectionValue);
+ }
+ }
+
+ for (let i = 0; i < values.length; i++) {
+ const index = i;
+ const value = values[i];
+ const resolver = result => {
+ resolutionValues[index] = result;
+ checkForCompletion();
+ };
+ const rejecter = error => {
+ if (!rejectionOccurred) {
+ rejectionValue = error;
+ rejectionOccurred = true;
+ }
+ checkForCompletion();
+ };
+
+ if (value && typeof value.then == "function") {
+ value.then(resolver, rejecter);
+ } else {
+ // Given value is not a promise, forward it as a resolution value.
+ resolver(value);
+ }
+ }
+ });
+};
diff --git a/devtools/shared/accessibility.js b/devtools/shared/accessibility.js
new file mode 100644
index 0000000000..db6983d033
--- /dev/null
+++ b/devtools/shared/accessibility.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+loader.lazyRequireGetter(
+ this,
+ "colorUtils",
+ "resource://devtools/shared/css/color.js",
+ true
+);
+const {
+ accessibility: {
+ SCORES: { FAIL, AA, AAA },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+/**
+ * Mapping of text size to contrast ratio score levels
+ */
+const LEVELS = {
+ LARGE_TEXT: { AA: 3, AAA: 4.5 },
+ REGULAR_TEXT: { AA: 4.5, AAA: 7 },
+};
+
+/**
+ * Mapping of large text size to CSS pixel value
+ */
+const LARGE_TEXT = {
+ // CSS pixel value (constant) that corresponds to 14 point text size which defines large
+ // text when font text is bold (font weight is greater than or equal to 600).
+ BOLD_LARGE_TEXT_MIN_PIXELS: 18.66,
+ // CSS pixel value (constant) that corresponds to 18 point text size which defines large
+ // text for normal text (e.g. not bold).
+ LARGE_TEXT_MIN_PIXELS: 24,
+};
+
+/**
+ * Get contrast ratio score based on WCAG criteria.
+ * @param {Number} ratio
+ * Value of the contrast ratio for a given accessible object.
+ * @param {Boolean} isLargeText
+ * True if the accessible object contains large text.
+ * @return {String}
+ * Value that represents calculated contrast ratio score.
+ */
+function getContrastRatioScore(ratio, isLargeText) {
+ const levels = isLargeText ? LEVELS.LARGE_TEXT : LEVELS.REGULAR_TEXT;
+
+ let score = FAIL;
+ if (ratio >= levels.AAA) {
+ score = AAA;
+ } else if (ratio >= levels.AA) {
+ score = AA;
+ }
+
+ return score;
+}
+
+/**
+ * Get calculated text style properties from a node's computed style, if possible.
+ * @param {Object} computedStyle
+ * Computed style using which text styling information is to be calculated.
+ * - fontSize {String}
+ * Font size of the text
+ * - fontWeight {String}
+ * Font weight of the text
+ * - color {String}
+ * Rgb color of the text
+ * - opacity {String} Optional
+ * Opacity of the text
+ * @return {Object}
+ * Color and text size information for a given DOM node.
+ */
+function getTextProperties(computedStyle) {
+ const { color, fontSize, fontWeight } = computedStyle;
+ let { r, g, b, a } = InspectorUtils.colorToRGBA(color);
+
+ // If the element has opacity in addition to background alpha value, take it
+ // into account. TODO: this does not handle opacity set on ancestor elements
+ // (see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1544721).
+ const opacity = computedStyle.opacity
+ ? parseFloat(computedStyle.opacity)
+ : null;
+ if (opacity) {
+ a = opacity * a;
+ }
+
+ const textRgbaColor = new colorUtils.CssColor(
+ `rgba(${r}, ${g}, ${b}, ${a})`,
+ true
+ );
+ // TODO: For cases where text color is transparent, it likely comes from the color of
+ // the background that is underneath it (commonly from background-clip: text
+ // property). With some additional investigation it might be possible to calculate the
+ // color contrast where the color of the background is used as text color and the
+ // color of the ancestor's background is used as its background.
+ if (textRgbaColor.isTransparent()) {
+ return null;
+ }
+
+ const isBoldText = parseInt(fontWeight, 10) >= 600;
+ const size = parseFloat(fontSize);
+ const isLargeText =
+ size >=
+ (isBoldText
+ ? LARGE_TEXT.BOLD_LARGE_TEXT_MIN_PIXELS
+ : LARGE_TEXT.LARGE_TEXT_MIN_PIXELS);
+
+ return {
+ color: [r, g, b, a],
+ isLargeText,
+ isBoldText,
+ size,
+ opacity,
+ };
+}
+
+/**
+ * Calculates contrast ratio or range of contrast ratios of the referenced DOM node
+ * against the given background color data. If background is multi-colored, return a
+ * range, otherwise a single contrast ratio.
+ *
+ * @param {Object} backgroundColorData
+ * Object with one or more of the following properties:
+ * - value {Array}
+ * rgba array for single color background
+ * - min {Array}
+ * min luminance rgba array for multi color background
+ * - max {Array}
+ * max luminance rgba array for multi color background
+ * @param {Object} textData
+ * - color {Array}
+ * rgba array for text of referenced DOM node
+ * - isLargeText {Boolean}
+ * True if text of referenced DOM node is large
+ * @return {Object}
+ * An object that may contain one or more of the following fields: error,
+ * isLargeText, value, min, max values for contrast.
+ */
+function getContrastRatioAgainstBackground(
+ backgroundColorData,
+ { color, isLargeText }
+) {
+ if (backgroundColorData.value) {
+ const value = colorUtils.calculateContrastRatio(
+ backgroundColorData.value,
+ color
+ );
+ return {
+ value,
+ color,
+ backgroundColor: backgroundColorData.value,
+ isLargeText,
+ score: getContrastRatioScore(value, isLargeText),
+ };
+ }
+
+ let { min: backgroundColorMin, max: backgroundColorMax } =
+ backgroundColorData;
+ let min = colorUtils.calculateContrastRatio(backgroundColorMin, color);
+ let max = colorUtils.calculateContrastRatio(backgroundColorMax, color);
+
+ // Flip minimum and maximum contrast ratios if necessary.
+ if (min > max) {
+ [min, max] = [max, min];
+ [backgroundColorMin, backgroundColorMax] = [
+ backgroundColorMax,
+ backgroundColorMin,
+ ];
+ }
+
+ const score = getContrastRatioScore(min, isLargeText);
+
+ return {
+ min,
+ max,
+ color,
+ backgroundColorMin,
+ backgroundColorMax,
+ isLargeText,
+ score,
+ scoreMin: score,
+ scoreMax: getContrastRatioScore(max, isLargeText),
+ };
+}
+
+exports.getContrastRatioScore = getContrastRatioScore;
+exports.getTextProperties = getTextProperties;
+exports.getContrastRatioAgainstBackground = getContrastRatioAgainstBackground;
+exports.LARGE_TEXT = LARGE_TEXT;
diff --git a/devtools/shared/async-storage.js b/devtools/shared/async-storage.js
new file mode 100644
index 0000000000..dd7ee0674e
--- /dev/null
+++ b/devtools/shared/async-storage.js
@@ -0,0 +1,226 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ *
+ * Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/shared/js/async_storage.js
+ * (converted to use Promises instead of callbacks).
+ *
+ * This file defines an asynchronous version of the localStorage API, backed by
+ * an IndexedDB database. It creates a global asyncStorage object that has
+ * methods like the localStorage object.
+ *
+ * To store a value use setItem:
+ *
+ * asyncStorage.setItem("key", "value");
+ *
+ * This returns a promise in case you want confirmation that the value has been stored.
+ *
+ * asyncStorage.setItem("key", "newvalue").then(function() {
+ * console.log("new value stored");
+ * });
+ *
+ * To read a value, call getItem(), but note that you must wait for a promise
+ * resolution for the value to be retrieved.
+ *
+ * asyncStorage.getItem("key").then(function(value) {
+ * console.log("The value of key is:", value);
+ * });
+ *
+ * Note that unlike localStorage, asyncStorage does not allow you to store and
+ * retrieve values by setting and querying properties directly. You cannot just
+ * write asyncStorage.key; you have to explicitly call setItem() or getItem().
+ *
+ * removeItem(), clear(), length(), and key() are like the same-named methods of
+ * localStorage, and all return a promise.
+ *
+ * The asynchronous nature of getItem() makes it tricky to retrieve multiple
+ * values. But unlike localStorage, asyncStorage does not require the values you
+ * store to be strings. So if you need to save multiple values and want to
+ * retrieve them together, in a single asynchronous operation, just group the
+ * values into a single object. The properties of this object may not include
+ * DOM elements, but they may include things like Blobs and typed arrays.
+ *
+ */
+
+"use strict";
+
+const DBNAME = "devtools-async-storage";
+const DBVERSION = 1;
+const STORENAME = "keyvaluepairs";
+var db = null;
+
+loader.lazyRequireGetter(
+ this,
+ "indexedDB",
+ "resource://devtools/shared/indexed-db.js"
+);
+
+function withStore(type, onsuccess, onerror) {
+ if (db) {
+ const transaction = db.transaction(STORENAME, type);
+ const store = transaction.objectStore(STORENAME);
+ onsuccess(store);
+ } else {
+ const openreq = indexedDB.open(DBNAME, DBVERSION);
+ openreq.onerror = function withStoreOnError() {
+ onerror();
+ };
+ openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
+ // First time setup: create an empty object store
+ openreq.result.createObjectStore(STORENAME);
+ };
+ openreq.onsuccess = function withStoreOnSuccess() {
+ db = openreq.result;
+ const transaction = db.transaction(STORENAME, type);
+ const store = transaction.objectStore(STORENAME);
+ onsuccess(store);
+ };
+ }
+}
+
+function getItem(itemKey) {
+ return new Promise((resolve, reject) => {
+ let req;
+ withStore(
+ "readonly",
+ store => {
+ store.transaction.oncomplete = function onComplete() {
+ let value = req.result;
+ if (value === undefined) {
+ value = null;
+ }
+ resolve(value);
+ };
+ req = store.get(itemKey);
+ req.onerror = function getItemOnError() {
+ console.error("Error in asyncStorage.getItem():", req.error.name);
+ reject(req.error);
+ };
+ },
+ reject
+ );
+ });
+}
+
+function setItem(itemKey, value) {
+ return new Promise((resolve, reject) => {
+ withStore(
+ "readwrite",
+ store => {
+ store.transaction.oncomplete = resolve;
+ const req = store.put(value, itemKey);
+ req.onerror = function setItemOnError() {
+ console.error("Error in asyncStorage.setItem():", req.error.name);
+ reject(req.error);
+ };
+ },
+ reject
+ );
+ });
+}
+
+function removeItem(itemKey) {
+ return new Promise((resolve, reject) => {
+ withStore(
+ "readwrite",
+ store => {
+ store.transaction.oncomplete = resolve;
+ const req = store.delete(itemKey);
+ req.onerror = function removeItemOnError() {
+ console.error("Error in asyncStorage.removeItem():", req.error.name);
+ reject(req.error);
+ };
+ },
+ reject
+ );
+ });
+}
+
+function clear() {
+ return new Promise((resolve, reject) => {
+ withStore(
+ "readwrite",
+ store => {
+ store.transaction.oncomplete = resolve;
+ const req = store.clear();
+ req.onerror = function clearOnError() {
+ console.error("Error in asyncStorage.clear():", req.error.name);
+ reject(req.error);
+ };
+ },
+ reject
+ );
+ });
+}
+
+function length() {
+ return new Promise((resolve, reject) => {
+ let req;
+ withStore(
+ "readonly",
+ store => {
+ store.transaction.oncomplete = function onComplete() {
+ resolve(req.result);
+ };
+ req = store.count();
+ req.onerror = function lengthOnError() {
+ console.error("Error in asyncStorage.length():", req.error.name);
+ reject(req.error.name);
+ };
+ },
+ reject
+ );
+ });
+}
+
+function key(n) {
+ return new Promise((resolve, reject) => {
+ if (n < 0) {
+ resolve(null);
+ return;
+ }
+
+ let req;
+ withStore(
+ "readonly",
+ store => {
+ store.transaction.oncomplete = function onComplete() {
+ const cursor = req.result;
+ resolve(cursor ? cursor.key : null);
+ };
+ let advanced = false;
+ req = store.openCursor();
+ req.onsuccess = function keyOnSuccess() {
+ const cursor = req.result;
+ if (!cursor) {
+ // this means there weren"t enough keys
+ return;
+ }
+ if (n === 0 || advanced) {
+ // Either 1) we have the first key, return it if that's what they
+ // wanted, or 2) we"ve got the nth key.
+ return;
+ }
+
+ // Otherwise, ask the cursor to skip ahead n records
+ advanced = true;
+ cursor.advance(n);
+ };
+ req.onerror = function keyOnError() {
+ console.error("Error in asyncStorage.key():", req.error.name);
+ reject(req.error);
+ };
+ },
+ reject
+ );
+ });
+}
+
+exports.getItem = getItem;
+exports.setItem = setItem;
+exports.removeItem = removeItem;
+exports.clear = clear;
+exports.length = length;
+exports.key = key;
diff --git a/devtools/shared/async-utils.js b/devtools/shared/async-utils.js
new file mode 100644
index 0000000000..cb1fee1a4f
--- /dev/null
+++ b/devtools/shared/async-utils.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Helpers for async functions. An async function returns a Promise for the
+ * resolution of the function. When the function returns, the promise is
+ * resolved with the returned value. If it throws the promise rejects with
+ * the thrown error.
+ */
+
+/**
+ * Adds an event listener to the given element, and then removes its event
+ * listener once the event is called, returning the event object as a promise.
+ * @param Element element
+ * The DOM element to listen on
+ * @param String event
+ * The name of the event type to listen for
+ * @param Boolean useCapture
+ * Should we initiate the capture phase?
+ * @return Promise
+ * The promise resolved with the event object when the event first
+ * happens
+ */
+exports.listenOnce = function listenOnce(element, event, useCapture) {
+ return new Promise(function (resolve, reject) {
+ const onEvent = function (ev) {
+ element.removeEventListener(event, onEvent, useCapture);
+ resolve(ev);
+ };
+ element.addEventListener(event, onEvent, useCapture);
+ });
+};
+
+// Return value when `safeAsyncMethod` catches an error.
+const SWALLOWED_RET = Symbol("swallowed");
+
+/**
+ * Wraps the provided async method in a try/catch block.
+ * If an error is caught while running the method, check the provided condition
+ * to decide whether the error should bubble or not.
+ *
+ * @param {Function} asyncFn
+ * The async method to wrap.
+ * @param {Function} shouldSwallow
+ * Function that will run when an error is caught. If it returns true,
+ * the error will be swallowed. Otherwise, it will bubble up.
+ * @param {Mixed} retValue
+ * Optional value to return when an error is caught and is swallowed.
+ * @return {Function} The wrapped method.
+ */
+exports.safeAsyncMethod = function (
+ asyncFn,
+ shouldSwallow,
+ retValue = SWALLOWED_RET
+) {
+ return async function (...args) {
+ try {
+ const ret = await asyncFn(...args);
+ return ret;
+ } catch (e) {
+ if (shouldSwallow()) {
+ console.warn("Async method failed in safeAsyncMethod", e);
+ return retValue;
+ }
+ throw e;
+ }
+ };
+};
diff --git a/devtools/shared/commands/README.md b/devtools/shared/commands/README.md
new file mode 100644
index 0000000000..8ed199828d
--- /dev/null
+++ b/devtools/shared/commands/README.md
@@ -0,0 +1,51 @@
+# Commands
+
+Commands are singletons, which can be easily used by any frontend code.
+They are meant to be exposed widely to the frontend so that any code can easily call any of their methods.
+
+Commands classes expose static methods, which:
+* route to the right Front/Actor's method
+* handle backward compatibility
+* map to many target's actor if needed
+
+These classes are instantiated once per descriptor
+and may have inner state, emit events, fire callbacks,...
+
+A transient backward compat need, required by Fission refactorings will be to have some code checking a trait, and either:
+* call a single method on a parent process actor (like BreakpointListActor.setBreakpoint)
+* otherwise, call a method on each target's scoped actor (like ThreadActor.setBreakpoint, that, for each available target)
+
+Without such layer, we would have to put such code here and there in the frontend code.
+This will be harder to remove later, once we get rid of old pre-fission-refactoring codepaths.
+
+This layer already exists in some panels, but we are using slightly different names and practices:
+* Debugger uses "client" (devtools/client/debugger/src/client/) and "commands" (devtools/client/debugger/src/client/firefox/commands.js)
+ Debugger's commands already bundle the code to dispatch an action to many target's actor.
+ They also contain some backward compat code.
+ Today, we pass around a `client` object via thunkArgs, which is mapped to commands.js,
+ instead we could pass a debugger command object.
+* Network Monitor uses "connector" (devtools/client/netmonitor/src/connector)
+ Connectors also bundles backward compat and dispatch to many target's actor.
+ Today, we pass the `connector` to all middlewares from configureStore,
+ we could instead pass the netmonitor command object.
+* Web Console has:
+ * devtools/client/webconsole/actions/input.js:handleHelperResult(), where we have to put some code, which is a duplicate of Netmonitor Connector,
+ and could be shared via a netmonitor command class.
+* Inspector is probably the panel doing the most dispatch to many target's actor.
+ Codes using getAllInspectorFronts could all be migrated to an inspector command class:
+ https://searchfox.org/mozilla-central/search?q=symbol:%23getAllInspectorFronts&redirect=false
+ and simplify a bit the frontend.
+ It is also one panel, which still register listener to each target's inspector/walker fronts.
+ Because inspector isn't using resources.
+ But this work, registering listeners for each target might be done by such layer and translate the many actor's event into a unified one.
+
+Last, but not least, this layer may allow us to slowly get rid of protocol.js.
+Command classes aren't Fronts, nor are they particularly connected to protocol.js.
+If we make it so that all the Frontend code using Fronts uses Commands instead, we might more easily get away from protocol.js.
+
+If you want to create a new command, you can use a bash script to help your bootstrap all basic required files:
+```
+$ ./create-command.sh command-file-name CommandName
+```
+Where the first argument will be the name used for folder and files, using lower case and dash as separator.
+And the second argument will be the class name in code, using camlcase.
diff --git a/devtools/shared/commands/commands-factory.js b/devtools/shared/commands/commands-factory.js
new file mode 100644
index 0000000000..257f50ce2f
--- /dev/null
+++ b/devtools/shared/commands/commands-factory.js
@@ -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/. */
+
+"use strict";
+
+const {
+ createCommandsDictionary,
+} = require("resource://devtools/shared/commands/index.js");
+const { DevToolsLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsServer",
+ "resource://devtools/server/devtools-server.js",
+ true
+);
+// eslint-disable-next-line mozilla/reject-some-requires
+loader.lazyRequireGetter(
+ this,
+ "DevToolsClient",
+ "resource://devtools/client/devtools-client.js",
+ true
+);
+
+/**
+ * Functions for creating Commands for all debuggable contexts.
+ *
+ * All methods of this `CommandsFactory` object receive argument to describe to
+ * which particular context we want to debug. And all returns a new instance of `commands` object.
+ * Commands are implemented by modules defined in devtools/shared/commands.
+ */
+exports.CommandsFactory = {
+ /**
+ * Create commands for a given local tab.
+ *
+ * @param {Tab} tab: A local Firefox tab, running in this process.
+ * @param {Object} options
+ * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
+ * a new one will be created.
+ * @param {DevToolsClient} options.isWebExtension: An optional boolean to flag commands
+ * that are created for the WebExtension codebase.
+ * @returns {Object} Commands
+ */
+ async forTab(tab, { client, isWebExtension } = {}) {
+ if (!client) {
+ client = await createLocalClient();
+ }
+
+ const descriptor = await client.mainRoot.getTab({ tab, isWebExtension });
+ descriptor.doNotAttachThreadActor = isWebExtension;
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+
+ /**
+ * Chrome mochitest don't have access to any "tab",
+ * so that the only way to attach to a fake tab is call RootFront.getTab
+ * without any argument.
+ */
+ async forCurrentTabInChromeMochitest() {
+ const client = await createLocalClient();
+ const descriptor = await client.mainRoot.getTab();
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+
+ /**
+ * Create commands for the main process.
+ *
+ * @param {Object} options
+ * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
+ * a new one will be created.
+ * @returns {Object} Commands
+ */
+ async forMainProcess({ client } = {}) {
+ if (!client) {
+ client = await createLocalClient();
+ }
+
+ const descriptor = await client.mainRoot.getMainProcess();
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+
+ /**
+ * Create commands for a given remote tab.
+ *
+ * Note that it can also be used for local tab, but isLocalTab attribute
+ * on commands.descriptorFront will be false.
+ *
+ * @param {Number} browserId: Identify which tab we should create commands for.
+ * @param {Object} options
+ * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
+ * a new one will be created.
+ * @returns {Object} Commands
+ */
+ async forRemoteTab(browserId, { client } = {}) {
+ if (!client) {
+ client = await createLocalClient();
+ }
+
+ const descriptor = await client.mainRoot.getTab({ browserId });
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+
+ /**
+ * Create commands for a given main process worker.
+ *
+ * @param {String} id: WorkerDebugger's id, which is a unique ID computed by the platform code.
+ * These ids are exposed via WorkerDescriptor's id attributes.
+ * WorkerDescriptors can be retrieved via MainFront.listAllWorkers()/listWorkers().
+ * @param {Object} options
+ * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
+ * a new one will be created.
+ * @returns {Object} Commands
+ */
+ async forWorker(id, { client } = {}) {
+ if (!client) {
+ client = await createLocalClient();
+ }
+
+ const descriptor = await client.mainRoot.getWorker(id);
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+
+ /**
+ * Create commands for a Web Extension.
+ *
+ * @param {String} id The Web Extension ID to debug.
+ * @param {Object} options
+ * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
+ * a new one will be created.
+ * @returns {Object} Commands
+ */
+ async forAddon(id, { client } = {}) {
+ if (!client) {
+ client = await createLocalClient();
+ }
+
+ const descriptor = await client.mainRoot.getAddon({ id });
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+
+ /**
+ * This method will spawn a special `DevToolsClient`
+ * which is meant to debug the same Firefox instance
+ * and especially be able to debug chrome code.
+ * The chrome code typically runs in the system principal.
+ * This principal is a singleton which is shared among most Firefox internal codebase
+ * (JSM, privileged html documents, JS-XPCOM,...)
+ * In order to be able to debug these script we need to connect to a special DevToolsServer
+ * that runs in a dedicated and distinct system principal which is different from
+ * the one shared with the rest of Firefox frontend codebase.
+ */
+ async spawnClientToDebugSystemPrincipal() {
+ // The Browser console ends up using the debugger in autocomplete.
+ // Because the debugger can't be running in the same compartment than its debuggee,
+ // we have to load the server in a dedicated Loader, flagged with
+ // `freshCompartment`, which will force it to be loaded in another compartment.
+ // We aren't using `invisibleToDebugger` in order to allow the Browser toolbox to
+ // debug the Browser console. This is fine as they will spawn distinct Loaders and
+ // so distinct `DevToolsServer` and actor modules.
+ const customLoader = new DevToolsLoader({
+ freshCompartment: true,
+ });
+ const { DevToolsServer: customDevToolsServer } = customLoader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+
+ customDevToolsServer.init();
+
+ // We want all the actors (root, browser and target-scoped) to be registered on the
+ // DevToolsServer. This is needed so the Browser Console can retrieve:
+ // - the console actors, which are target-scoped (See Bug 1416105)
+ // - the screenshotActor, which is browser-scoped (for the `:screenshot` command)
+ customDevToolsServer.registerAllActors();
+
+ customDevToolsServer.allowChromeProcess = true;
+
+ const client = new DevToolsClient(customDevToolsServer.connectPipe());
+ await client.connect();
+
+ return client;
+ },
+
+ /**
+ * One method to handle the whole setup sequence to connect to RDP backend for the Browser Console.
+ *
+ * This will instantiate a special DevTools module loader for the DevToolsServer.
+ * Then spawn a DevToolsClient to connect to it.
+ * Get a Main Process Descriptor from it.
+ * Finally spawn a commands object for this descriptor.
+ */
+ async forBrowserConsole() {
+ // The Browser console ends up using the debugger in autocomplete.
+ // Because the debugger can't be running in the same compartment than its debuggee,
+ // we have to load the server in a dedicated Loader and so spawn a special client
+ const client = await this.spawnClientToDebugSystemPrincipal();
+
+ const descriptor = await client.mainRoot.getMainProcess();
+
+ descriptor.doNotAttachThreadActor = true;
+
+ // Force fetching the first top level target right away.
+ await descriptor.getTarget();
+
+ const commands = await createCommandsDictionary(descriptor);
+ return commands;
+ },
+};
+
+async function createLocalClient() {
+ // Make sure the DevTools server is started.
+ ensureDevToolsServerInitialized();
+
+ // Create the client and connect it to the local server.
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ return client;
+}
+// Also expose this method for tests which would like to create a client
+// without involving commands. This would typically be tests against the Watcher actor
+// and requires to prevent having TargetCommand from running.
+// Or tests which are covering RootFront or global actor's fronts.
+exports.createLocalClientForTests = createLocalClient;
+
+function ensureDevToolsServerInitialized() {
+ // Since a remote protocol connection will be made, let's start the
+ // DevToolsServer here, once and for all tools.
+ DevToolsServer.init();
+
+ // Enable all the actors. We may not need all of them and registering
+ // only root and target might be enough
+ DevToolsServer.registerAllActors();
+
+ // Enable being able to get child process actors
+ // Same, this might not be useful
+ DevToolsServer.allowChromeProcess = true;
+}
diff --git a/devtools/shared/commands/create-command.sh b/devtools/shared/commands/create-command.sh
new file mode 100755
index 0000000000..1f95f96df6
--- /dev/null
+++ b/devtools/shared/commands/create-command.sh
@@ -0,0 +1,129 @@
+#!/bin/bash
+
+# Script to easily create a new command, including:
+# - a template for the main command file
+# - test folder and test head.js file
+# - a template for a first test
+# - all necessary build manifests
+
+if [[ -z $1 || -z $2 ]]; then
+ echo "$0 expects two arguments:"
+ echo "$(basename $0) command-file-name CommandName"
+ echo " 1) The file name for the command, with '-' as separators between words"
+ echo " This will be the name of the folder"
+ echo " 2) The command name being caml cased"
+ echo " This will be used to craft the name of the JavaScript class"
+ exit
+fi
+
+if [ -e $1 ]; then
+ echo "$1 already exists. Please use a new folder/command name."
+fi
+
+CMD_FOLDER=$1
+CMD_FILE_NAME=$1-command.js
+CMD_NAME=$2Command
+
+pushd `dirname $0`
+
+echo "Creating a new command called '$CMD_NAME' in $CMD_FOLDER"
+
+mkdir $CMD_FOLDER
+
+cat > $CMD_FOLDER/moz.build <<EOF
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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(
+ "$CMD_FILE_NAME",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
+EOF
+
+cat > $CMD_FOLDER/$CMD_FILE_NAME <<EOF
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * The $CMD_NAME ...
+ */
+class $CMD_NAME {
+ constructor({ commands, descriptorFront, watcherFront }) {
+ this.#commands = commands;
+ }
+ #commands = null;
+
+}
+
+module.exports = $CMD_NAME;
+EOF
+
+mkdir $CMD_FOLDER/tests
+cat > $CMD_FOLDER/tests/browser.toml <<EOF
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "head.js",
+]
+
+[browser_$1.js]
+EOF
+
+
+cat > $CMD_FOLDER/tests/head.js <<EOF
+* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+EOF
+
+CMD_NAME_FIRST_LOWERCASE=${CMD_NAME,}
+cat > $CMD_FOLDER/tests/browser_$1.js <<EOF
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the $CMD_NAME
+
+add_task(async function () {
+ info("Setup the test page");
+ const tab = await addTab("data:text/html;charset=utf-8,Test page");
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const { $CMD_NAME_FIRST_LOWERCASE } = commands;
+
+ // assertions...
+
+ await commands.destroy();
+ BrowserTestUtils.removeTab(tab);
+});
+EOF
+
+popd
+
+echo ""
+echo "Command created!"
+echo ""
+echo "Now:"
+echo " - edit moz.build to add '\"$CMD_FOLDER\",' in DIRS (this need to be kept sorted)"
+echo " - edit index.js to add '$CMD_NAME_FIRST_LOWERCASE: \"devtools/shared/commands/$CMD_FOLDER/$1-command\"' in Commands dictionary"
diff --git a/devtools/shared/commands/index.js b/devtools/shared/commands/index.js
new file mode 100644
index 0000000000..a9a35bedf7
--- /dev/null
+++ b/devtools/shared/commands/index.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";
+
+// List of all command modules
+// (please try to keep the list alphabetically sorted)
+/* eslint sort-keys: "error" */
+/* eslint-enable sort-keys */
+const Commands = {
+ inspectedWindowCommand:
+ "devtools/shared/commands/inspected-window/inspected-window-command",
+ inspectorCommand: "devtools/shared/commands/inspector/inspector-command",
+ networkCommand: "devtools/shared/commands/network/network-command",
+ objectCommand: "devtools/shared/commands/object/object-command",
+ resourceCommand: "devtools/shared/commands/resource/resource-command",
+ rootResourceCommand:
+ "devtools/shared/commands/root-resource/root-resource-command",
+ scriptCommand: "devtools/shared/commands/script/script-command",
+ targetCommand: "devtools/shared/commands/target/target-command",
+ targetConfigurationCommand:
+ "devtools/shared/commands/target-configuration/target-configuration-command",
+ threadConfigurationCommand:
+ "devtools/shared/commands/thread-configuration/thread-configuration-command",
+ tracerCommand: "devtools/shared/commands/tracer/tracer-command",
+};
+/* eslint-disable sort-keys */
+
+/**
+ * For a given descriptor and its related Targets, already initialized,
+ * return the dictionary with all command instances.
+ * This dictionary is lazy and commands will be loaded and instanciated on-demand.
+ */
+async function createCommandsDictionary(descriptorFront) {
+ // Bug 1675763: Watcher actor is not available in all situations yet.
+ let watcherFront;
+ const supportsWatcher = descriptorFront.traits?.watcher;
+ if (supportsWatcher) {
+ watcherFront = await descriptorFront.getWatcher();
+ }
+ const { client } = descriptorFront;
+
+ const allInstantiatedCommands = new Set();
+
+ const dictionary = {
+ // Expose both client and descriptor for legacy codebases, or tests.
+ // But ideally only commands should interact with these two objects
+ client,
+ descriptorFront,
+ watcherFront,
+
+ // Expose for tests
+ waitForRequestsToSettle() {
+ return descriptorFront.client.waitForRequestsToSettle();
+ },
+
+ // Boolean flag to know if the DevtoolsClient should be closed
+ // when this commands happens to be destroyed.
+ // This is set by:
+ // * commands-from-url in case we are opening a toolbox
+ // with a dedicated DevToolsClient (mostly from about:debugging, when the client isn't "cached").
+ // * CommandsFactory, when we are connecting to a local tab and expect
+ // the client, toolbox and descriptor to all follow the same lifecycle.
+ shouldCloseClient: true,
+
+ /**
+ * Destroy the commands which will destroy:
+ * - all inner commands,
+ * - the related descriptor,
+ * - the related DevToolsClient (not always)
+ */
+ async destroy() {
+ descriptorFront.off("descriptor-destroyed", this.destroy);
+
+ // Destroy all inner command modules
+ for (const command of allInstantiatedCommands) {
+ if (typeof command.destroy == "function") {
+ command.destroy();
+ }
+ }
+ allInstantiatedCommands.clear();
+
+ // Destroy the descriptor front, and all its children fronts.
+ // Watcher, targets,...
+ //
+ // Note that DescriptorFront.destroy will be null because of Pool.destroy
+ // when this function is called while the descriptor front itself is being
+ // destroyed.
+ if (!descriptorFront.isDestroyed()) {
+ await descriptorFront.destroy();
+ }
+
+ // Close the DevToolsClient. Shutting down the connection
+ // to the debuggable context and its DevToolsServer.
+ //
+ // See shouldCloseClient jsdoc about this condition.
+ if (this.shouldCloseClient) {
+ await client.close();
+ }
+ },
+ };
+ dictionary.destroy = dictionary.destroy.bind(dictionary);
+
+ // Automatically destroy the commands object if the descriptor
+ // happens to be destroyed. Which means that the debuggable context
+ // is no longer debuggable.
+ descriptorFront.on("descriptor-destroyed", dictionary.destroy);
+
+ for (const name in Commands) {
+ loader.lazyGetter(dictionary, name, () => {
+ const Constructor = require(Commands[name]);
+ const command = new Constructor({
+ // Commands can use other commands
+ commands: dictionary,
+
+ // The context to inspect identified by this descriptor
+ descriptorFront,
+
+ // The front for the Watcher Actor, related to the given descriptor
+ // This is a key actor to watch for targets and resources and pull global actors running in the parent process
+ watcherFront,
+
+ // From here, we could pass DevToolsClient, or any useful protocol classes...
+ // so that we abstract where and how to fetch all necessary interfaces
+ // and avoid having to know that you might pull the client via descriptorFront.client
+ });
+ allInstantiatedCommands.add(command);
+ return command;
+ });
+ }
+
+ return dictionary;
+}
+exports.createCommandsDictionary = createCommandsDictionary;
diff --git a/devtools/shared/commands/inspected-window/inspected-window-command.js b/devtools/shared/commands/inspected-window/inspected-window-command.js
new file mode 100644
index 0000000000..0d15016ebd
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/inspected-window-command.js
@@ -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/. */
+
+"use strict";
+
+const {
+ getAdHocFrontOrPrimitiveGrip,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/fronts/object.js");
+
+/**
+ * For now, this class is mostly a wrapper around webExtInspectedWindow actor.
+ */
+class InspectedWindowCommand {
+ constructor({ commands }) {
+ this.commands = commands;
+ }
+
+ /**
+ * Return a promise that resolves to the related target actor's front.
+ * The Web Extension inspected window actor.
+ *
+ * @return {Promise<WebExtensionInspectedWindowFront>}
+ */
+ getFront() {
+ return this.commands.targetCommand.targetFront.getFront(
+ "webExtensionInspectedWindow"
+ );
+ }
+
+ /**
+ * Evaluate the provided javascript code in a target window.
+ *
+ * @param {Object} webExtensionCallerInfo - The addonId and the url (the addon base url
+ * or the url of the actual caller filename and lineNumber) used to log useful
+ * debugging information in the produced error logs and eval stack trace.
+ * @param {String} expression - The expression to evaluate.
+ * @param {Object} options - An option object. Check the actor method definition to see
+ * what properties it can hold (minus the `consoleFront` property which is defined
+ * below).
+ * @param {WebConsoleFront} options.consoleFront - An optional webconsole front. When
+ * set, the result will be either a primitive, a LongStringFront or an
+ * ObjectFront, and the WebConsoleActor corresponding to the console front will
+ * be used to generate those, which is needed if we want to handle ObjectFronts
+ * on the client.
+ */
+ async eval(webExtensionCallerInfo, expression, options = {}) {
+ const { consoleFront } = options;
+
+ if (consoleFront) {
+ options.evalResultAsGrip = true;
+ options.toolboxConsoleActorID = consoleFront.actor;
+ delete options.consoleFront;
+ }
+
+ const front = await this.getFront();
+ const response = await front.eval(
+ webExtensionCallerInfo,
+ expression,
+ options
+ );
+
+ // If no consoleFront was provided, we can directly return the response.
+ if (!consoleFront) {
+ return response;
+ }
+
+ if (
+ !response.hasOwnProperty("exceptionInfo") &&
+ !response.hasOwnProperty("valueGrip")
+ ) {
+ throw new Error(
+ "Response does not have `exceptionInfo` or `valueGrip` property"
+ );
+ }
+
+ if (response.exceptionInfo) {
+ console.error(
+ response.exceptionInfo.description,
+ ...(response.exceptionInfo.details || [])
+ );
+ return response;
+ }
+
+ // On the server, the valueGrip is created from the toolbox webconsole actor.
+ // If we want since the ObjectFront connection is inherited from the parent front, we
+ // need to set the console front as the parent front.
+ return getAdHocFrontOrPrimitiveGrip(
+ response.valueGrip,
+ consoleFront || this
+ );
+ }
+
+ /**
+ * Reload the target tab, optionally bypass cache, customize the userAgent and/or
+ * inject a script in targeted document or any of its sub-frame.
+ *
+ * @param {WebExtensionCallerInfo} callerInfo
+ * the addonId and the url (the addon base url or the url of the actual caller
+ * filename and lineNumber) used to log useful debugging information in the
+ * produced error logs and eval stack trace.
+ * @param {Object} options
+ * @param {boolean|undefined} options.ignoreCache
+ * Enable/disable the cache bypass headers.
+ * @param {string|undefined} options.injectedScript
+ * Evaluate the provided javascript code in the top level and every sub-frame
+ * created during the page reload, before any other script in the page has been
+ * executed.
+ * @param {string|undefined} options.userAgent
+ * Customize the userAgent during the page reload.
+ * @returns {Promise} A promise that resolves once the page is done loading when userAgent
+ * or injectedScript option are passed. If those options are not provided, the
+ * Promise will resolve after the reload was initiated.
+ */
+ async reload(callerInfo, options = {}) {
+ if (this._reloadPending) {
+ return null;
+ }
+
+ this._reloadPending = true;
+
+ try {
+ // We always want to update the target configuration to set the user agent if one is
+ // passed, or to reset a potential existing override if userAgent isn't defined.
+ await this.commands.targetConfigurationCommand.updateConfiguration({
+ customUserAgent: options.userAgent,
+ });
+
+ const front = await this.getFront();
+ const result = await front.reload(callerInfo, options);
+ this._reloadPending = false;
+
+ return result;
+ } catch (e) {
+ this._reloadPending = false;
+ console.error(e);
+ return Promise.reject({
+ message: "An unexpected error occurred",
+ });
+ }
+ }
+}
+
+module.exports = InspectedWindowCommand;
diff --git a/devtools/shared/commands/inspected-window/moz.build b/devtools/shared/commands/inspected-window/moz.build
new file mode 100644
index 0000000000..69e72048f8
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/moz.build
@@ -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/.
+
+DevToolsModules(
+ "inspected-window-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/inspected-window/tests/browser.toml b/devtools/shared/commands/inspected-window/tests/browser.toml
new file mode 100644
index 0000000000..4c700ff7fe
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/browser.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+ "head.js",
+ "inspectedwindow-reload-target.sjs",
+]
+prefs = [
+ # restrictedDomains must be set as early as possible, before the first use of
+ # the preference. browser_webextension_inspected_window_access.js relies on
+ # this pref to be set. We cannot use "prefs =" at the individual file, because
+ # another test in this manifest may already have resulted in browser startup.
+ "extensions.webextensions.restrictedDomains=test2.example.com"
+]
+
+["browser_webextension_inspected_window.js"]
+["browser_webextension_inspected_window_access.js"]
diff --git a/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js
new file mode 100644
index 0000000000..bf2b752e4d
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js
@@ -0,0 +1,523 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_RELOAD_URL = `${URL_ROOT_SSL}/inspectedwindow-reload-target.sjs`;
+
+async function setup(pageUrl) {
+ // Disable bfcache for Fission for now.
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // This is just an empty extension used to ensure that the caller extension uuid
+ // actually exists.
+ },
+ });
+
+ await extension.startup();
+
+ const fakeExtCallerInfo = {
+ url: WebExtensionPolicy.getByID(extension.id).getURL(
+ "fake-caller-script.js"
+ ),
+ lineNumber: 1,
+ addonId: extension.id,
+ };
+
+ const tab = await addTab(pageUrl);
+
+ const commands = await CommandsFactory.forTab(tab, { isWebExtension: true });
+ await commands.targetCommand.startListening();
+
+ const webConsoleFront = await commands.targetCommand.targetFront.getFront(
+ "console"
+ );
+
+ return {
+ webConsoleFront,
+ commands,
+ extension,
+ fakeExtCallerInfo,
+ };
+}
+
+async function teardown({ commands, extension }) {
+ await commands.destroy();
+ gBrowser.removeCurrentTab();
+ await extension.unload();
+}
+
+function waitForNextTabNavigated(commands) {
+ const target = commands.targetCommand.targetFront;
+ return new Promise(resolve => {
+ target.on("tabNavigated", function tabNavigatedListener(pkt) {
+ if (pkt.state == "stop" && !pkt.isFrameSwitching) {
+ target.off("tabNavigated", tabNavigatedListener);
+ resolve();
+ }
+ });
+ });
+}
+
+// Script used as the injectedScript option in the inspectedWindow.reload tests.
+function injectedScript() {
+ if (!window.pageScriptExecutedFirst) {
+ window.addEventListener(
+ "DOMContentLoaded",
+ function () {
+ if (document.querySelector("pre")) {
+ document.querySelector("pre").textContent =
+ "injected script executed first";
+ }
+ },
+ { once: true }
+ );
+ }
+}
+
+// Script evaluated in the target tab, to collect the results of injectedScript
+// evaluation in the inspectedWindow.reload tests.
+function collectEvalResults() {
+ const results = [];
+ let iframeDoc = document;
+
+ while (iframeDoc) {
+ if (iframeDoc.querySelector("pre")) {
+ results.push(iframeDoc.querySelector("pre").textContent);
+ }
+ const iframe = iframeDoc.querySelector("iframe");
+ iframeDoc = iframe ? iframe.contentDocument : null;
+ }
+ return JSON.stringify(results);
+}
+
+add_task(async function test_successfull_inspectedWindowEval_result() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL);
+
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window.location",
+ {}
+ );
+
+ ok(result.value, "Got a result from inspectedWindow eval");
+ is(
+ result.value.href,
+ URL_ROOT_SSL,
+ "Got the expected window.location.href property value"
+ );
+ is(
+ result.value.protocol,
+ "https:",
+ "Got the expected window.location.protocol property value"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_successfull_inspectedWindowEval_resultAsGrip() {
+ const { commands, extension, fakeExtCallerInfo, webConsoleFront } =
+ await setup(URL_ROOT_SSL);
+
+ let result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window",
+ {
+ evalResultAsGrip: true,
+ toolboxConsoleActorID: webConsoleFront.actor,
+ }
+ );
+
+ ok(result.valueGrip, "Got a result from inspectedWindow eval");
+ ok(result.valueGrip.actor, "Got a object actor as expected");
+ is(result.valueGrip.type, "object", "Got a value grip of type object");
+ is(
+ result.valueGrip.class,
+ "Window",
+ "Got a value grip which is instanceof Location"
+ );
+
+ // Test invalid evalResultAsGrip request.
+ result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window",
+ {
+ evalResultAsGrip: true,
+ }
+ );
+
+ ok(
+ !result.value && !result.valueGrip,
+ "Got a null result from the invalid inspectedWindow eval call"
+ );
+ ok(
+ result.exceptionInfo.isError,
+ "Got an API Error result from inspectedWindow eval"
+ );
+ ok(
+ !result.exceptionInfo.isException,
+ "An error isException is false as expected"
+ );
+ is(
+ result.exceptionInfo.code,
+ "E_PROTOCOLERROR",
+ "Got the expected 'code' property in the error result"
+ );
+ is(
+ result.exceptionInfo.description,
+ "Inspector protocol error: %s - %s",
+ "Got the expected 'description' property in the error result"
+ );
+ is(
+ result.exceptionInfo.details.length,
+ 2,
+ "The 'details' array property should contains 1 element"
+ );
+ is(
+ result.exceptionInfo.details[0],
+ "Unexpected invalid sidebar panel expression request",
+ "Got the expected content in the error results's details"
+ );
+ is(
+ result.exceptionInfo.details[1],
+ "missing toolboxConsoleActorID",
+ "Got the expected content in the error results's details"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_error_inspectedWindowEval_result() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL);
+
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window",
+ {}
+ );
+
+ ok(!result.value, "Got a null result from inspectedWindow eval");
+ ok(
+ result.exceptionInfo.isError,
+ "Got an API Error result from inspectedWindow eval"
+ );
+ ok(
+ !result.exceptionInfo.isException,
+ "An error isException is false as expected"
+ );
+ is(
+ result.exceptionInfo.code,
+ "E_PROTOCOLERROR",
+ "Got the expected 'code' property in the error result"
+ );
+ is(
+ result.exceptionInfo.description,
+ "Inspector protocol error: %s",
+ "Got the expected 'description' property in the error result"
+ );
+ is(
+ result.exceptionInfo.details.length,
+ 1,
+ "The 'details' array property should contains 1 element"
+ );
+ ok(
+ result.exceptionInfo.details[0].includes("cyclic object value"),
+ "Got the expected content in the error results's details"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowEval_result() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL);
+
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "throw Error('fake eval error');",
+ {}
+ );
+
+ ok(result.exceptionInfo.isException, "Got an exception as expected");
+ ok(!result.value, "Got an undefined eval value");
+ ok(!result.exceptionInfo.isError, "An exception should not be isError=true");
+ ok(
+ result.exceptionInfo.value.includes("Error: fake eval error"),
+ "Got the expected exception message"
+ );
+
+ const expectedCallerInfo = `called from ${fakeExtCallerInfo.url}:${fakeExtCallerInfo.lineNumber}`;
+ ok(
+ result.exceptionInfo.value.includes(expectedCallerInfo),
+ "Got the expected caller info in the exception message"
+ );
+
+ const expectedStack = `eval code:1:7`;
+ ok(
+ result.exceptionInfo.value.includes(expectedStack),
+ "Got the expected stack trace in the exception message"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=cache`
+ );
+
+ // Test reload with bypassCache=false.
+
+ const waitForNoBypassCacheReload = waitForNextTabNavigated(commands);
+ const reloadResult = await commands.inspectedWindowCommand.reload(
+ fakeExtCallerInfo,
+ {
+ ignoreCache: false,
+ }
+ );
+
+ ok(
+ !reloadResult,
+ "Got the expected undefined result from inspectedWindow reload"
+ );
+
+ await waitForNoBypassCacheReload;
+
+ const noBypassCacheEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ noBypassCacheEval.result,
+ "empty cache headers",
+ "Got the expected result with reload forceBypassCache=false"
+ );
+
+ // Test reload with bypassCache=true.
+
+ const waitForForceBypassCacheReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ ignoreCache: true,
+ });
+
+ await waitForForceBypassCacheReload;
+
+ const forceBypassCacheEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ forceBypassCacheEval.result,
+ "no-cache:no-cache",
+ "Got the expected result with reload forceBypassCache=true"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_customUserAgent() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=user-agent`
+ );
+
+ // Test reload with custom userAgent.
+
+ const waitForCustomUserAgentReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ userAgent: "Customized User Agent",
+ });
+
+ await waitForCustomUserAgentReload;
+
+ const customUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ customUserAgentEval.result,
+ "Customized User Agent",
+ "Got the expected result on reload with a customized userAgent"
+ );
+
+ // Test reload with no custom userAgent.
+
+ const waitForNoCustomUserAgentReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+
+ await waitForNoCustomUserAgentReload;
+
+ const noCustomUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ noCustomUserAgentEval.result,
+ window.navigator.userAgent,
+ "Got the expected result with reload without a customized userAgent"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_injectedScript() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=injected-script&frames=3`
+ );
+
+ // Test reload with an injectedScript.
+
+ const waitForInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ injectedScript: `new ${injectedScript}`,
+ });
+ await waitForInjectedScriptReload;
+
+ const injectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ const expectedResult = new Array(5).fill("injected script executed first");
+
+ SimpleTest.isDeeply(
+ JSON.parse(injectedScriptEval.result),
+ expectedResult,
+ "Got the expected result on reload with an injected script"
+ );
+
+ // Test reload without an injectedScript.
+
+ const waitForNoInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+ await waitForNoInjectedScriptReload;
+
+ const noInjectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ const newExpectedResult = new Array(5).fill("injected script NOT executed");
+
+ SimpleTest.isDeeply(
+ JSON.parse(noInjectedScriptEval.result),
+ newExpectedResult,
+ "Got the expected result on reload with no injected script"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_multiple_calls() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=user-agent`
+ );
+
+ // Test reload with custom userAgent three times (and then
+ // check that only the first one has affected the page reload.
+
+ const waitForCustomUserAgentReload = waitForNextTabNavigated(commands);
+
+ commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ userAgent: "Customized User Agent 1",
+ });
+ commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ userAgent: "Customized User Agent 2",
+ });
+
+ await waitForCustomUserAgentReload;
+
+ const customUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ customUserAgentEval.result,
+ "Customized User Agent 1",
+ "Got the expected result on reload with a customized userAgent"
+ );
+
+ // Test reload with no custom userAgent.
+
+ const waitForNoCustomUserAgentReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+
+ await waitForNoCustomUserAgentReload;
+
+ const noCustomUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ noCustomUserAgentEval.result,
+ window.navigator.userAgent,
+ "Got the expected result with reload without a customized userAgent"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_stopped() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=injected-script&frames=3`
+ );
+
+ // Test reload on a page that calls window.stop() immediately during the page loading
+
+ const waitForPageLoad = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window.location += '&stop=windowStop'"
+ );
+
+ info("Load a webpage that calls 'window.stop()' while is still loading");
+ await waitForPageLoad;
+
+ info("Starting a reload with an injectedScript");
+ const waitForInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ injectedScript: `new ${injectedScript}`,
+ });
+ await waitForInjectedScriptReload;
+
+ const injectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ // The page should have stopped during the reload and only one injected script
+ // is expected.
+ const expectedResult = new Array(1).fill("injected script executed first");
+
+ SimpleTest.isDeeply(
+ JSON.parse(injectedScriptEval.result),
+ expectedResult,
+ "The injected script has been executed on the 'stopped' page reload"
+ );
+
+ // Reload again with no options.
+
+ info("Reload the tab again without any reload options");
+ const waitForNoInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+ await waitForNoInjectedScriptReload;
+
+ const noInjectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ // The page should have stopped during the reload and no injected script should
+ // have been executed during this second reload (or it would mean that the previous
+ // customized reload was still pending and has wrongly affected the second reload)
+ const newExpectedResult = new Array(1).fill("injected script NOT executed");
+
+ SimpleTest.isDeeply(
+ JSON.parse(noInjectedScriptEval.result),
+ newExpectedResult,
+ "No injectedScript should have been evaluated during the second reload"
+ );
+
+ await teardown({ commands, extension });
+});
+
+// TODO: check eval with $0 binding once implemented (Bug 1300590)
diff --git a/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js
new file mode 100644
index 0000000000..3b32bb0aaa
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function run_inspectedWindow_eval({ tab, codeToEval, extension }) {
+ const fakeExtCallerInfo = {
+ url: `moz-extension://${extension.uuid}/another/fake-caller-script.js`,
+ lineNumber: 1,
+ addonId: extension.id,
+ };
+ const commands = await CommandsFactory.forTab(tab, { isWebExtension: true });
+ await commands.targetCommand.startListening();
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ codeToEval,
+ {}
+ );
+ await commands.destroy();
+ return result;
+}
+
+async function openAboutBlankTabWithExtensionOrigin(extension) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `moz-extension://${extension.uuid}/manifest.json`
+ );
+ const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ // about:blank inherits the principal when opened from content.
+ content.wrappedJSObject.location.assign("about:blank");
+ });
+ await loaded;
+ // Sanity checks:
+ is(tab.linkedBrowser.currentURI.spec, "about:blank", "expected tab");
+ is(
+ tab.linkedBrowser.contentPrincipal.originNoSuffix,
+ `moz-extension://${extension.uuid}`,
+ "about:blank should be at the extension origin"
+ );
+ return tab;
+}
+
+async function checkEvalResult({
+ extension,
+ description,
+ url,
+ createTab = () => BrowserTestUtils.openNewForegroundTab(gBrowser, url),
+ expectedResult,
+}) {
+ const tab = await createTab();
+ is(tab.linkedBrowser.currentURI.spec, url, "Sanity check: tab URL");
+ const result = await run_inspectedWindow_eval({
+ tab,
+ codeToEval: "'code executed at ' + location.href",
+ extension,
+ });
+ BrowserTestUtils.removeTab(tab);
+ SimpleTest.isDeeply(
+ result,
+ expectedResult,
+ `eval result for devtools.inspectedWindow.eval at ${url} (${description})`
+ );
+}
+
+async function checkEvalAllowed({ extension, description, url, createTab }) {
+ info(`checkEvalAllowed: ${description} (at URL: ${url})`);
+ await checkEvalResult({
+ extension,
+ description,
+ url,
+ createTab,
+ expectedResult: { value: `code executed at ${url}` },
+ });
+}
+async function checkEvalDenied({ extension, description, url, createTab }) {
+ info(`checkEvalDenied: ${description} (at URL: ${url})`);
+ await checkEvalResult({
+ extension,
+ description,
+ url,
+ createTab,
+ expectedResult: {
+ exceptionInfo: {
+ isError: true,
+ code: "E_PROTOCOLERROR",
+ details: [
+ "This extension is not allowed on the current inspected window origin",
+ ],
+ description: "Inspector protocol error: %s",
+ },
+ },
+ });
+}
+
+add_task(async function test_eval_at_http() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const httpUrl = "http://example.com/";
+
+ // When running with --use-http3-server, http:-URLs cannot be loaded.
+ try {
+ await fetch(httpUrl);
+ } catch {
+ info("Skipping test_eval_at_http because http:-URL cannot be loaded");
+ return;
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ await checkEvalAllowed({
+ extension,
+ description: "http:-URL",
+ url: httpUrl,
+ });
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_eval_at_https() {
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ const privilegedExtension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ });
+ await privilegedExtension.startup();
+
+ await checkEvalAllowed({
+ extension,
+ description: "https:-URL",
+ url: "https://example.com/",
+ });
+
+ await checkEvalDenied({
+ extension,
+ description: "a restricted domain",
+ // Domain in extensions.webextensions.restrictedDomains by browser.toml.
+ url: "https://test2.example.com/",
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.quarantinedDomains.list", "example.com"]],
+ });
+
+ await checkEvalDenied({
+ extension,
+ description: "a quarantined domain",
+ url: "https://example.com/",
+ });
+
+ await checkEvalAllowed({
+ extension: privilegedExtension,
+ description: "a quarantined domain",
+ url: "https://example.com/",
+ });
+
+ await SpecialPowers.popPrefEnv();
+
+ await extension.unload();
+ await privilegedExtension.unload();
+});
+
+add_task(async function test_eval_at_sandboxed_page() {
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ await checkEvalAllowed({
+ extension,
+ description: "page with CSP sandbox",
+ url: "https://example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x",
+ });
+ await checkEvalDenied({
+ extension,
+ description: "restricted domain with CSP sandbox",
+ url: "https://test2.example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x",
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_own_extension_origin_allowed() {
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage(
+ "blob_url",
+ URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }]))
+ );
+ },
+ files: {
+ "mozext.html": `<!DOCTYPE html>moz-extension: here`,
+ },
+ });
+ await extension.startup();
+ const blobUrl = await extension.awaitMessage("blob_url");
+
+ await checkEvalAllowed({
+ extension,
+ description: "moz-extension:-URL from own extension",
+ url: `moz-extension://${extension.uuid}/mozext.html`,
+ });
+ await checkEvalAllowed({
+ extension,
+ description: "blob:-URL from own extension",
+ url: blobUrl,
+ });
+ await checkEvalAllowed({
+ extension,
+ description: "about:blank with origin from own extension",
+ url: "about:blank",
+ createTab: () => openAboutBlankTabWithExtensionOrigin(extension),
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_other_extension_denied() {
+ // The extension for which we simulate devtools_page, chosen as caller of
+ // devtools.inspectedWindow.eval API calls.
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ // The other extension, that |extension| should not be able to access:
+ const otherExt = ExtensionTestUtils.loadExtension({
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage(
+ "blob_url",
+ URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }]))
+ );
+ },
+ files: {
+ "mozext.html": `<!DOCTYPE html>moz-extension: here`,
+ },
+ });
+ await otherExt.startup();
+ const otherExtBlobUrl = await otherExt.awaitMessage("blob_url");
+
+ await checkEvalDenied({
+ extension,
+ description: "moz-extension:-URL from another extension",
+ url: `moz-extension://${otherExt.uuid}/mozext.html`,
+ });
+ await checkEvalDenied({
+ extension,
+ description: "blob:-URL from another extension",
+ url: otherExtBlobUrl,
+ });
+ await checkEvalDenied({
+ extension,
+ description: "about:blank with origin from another extension",
+ url: "about:blank",
+ createTab: () => openAboutBlankTabWithExtensionOrigin(otherExt),
+ });
+
+ await otherExt.unload();
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_about() {
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+ await checkEvalAllowed({
+ extension,
+ description: "about:blank (null principal)",
+ url: "about:blank",
+ });
+ await checkEvalDenied({
+ extension,
+ description: "about:addons (system principal)",
+ url: "about:addons",
+ });
+ await checkEvalDenied({
+ extension,
+ description: "about:robots (about page)",
+ url: "about:robots",
+ });
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_file() {
+ // FYI: There is also an equivalent test case with a full end-to-end test at:
+ // browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js
+
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ // A dummy file URL that can be loaded in a tab.
+ const fileUrl =
+ "file://" +
+ getTestFilePath("browser_webextension_inspected_window_access.js");
+
+ // checkEvalAllowed test helper cannot be used, because the file:-URL may
+ // redirect elsewhere, so the comparison with the full URL fails.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, fileUrl);
+ const result = await run_inspectedWindow_eval({
+ tab,
+ codeToEval: "'code executed at ' + location.protocol",
+ extension,
+ });
+ BrowserTestUtils.removeTab(tab);
+ SimpleTest.isDeeply(
+ result,
+ { value: "code executed at file:" },
+ `eval result for devtools.inspectedWindow.eval at ${fileUrl}`
+ );
+
+ await extension.unload();
+});
diff --git a/devtools/shared/commands/inspected-window/tests/head.js b/devtools/shared/commands/inspected-window/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/head.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs b/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs
new file mode 100644
index 0000000000..4e737ad207
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs
@@ -0,0 +1,87 @@
+"use strict";
+
+function handleRequest(request, response) {
+ const params = new URLSearchParams(request.queryString);
+
+ switch (params.get("test")) {
+ case "cache":
+ handleCacheTestRequest(request, response);
+ break;
+
+ case "user-agent":
+ handleUserAgentTestRequest(request, response);
+ break;
+
+ case "injected-script":
+ handleInjectedScriptTestRequest(request, response, params);
+ break;
+ }
+}
+
+function handleCacheTestRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+
+ if (request.hasHeader("pragma") && request.hasHeader("cache-control")) {
+ response.write(
+ `${request.getHeader("pragma")}:${request.getHeader("cache-control")}`
+ );
+ } else {
+ response.write("empty cache headers");
+ }
+}
+
+function handleUserAgentTestRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+
+ if (request.hasHeader("user-agent")) {
+ response.write(request.getHeader("user-agent"));
+ } else {
+ response.write("no user agent header");
+ }
+}
+
+function handleInjectedScriptTestRequest(request, response, params) {
+ response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+
+ const frames = parseInt(params.get("frames"), 10);
+ let content = "";
+
+ if (frames > 0) {
+ // Output an iframe in seamless mode, so that there is an higher chance that in case
+ // of test failures we get a screenshot where the nested iframes are all visible.
+ content = `<iframe seamless src="?test=injected-script&frames=${
+ frames - 1
+ }"></iframe>`;
+ } else {
+ // Output an about:srcdoc frame to be sure that inspectedWindow.eval is able to
+ // evaluate js code into it.
+ const srcdoc = `
+ <pre>injected script NOT executed</pre>
+ <script>window.pageScriptExecutedFirst = true</script>
+ `;
+ content = `<iframe style="height: 30px;" srcdoc="${srcdoc}"></iframe>`;
+ }
+
+ if (params.get("stop") == "windowStop") {
+ content = "<script>window.stop();</script>" + content;
+ }
+
+ response.write(`<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ iframe { width: 100%; height: ${frames * 150}px; }
+ </style>
+ </head>
+ <body>
+ <h1>IFRAME ${frames}</h1>
+ <pre>injected script NOT executed</pre>
+ <script>
+ window.pageScriptExecutedFirst = true;
+ </script>
+ ${content}
+ </body>
+ </html>
+ `);
+}
diff --git a/devtools/shared/commands/inspector/inspector-command.js b/devtools/shared/commands/inspector/inspector-command.js
new file mode 100644
index 0000000000..a8c4edd6c1
--- /dev/null
+++ b/devtools/shared/commands/inspector/inspector-command.js
@@ -0,0 +1,483 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+loader.lazyRequireGetter(
+ this,
+ "getTargetBrowsers",
+ "resource://devtools/shared/compatibility/compatibility-user-settings.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "TARGET_BROWSER_PREF",
+ "resource://devtools/shared/compatibility/constants.js",
+ true
+);
+
+class InspectorCommand {
+ constructor({ commands }) {
+ this.commands = commands;
+ }
+
+ #cssDeclarationBlockIssuesQueuedDomRulesDeclarations = [];
+ #cssDeclarationBlockIssuesPendingTimeoutPromise;
+ #cssDeclarationBlockIssuesTargetBrowsersPromise;
+
+ /**
+ * Return the list of all current target's inspector fronts
+ *
+ * @return {Promise<Array<InspectorFront>>}
+ */
+ async getAllInspectorFronts() {
+ return this.commands.targetCommand.getAllFronts(
+ [this.commands.targetCommand.TYPES.FRAME],
+ "inspector"
+ );
+ }
+
+ /**
+ * Search the document for the given string and return all the results.
+ *
+ * @param {Object} walkerFront
+ * @param {String} query
+ * The string to search for.
+ * @param {Object} options
+ * {Boolean} options.reverse - search backwards
+ * @returns {Array} The list of search results
+ */
+ async walkerSearch(walkerFront, query, options = {}) {
+ const result = await walkerFront.search(query, options);
+ return result.list.items();
+ }
+
+ /**
+ * Incrementally search the top-level document and sub frames for a given string.
+ * Only one result is sent back at a time. Calling the
+ * method again with the same query will send the next result.
+ * If a new query which does not match the current one all is reset and new search
+ * is kicked off.
+ *
+ * @param {String} query
+ * The string / selector searched for
+ * @param {Object} options
+ * {Boolean} reverse - determines if the search is done backwards
+ * @returns {Object} res
+ * {String} res.type
+ * {String} res.query - The string / selector searched for
+ * {Object} res.node - the current node
+ * {Number} res.resultsIndex - The index of the current node
+ * {Number} res.resultsLength - The total number of results found.
+ */
+ async findNextNode(query, { reverse } = {}) {
+ const inspectors = await this.getAllInspectorFronts();
+ const nodes = await Promise.all(
+ inspectors.map(({ walker }) =>
+ this.walkerSearch(walker, query, { reverse })
+ )
+ );
+ const results = nodes.flat();
+
+ // If the search query changes
+ if (this._searchQuery !== query) {
+ this._searchQuery = query;
+ this._currentIndex = -1;
+ }
+
+ if (!results.length) {
+ return null;
+ }
+
+ this._currentIndex = reverse
+ ? this._currentIndex - 1
+ : this._currentIndex + 1;
+
+ if (this._currentIndex >= results.length) {
+ this._currentIndex = 0;
+ }
+ if (this._currentIndex < 0) {
+ this._currentIndex = results.length - 1;
+ }
+
+ return {
+ node: results[this._currentIndex],
+ resultsIndex: this._currentIndex,
+ resultsLength: results.length,
+ };
+ }
+
+ /**
+ * Returns a list of matching results for CSS selector autocompletion.
+ *
+ * @param {String} query
+ * The selector query being completed
+ * @param {String} firstPart
+ * The exact token being completed out of the query
+ * @param {String} state
+ * One of "pseudo", "id", "tag", "class", "null"
+ * @return {Array<string>} suggestions
+ * The list of suggested CSS selectors
+ */
+ async getSuggestionsForQuery(query, firstPart, state) {
+ // Get all inspectors where we want suggestions from.
+ const inspectors = await this.getAllInspectorFronts();
+
+ const mergedSuggestions = [];
+ // Get all of the suggestions.
+ await Promise.all(
+ inspectors.map(async ({ walker }) => {
+ const { suggestions } = await walker.getSuggestionsForQuery(
+ query,
+ firstPart,
+ state
+ );
+ for (const [suggestion, count, type] of suggestions) {
+ // Merge any already existing suggestion with the new one, by incrementing the count
+ // which is the second element of the array.
+ const existing = mergedSuggestions.find(
+ ([s, , t]) => s == suggestion && t == type
+ );
+ if (existing) {
+ existing[1] += count;
+ } else {
+ mergedSuggestions.push([suggestion, count, type]);
+ }
+ }
+ })
+ );
+
+ // Descending sort the list by count, i.e. second element of the arrays
+ return sortSuggestions(mergedSuggestions);
+ }
+
+ /**
+ * Find a nodeFront from an array of selectors. The last item of the array is the selector
+ * for the element in its owner document, and the previous items are selectors to iframes
+ * that lead to the frame where the searched node lives in.
+ *
+ * For example, with the following markup
+ * <html>
+ * <iframe id="level-1" src="…">
+ * <iframe id="level-2" src="…">
+ * <h1>Waldo</h1>
+ * </iframe>
+ * </iframe>
+ *
+ * If you want to retrieve the `<h1>` nodeFront, `selectors` would be:
+ * [
+ * "#level-1",
+ * "#level-2",
+ * "h1",
+ * ]
+ *
+ * @param {Array} selectors
+ * An array of CSS selectors to find the target accessible object.
+ * Several selectors can be needed if the element is nested in frames
+ * and not directly in the root document.
+ * @param {Integer} timeoutInMs
+ * The maximum number of ms the function should run (defaults to 5000).
+ * If it exceeds this, the returned promise will resolve with `null`.
+ * @return {Promise<NodeFront|null>} a promise that resolves when the node front is found
+ * for selection using inspector tools. It resolves with the deepest frame document
+ * that could be retrieved when the "final" nodeFront couldn't be found in the page.
+ * It resolves with `null` when the function runs for more than timeoutInMs.
+ */
+ async findNodeFrontFromSelectors(nodeSelectors, timeoutInMs = 5000) {
+ if (
+ !nodeSelectors ||
+ !Array.isArray(nodeSelectors) ||
+ nodeSelectors.length === 0
+ ) {
+ console.warn(
+ "findNodeFrontFromSelectors expect a non-empty array but got",
+ nodeSelectors
+ );
+ return null;
+ }
+
+ const { walker } = await this.commands.targetCommand.targetFront.getFront(
+ "inspector"
+ );
+ const querySelectors = async nodeFront => {
+ const selector = nodeSelectors.shift();
+ if (!selector) {
+ return nodeFront;
+ }
+ nodeFront = await nodeFront.walkerFront.querySelector(
+ nodeFront,
+ selector
+ );
+ // It's possible the containing iframe isn't available by the time
+ // walkerFront.querySelector is called, which causes the re-selected node to be
+ // unavailable. There also isn't a way for us to know when all iframes on the page
+ // have been created after a reload. Because of this, we should should bail here.
+ if (!nodeFront) {
+ return null;
+ }
+
+ if (nodeSelectors.length) {
+ if (!nodeFront.isShadowHost) {
+ await this.#waitForFrameLoad(nodeFront);
+ }
+
+ const { nodes } = await walker.children(nodeFront);
+
+ // If there are remaining selectors to process, they will target a document or a
+ // document-fragment under the current node. Whether the element is a frame or
+ // a web component, it can only contain one document/document-fragment, so just
+ // select the first one available.
+ nodeFront = nodes.find(node => {
+ const { nodeType } = node;
+ return (
+ nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
+ nodeType === Node.DOCUMENT_NODE
+ );
+ });
+
+ // The iframe selector might have matched an element which is not an
+ // iframe in the new page (or an iframe with no document?). In this
+ // case, bail out and fallback to the root body element.
+ if (!nodeFront) {
+ return null;
+ }
+ }
+ const childrenNodeFront = await querySelectors(nodeFront);
+ return childrenNodeFront || nodeFront;
+ };
+ const rootNodeFront = await walker.getRootNode();
+
+ // Since this is only used for re-setting a selection after a page reloads, we can
+ // put a timeout, in case there's an iframe that would take too much time to load,
+ // and prevent the markup view to be populated.
+ const onTimeout = new Promise(res => setTimeout(res, timeoutInMs)).then(
+ () => null
+ );
+ const onQuerySelectors = querySelectors(rootNodeFront);
+ return Promise.race([onTimeout, onQuerySelectors]);
+ }
+
+ /**
+ * Wait for the given NodeFront child document to be loaded.
+ *
+ * @param {NodeFront} A nodeFront representing a frame
+ */
+ async #waitForFrameLoad(nodeFront) {
+ const domLoadingPromises = [];
+
+ // if the flag isn't true, we don't know for sure if the iframe will be remote
+ // or not; when the nodeFront was created, the iframe might still have been loading
+ // and in such case, its associated window can be an initial document.
+ // Luckily, once EFT is enabled everywhere we can remove this call and only wait
+ // for the associated target.
+ if (!nodeFront.useChildTargetToFetchChildren) {
+ domLoadingPromises.push(nodeFront.waitForFrameLoad());
+ }
+
+ const { onResource: onDomInteractiveResource } =
+ await this.commands.resourceCommand.waitForNextResource(
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
+ {
+ // We might be in a case where the children document is already loaded (i.e. we
+ // would already have received the dom-interactive resource), so it's important
+ // to _not_ ignore existing resource.
+ predicate: resource =>
+ resource.name == "dom-interactive" &&
+ resource.targetFront !== nodeFront.targetFront &&
+ resource.targetFront.browsingContextID ==
+ nodeFront.browsingContextID,
+ }
+ );
+ const newTargetResolveValue = Symbol();
+ domLoadingPromises.push(
+ onDomInteractiveResource.then(() => newTargetResolveValue)
+ );
+
+ // Here we wait for any promise to resolve first. `waitForFrameLoad` might throw
+ // (if the iframe does end up being remote), so we don't want to use `Promise.race`.
+ const loadResult = await Promise.any(domLoadingPromises);
+
+ // The Node may have `useChildTargetToFetchChildren` set to false because the
+ // child document was still loading when fetching its form. But it may happen that
+ // the Node ends up being a remote iframe.
+ // When this happen we will try to call `waitForFrameLoad` which will throw, but
+ // we will be notified about the new target.
+ // This is the special edge case we are trying to handle here.
+ // We want WalkerFront.children to consider this as an iframe with a dedicated target.
+ if (loadResult == newTargetResolveValue) {
+ nodeFront._form.useChildTargetToFetchChildren = true;
+ }
+ }
+
+ /**
+ * Get the full array of selectors from the topmost document, going through
+ * iframes.
+ * For example, given the following markup:
+ *
+ * <html>
+ * <body>
+ * <iframe src="...">
+ * <html>
+ * <body>
+ * <h1 id="sub-document-title">Title of sub document</h1>
+ * </body>
+ * </html>
+ * </iframe>
+ * </body>
+ * </html>
+ *
+ * If this function is called with the NodeFront for the h1#sub-document-title element,
+ * it will return something like: ["body > iframe", "#sub-document-title"]
+ *
+ * @param {NodeFront} nodeFront: The nodefront to get the selectors for
+ * @returns {Promise<Array<String>>} A promise that resolves with an array of selectors (strings)
+ */
+ async getNodeFrontSelectorsFromTopDocument(nodeFront) {
+ const selectors = [];
+
+ let currentNode = nodeFront;
+ while (currentNode) {
+ // Get the selector for the node inside its document
+ const selector = await currentNode.getUniqueSelector();
+ selectors.unshift(selector);
+
+ // Retrieve the node's document/shadowRoot nodeFront so we can get its parent
+ // (so if we're in an iframe, we'll get the <iframe> node front, and if we're in a
+ // shadow dom document, we'll get the host).
+ const rootNode = currentNode.getOwnerRootNodeFront();
+ currentNode = rootNode?.parentOrHost();
+ }
+
+ return selectors;
+ }
+
+ #updateTargetBrowsersCache = async () => {
+ this.#cssDeclarationBlockIssuesTargetBrowsersPromise = getTargetBrowsers();
+ };
+
+ /**
+ * Get compatibility issues for given domRule declarations
+ *
+ * @param {Array<Object>} domRuleDeclarations
+ * @param {string} domRuleDeclarations[].name: Declaration name
+ * @param {string} domRuleDeclarations[].value: Declaration value
+ * @returns {Promise<Array<Object>>}
+ */
+ async getCSSDeclarationBlockIssues(domRuleDeclarations) {
+ const resultIndex =
+ this.#cssDeclarationBlockIssuesQueuedDomRulesDeclarations.length;
+ this.#cssDeclarationBlockIssuesQueuedDomRulesDeclarations.push(
+ domRuleDeclarations
+ );
+
+ // We're getting the target browsers from RemoteSettings, which can take some time.
+ // We cache the target browsers to avoid bad performance.
+ if (!this.#cssDeclarationBlockIssuesTargetBrowsersPromise) {
+ this.#updateTargetBrowsersCache();
+ // Update the target browsers cache when the pref in which we store the compat
+ // panel settings is updated.
+ Services.prefs.addObserver(
+ TARGET_BROWSER_PREF,
+ this.#updateTargetBrowsersCache
+ );
+ }
+
+ // This can be a hot path if the rules view has a lot of rules displayed.
+ // Here we wait before sending the RDP request so we can collect all the domRule declarations
+ // of "concurrent" calls, and only send a single RDP request.
+ if (!this.#cssDeclarationBlockIssuesPendingTimeoutPromise) {
+ // Wait before sending the RDP request so all "concurrent" calls can be handle
+ // in a single RDP request.
+ this.#cssDeclarationBlockIssuesPendingTimeoutPromise = new Promise(
+ resolve => {
+ setTimeout(() => {
+ this.#cssDeclarationBlockIssuesPendingTimeoutPromise = null;
+ this.#batchedGetCSSDeclarationBlockIssues().then(data =>
+ resolve(data)
+ );
+ }, 50);
+ }
+ );
+ }
+
+ const results = await this.#cssDeclarationBlockIssuesPendingTimeoutPromise;
+ return results?.[resultIndex] || [];
+ }
+
+ /**
+ * Get compatibility issues for all queued domRules declarations
+ * @returns {Promise<Array<Array<Object>>>}
+ */
+ #batchedGetCSSDeclarationBlockIssues = async () => {
+ const declarations = Array.from(
+ this.#cssDeclarationBlockIssuesQueuedDomRulesDeclarations
+ );
+ this.#cssDeclarationBlockIssuesQueuedDomRulesDeclarations = [];
+
+ const { targetFront } = this.commands.targetCommand;
+ try {
+ // The server method isn't dependent on the target (it computes the values from the
+ // declarations we send, which are just property names and values), so we can always
+ // use the top-level target front.
+ const inspectorFront = await targetFront.getFront("inspector");
+
+ const [compatibilityFront, targetBrowsers] = await Promise.all([
+ inspectorFront.getCompatibilityFront(),
+ this.#cssDeclarationBlockIssuesTargetBrowsersPromise,
+ ]);
+
+ const data = await compatibilityFront.getCSSDeclarationBlockIssues(
+ declarations,
+ targetBrowsers
+ );
+ return data;
+ } catch (e) {
+ if (this.destroyed || targetFront.isDestroyed()) {
+ return [];
+ }
+ throw e;
+ }
+ };
+
+ destroy() {
+ Services.prefs.removeObserver(
+ TARGET_BROWSER_PREF,
+ this.#updateTargetBrowsersCache
+ );
+ this.destroyed = true;
+ }
+}
+
+// This is a fork of the server sort:
+// https://searchfox.org/mozilla-central/rev/46a67b8656ac12b5c180e47bc4055f713d73983b/devtools/server/actors/inspector/walker.js#1447
+function sortSuggestions(suggestions) {
+ const sorted = suggestions.sort((a, b) => {
+ // Computed a sortable string with first the inverted count, then the name
+ let sortA = 10000 - a[1] + a[0];
+ let sortB = 10000 - b[1] + b[0];
+
+ // Prefixing ids, classes and tags, to group results
+ const firstA = a[0].substring(0, 1);
+ const firstB = b[0].substring(0, 1);
+
+ const getSortKeyPrefix = firstLetter => {
+ if (firstLetter === "#") {
+ return "2";
+ }
+ if (firstLetter === ".") {
+ return "1";
+ }
+ return "0";
+ };
+
+ sortA = getSortKeyPrefix(firstA) + sortA;
+ sortB = getSortKeyPrefix(firstB) + sortB;
+
+ // String compare
+ return sortA.localeCompare(sortB);
+ });
+ return sorted.slice(0, 25);
+}
+
+module.exports = InspectorCommand;
diff --git a/devtools/shared/commands/inspector/moz.build b/devtools/shared/commands/inspector/moz.build
new file mode 100644
index 0000000000..1f92b4d9d0
--- /dev/null
+++ b/devtools/shared/commands/inspector/moz.build
@@ -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/.
+
+DevToolsModules(
+ "inspector-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/inspector/tests/browser.toml b/devtools/shared/commands/inspector/tests/browser.toml
new file mode 100644
index 0000000000..d068db90ac
--- /dev/null
+++ b/devtools/shared/commands/inspector/tests/browser.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/inspector/test/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+ "head.js",
+]
+
+["browser_inspector_command_findNodeFrontFromSelectors.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_inspector_command_getNodeFrontSelectorsFromTopDocument.js"]
+
+["browser_inspector_command_getSuggestionsForQuery.js"]
+
+["browser_inspector_command_search.js"]
diff --git a/devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js b/devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js
new file mode 100644
index 0000000000..7991421c8d
--- /dev/null
+++ b/devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ // Build a simple test page with a remote iframe, using two distinct origins .com and .org
+ const iframeOrgHtml = encodeURIComponent(
+ `<h2 id="in-iframe">in org - same origin</h2>`
+ );
+ const iframeComHtml = encodeURIComponent(`<h3>in com - remote</h3>`);
+ const html = encodeURIComponent(
+ `<main class="foo bar">
+ <button id="child">Click</button>
+ </main>
+ <!-- adding delay to both iframe so we can check we handle loading document has expected -->
+ <iframe id="iframe-org" src="https://example.org/document-builder.sjs?delay=3000&html=${iframeOrgHtml}"></iframe>
+ <iframe id="iframe-com" src="https://example.com/document-builder.sjs?delay=6000&html=${iframeComHtml}"></iframe>`
+ );
+ const tab = await addTab(
+ "https://example.org/document-builder.sjs?html=" + html,
+ { waitForLoad: false }
+ );
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ info("Check that it returns null when no params are passed");
+ let nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors();
+ is(
+ nodeFront,
+ null,
+ `findNodeFrontFromSelectors returns null when no param is passed`
+ );
+
+ info("Check that it returns null when a string is passed");
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors(
+ "body main"
+ );
+ is(
+ nodeFront,
+ null,
+ `findNodeFrontFromSelectors returns null when passed a string`
+ );
+
+ info("Check it returns null when an empty array is passed");
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([]);
+ is(
+ nodeFront,
+ null,
+ `findNodeFrontFromSelectors returns null when passed an empty array`
+ );
+
+ info("Check that passing a selector for a non-matching element returns null");
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([
+ "h1",
+ ]);
+ is(
+ nodeFront,
+ null,
+ "findNodeFrontFromSelectors returns null as there's no <h1> element in the page"
+ );
+
+ info("Check passing a selector for an element in the top document");
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([
+ "button",
+ ]);
+ is(
+ nodeFront.typeName,
+ "domnode",
+ "findNodeFrontFromSelectors returns a nodeFront"
+ );
+ is(
+ nodeFront.displayName,
+ "button",
+ "findNodeFrontFromSelectors returned the appropriate nodeFront"
+ );
+
+ info("Check passing a selector for an element in a same origin iframe");
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([
+ "#iframe-org",
+ "#in-iframe",
+ ]);
+ is(
+ nodeFront.displayName,
+ "h2",
+ "findNodeFrontFromSelectors returned the appropriate nodeFront"
+ );
+
+ info("Check passing a selector for an element in a cross origin iframe");
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([
+ "#iframe-com",
+ "h3",
+ ]);
+ is(
+ nodeFront.displayName,
+ "h3",
+ "findNodeFrontFromSelectors returned the appropriate nodeFront"
+ );
+
+ info(
+ "Check passing a selector for an non-existing element in an existing iframe"
+ );
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([
+ "iframe",
+ "#non-existant-id",
+ ]);
+ is(
+ nodeFront.displayName,
+ "#document",
+ "findNodeFrontFromSelectors returned the last matching iframe document if the children selector isn't found"
+ );
+ is(
+ nodeFront.parentNode().displayName,
+ "iframe",
+ "findNodeFrontFromSelectors returned the last matching iframe document if the children selector isn't found"
+ );
+
+ info("Check that timeout does work");
+ // Reload the page so we'll have the iframe loading (for 3s) and we can check that
+ // putting a smaller timeout will result in the function returning null.
+ // we need to wait until it's fully processed to avoid pending promises.
+ const onNewTargetProcessed = commands.targetCommand.once(
+ "processed-available-target"
+ );
+ await reloadBrowser({ waitForLoad: false });
+ await onNewTargetProcessed;
+ nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors(
+ ["#iframe-org", "#in-iframe"],
+ // timeout in ms (smaller than 3s)
+ 100
+ );
+ is(
+ nodeFront,
+ null,
+ "findNodeFrontFromSelectors timed out and returned null, as expected"
+ );
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/inspector/tests/browser_inspector_command_getNodeFrontSelectorsFromTopDocument.js b/devtools/shared/commands/inspector/tests/browser_inspector_command_getNodeFrontSelectorsFromTopDocument.js
new file mode 100644
index 0000000000..3e5abcddd0
--- /dev/null
+++ b/devtools/shared/commands/inspector/tests/browser_inspector_command_getNodeFrontSelectorsFromTopDocument.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing getNodeFrontSelectorsFromTopDocument
+
+add_task(async () => {
+ const html = `
+ <html>
+ <head>
+ <meta charset="utf8">
+ <title>Test</title>
+ </head>
+ <body>
+ <header>
+ <span>hello</span>
+ <span>world</span>
+ </header>
+ <main>
+ <iframe src="data:text/html,${encodeURIComponent(
+ "<html><body><h2 class='frame-child'>foo</h2></body></html>"
+ )}"></iframe>
+ </main>
+ <footer></footer>
+
+ <test-component>
+ <div slot="slot1" id="el1">content</div>
+ </test-component>
+ <script>
+ 'use strict';
+
+ customElements.define('test-component', class extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = '<slot class="slot-class" name="slot1"></slot>';
+ }
+ });
+ </script>
+ </body>
+ </html>`;
+
+ const tab = await addTab("data:text/html," + encodeURIComponent(html));
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const walker = (
+ await commands.targetCommand.targetFront.getFront("inspector")
+ ).walker;
+
+ const checkSelectors = (...args) =>
+ checkSelectorsFromTopDocumentForNode(commands, ...args);
+
+ let node = await getNodeFrontInFrames(["meta"], { walker });
+ await checkSelectors(
+ node,
+ ["head > meta:nth-child(1)"],
+ "Got expected selectors for the top-level meta node"
+ );
+
+ node = await getNodeFrontInFrames(["body"], { walker });
+ await checkSelectors(
+ node,
+ ["body"],
+ "Got expected selectors for the top-level body node"
+ );
+
+ node = await getNodeFrontInFrames(["header > span"], { walker });
+ await checkSelectors(
+ node,
+ ["body > header:nth-child(1) > span:nth-child(1)"],
+ "Got expected selectors for the top-level span node"
+ );
+
+ node = await getNodeFrontInFrames(["iframe"], { walker });
+ await checkSelectors(
+ node,
+ ["body > main:nth-child(2) > iframe:nth-child(1)"],
+ "Got expected selectors for the iframe node"
+ );
+
+ node = await getNodeFrontInFrames(["iframe", "body"], { walker });
+ await checkSelectors(
+ node,
+ ["body > main:nth-child(2) > iframe:nth-child(1)", "body"],
+ "Got expected selectors for the iframe body node"
+ );
+
+ const hostFront = await getNodeFront("test-component", { walker });
+ const { nodes } = await walker.children(hostFront);
+ const shadowRoot = nodes.find(hostNode => hostNode.isShadowRoot);
+ node = await walker.querySelector(shadowRoot, ".slot-class");
+
+ await checkSelectors(
+ node,
+ ["body > test-component:nth-child(4)", ".slot-class"],
+ "Got expected selectors for the shadow dom node"
+ );
+
+ await commands.destroy();
+});
+
+async function checkSelectorsFromTopDocumentForNode(
+ commands,
+ nodeFront,
+ expectedSelectors,
+ assertionText
+) {
+ const selectors =
+ await commands.inspectorCommand.getNodeFrontSelectorsFromTopDocument(
+ nodeFront
+ );
+ is(
+ JSON.stringify(selectors),
+ JSON.stringify(expectedSelectors),
+ assertionText
+ );
+}
diff --git a/devtools/shared/commands/inspector/tests/browser_inspector_command_getSuggestionsForQuery.js b/devtools/shared/commands/inspector/tests/browser_inspector_command_getSuggestionsForQuery.js
new file mode 100644
index 0000000000..e7b765b1d0
--- /dev/null
+++ b/devtools/shared/commands/inspector/tests/browser_inspector_command_getSuggestionsForQuery.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ // Build a test page with a remote iframe, using two distinct origins .com and .org
+ const iframeHtml = encodeURIComponent(`<div id="iframe"></div>`);
+ const html = encodeURIComponent(
+ `<div class="foo bar">
+ <div id="child"></div>
+ </div>
+ <iframe src="https://example.org/document-builder.sjs?html=${iframeHtml}"></iframe>`
+ );
+ const tab = await addTab(
+ "https://example.com/document-builder.sjs?html=" + html
+ );
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ info(
+ "Suggestions for 'di' with tag search, will match the two <div> elements in top document and the one in the iframe"
+ );
+ await assertSuggestion(
+ commands,
+ { query: "", firstPart: "di", state: "tag" },
+ [
+ {
+ suggestion: "div",
+ count: 3, // Matches the two <div> in the top document and the one in the iframe
+ type: "tag",
+ },
+ ]
+ );
+
+ info(
+ "Suggestions for 'ifram' with id search, will only match the <div> within the iframe"
+ );
+ await assertSuggestion(
+ commands,
+ { query: "", firstPart: "ifram", state: "id" },
+ [
+ {
+ suggestion: "#iframe",
+ count: 1,
+ type: "id",
+ },
+ ]
+ );
+
+ info(
+ "Suggestions for 'fo' with tag search, will match the class of the top <div> element"
+ );
+ await assertSuggestion(
+ commands,
+ { query: "", firstPart: "fo", state: "tag" },
+ [
+ {
+ suggestion: ".foo",
+ count: 1,
+ type: "class",
+ },
+ ]
+ );
+
+ info(
+ "Suggestions for classes, based on div elements, will match the two classes of top <div> element"
+ );
+ await assertSuggestion(
+ commands,
+ { query: "div", firstPart: "", state: "class" },
+ [
+ {
+ suggestion: ".bar",
+ count: 1,
+ type: "class",
+ },
+ {
+ suggestion: ".foo",
+ count: 1,
+ type: "class",
+ },
+ ]
+ );
+
+ info("Suggestion for non-existent tag names will return no suggestion");
+ await assertSuggestion(
+ commands,
+ { query: "", firstPart: "marquee", state: "tag" },
+ []
+ );
+
+ await commands.destroy();
+});
+
+async function assertSuggestion(
+ commands,
+ { query, firstPart, state },
+ expectedSuggestions
+) {
+ const suggestions = await commands.inspectorCommand.getSuggestionsForQuery(
+ query,
+ firstPart,
+ state
+ );
+ is(
+ suggestions.length,
+ expectedSuggestions.length,
+ "Got the expected number of suggestions"
+ );
+ for (let i = 0; i < expectedSuggestions.length; i++) {
+ info(` ## Asserting suggestion #${i}:`);
+ const expectedSuggestion = expectedSuggestions[i];
+ const [suggestion, count, type] = suggestions[i];
+ is(
+ suggestion,
+ expectedSuggestion.suggestion,
+ "The suggested string is valid"
+ );
+ is(count, expectedSuggestion.count, "The number of matches is valid");
+ is(type, expectedSuggestion.type, "The type of match is valid");
+ }
+}
diff --git a/devtools/shared/commands/inspector/tests/browser_inspector_command_search.js b/devtools/shared/commands/inspector/tests/browser_inspector_command_search.js
new file mode 100644
index 0000000000..d7d25d3ce6
--- /dev/null
+++ b/devtools/shared/commands/inspector/tests/browser_inspector_command_search.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing basic inspector search
+
+add_task(async () => {
+ const html = `<div>
+ <div>
+ <p>This is the paragraph node down in the tree</p>
+ </div>
+ <div class="child"></div>
+ <div class="child"></div>
+ <iframe src="data:text/html,${encodeURIComponent(
+ "<html><body><div class='frame-child'>foo</div></body></html>"
+ )}">
+ </iframe>
+ </div>`;
+
+ const tab = await addTab("data:text/html," + encodeURIComponent(html));
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ info("Search using text");
+ await searchAndAssert(
+ commands,
+ { query: "paragraph", reverse: false },
+ { resultsLength: 1, resultsIndex: 0 }
+ );
+
+ info("Search using class selector");
+ info(" > Get first result ");
+ await searchAndAssert(
+ commands,
+ { query: ".child", reverse: false },
+ { resultsLength: 2, resultsIndex: 0 }
+ );
+
+ info(" > Get next result ");
+ await searchAndAssert(
+ commands,
+ { query: ".child", reverse: false },
+ { resultsLength: 2, resultsIndex: 1 }
+ );
+
+ info("Search using el selector with reverse option");
+ info(" > Get first result ");
+ await searchAndAssert(
+ commands,
+ { query: "div", reverse: true },
+ { resultsLength: 6, resultsIndex: 5 }
+ );
+
+ info(" > Get next result ");
+ await searchAndAssert(
+ commands,
+ { query: "div", reverse: true },
+ { resultsLength: 6, resultsIndex: 4 }
+ );
+
+ info("Search for foo in remote frame");
+ await searchAndAssert(
+ commands,
+ { query: ".frame-child", reverse: false },
+ { resultsLength: 1, resultsIndex: 0 }
+ );
+
+ await commands.destroy();
+});
+/**
+ * Does an inspector search to find the next node and assert the results
+ *
+ * @param {Object} commands
+ * @param {Object} options
+ * options.query - search query
+ * options.reverse - search in reverse
+ * @param {Object} expected
+ * Holds the expected values
+ */
+async function searchAndAssert(commands, { query, reverse }, expected) {
+ const response = await commands.inspectorCommand.findNextNode(query, {
+ reverse,
+ });
+
+ is(
+ response.resultsLength,
+ expected.resultsLength,
+ "Got the expected no of results"
+ );
+
+ is(
+ response.resultsIndex,
+ expected.resultsIndex,
+ "Got the expected currently selected node index"
+ );
+}
diff --git a/devtools/shared/commands/inspector/tests/head.js b/devtools/shared/commands/inspector/tests/head.js
new file mode 100644
index 0000000000..73d9798446
--- /dev/null
+++ b/devtools/shared/commands/inspector/tests/head.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";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/moz.build b/devtools/shared/commands/moz.build
new file mode 100644
index 0000000000..b006ad8dbd
--- /dev/null
+++ b/devtools/shared/commands/moz.build
@@ -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/.
+
+DIRS += [
+ "inspected-window",
+ "inspector",
+ "network",
+ "object",
+ "resource",
+ "root-resource",
+ "script",
+ "target",
+ "target-configuration",
+ "thread-configuration",
+ "tracer",
+]
+
+DevToolsModules(
+ "commands-factory.js",
+ "index.js",
+)
diff --git a/devtools/shared/commands/network/moz.build b/devtools/shared/commands/network/moz.build
new file mode 100644
index 0000000000..9d74abdfa2
--- /dev/null
+++ b/devtools/shared/commands/network/moz.build
@@ -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/.
+
+DevToolsModules(
+ "network-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/network/network-command.js b/devtools/shared/commands/network/network-command.js
new file mode 100644
index 0000000000..44cdf4e759
--- /dev/null
+++ b/devtools/shared/commands/network/network-command.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";
+
+class NetworkCommand {
+ /**
+ * This class helps listen, inspect and control network requests.
+ *
+ * @param {DescriptorFront} descriptorFront
+ * The context to inspect identified by this descriptor.
+ * @param {WatcherFront} watcherFront
+ * If available, a reference to the related Watcher Front.
+ * @param {Object} commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ */
+ constructor({ descriptorFront, watcherFront, commands }) {
+ this.commands = commands;
+ this.descriptorFront = descriptorFront;
+ this.watcherFront = watcherFront;
+ }
+
+ /**
+ * Send a HTTP request data payload
+ *
+ * @param {object} data data payload would like to sent to backend
+ */
+ async sendHTTPRequest(data) {
+ // By default use the top-level target, but we might at some point
+ // allow using another target.
+ const networkContentFront =
+ await this.commands.targetCommand.targetFront.getFront("networkContent");
+ const { channelId } = await networkContentFront.sendHTTPRequest(data);
+ return { channelId };
+ }
+
+ /*
+ * Get the list of blocked URL filters.
+ *
+ * A URL filter is a RegExp string so that one filter can match many URLs.
+ * It can be an absolute URL to match only one precise request:
+ * http://mozilla.org/index.html
+ * Or just a string which would match all URL containing this string:
+ * mozilla
+ * Or a RegExp to match various types of URLs:
+ * http://*mozilla.org/*.css
+ *
+ * @return {Array}
+ * List of all currently blocked URL filters.
+ */
+ async getBlockedUrls() {
+ const networkParentFront = await this.watcherFront.getNetworkParentActor();
+ return networkParentFront.getBlockedUrls();
+ }
+
+ /**
+ * Updates the list of blocked URL filters.
+ *
+ * @param {Array} urls
+ * An array of URL filter strings.
+ * See getBlockedUrls for definition of URL filters.
+ */
+ async setBlockedUrls(urls) {
+ const networkParentFront = await this.watcherFront.getNetworkParentActor();
+ return networkParentFront.setBlockedUrls(urls);
+ }
+
+ /**
+ * Block only one additional URL filter
+ *
+ * @param {String} url
+ * URL filter to block.
+ * See getBlockedUrls for definition of URL filters.
+ */
+ async blockRequestForUrl(url) {
+ const networkParentFront = await this.watcherFront.getNetworkParentActor();
+ return networkParentFront.blockRequest({ url });
+ }
+
+ /**
+ * Stop blocking only one specific URL filter
+ *
+ * @param {String} url
+ * URL filter to unblock.
+ * See getBlockedUrls for definition of URL filters.
+ */
+ async unblockRequestForUrl(url) {
+ const networkParentFront = await this.watcherFront.getNetworkParentActor();
+ return networkParentFront.unblockRequest({ url });
+ }
+
+ destroy() {}
+}
+
+module.exports = NetworkCommand;
diff --git a/devtools/shared/commands/network/tests/browser.toml b/devtools/shared/commands/network/tests/browser.toml
new file mode 100644
index 0000000000..60b5949bb0
--- /dev/null
+++ b/devtools/shared/commands/network/tests/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+ "head.js",
+]
+
+["browser_network_command_request_blocking.js"]
+
+["browser_network_command_sendHTTPRequest.js"]
diff --git a/devtools/shared/commands/network/tests/browser_network_command_request_blocking.js b/devtools/shared/commands/network/tests/browser_network_command_request_blocking.js
new file mode 100644
index 0000000000..dd1167fa9b
--- /dev/null
+++ b/devtools/shared/commands/network/tests/browser_network_command_request_blocking.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the NetworkCommand API around request blocking
+
+add_task(async function () {
+ info("Test NetworkCommand request blocking");
+ const tab = await addTab("data:text/html,foo");
+ const commands = await CommandsFactory.forTab(tab);
+ const networkCommand = commands.networkCommand;
+ const resourceCommand = commands.resourceCommand;
+
+ // Usage of request blocking APIs requires to listen to NETWORK_EVENT.
+ await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable: () => {},
+ });
+
+ let blockedUrls = await networkCommand.getBlockedUrls();
+ Assert.deepEqual(
+ blockedUrls,
+ [],
+ "The list of blocked URLs is originaly empty"
+ );
+
+ await networkCommand.blockRequestForUrl("https://foo.com");
+ blockedUrls = await networkCommand.getBlockedUrls();
+ Assert.deepEqual(
+ blockedUrls,
+ ["https://foo.com"],
+ "The freshly added blocked URL is reported as blocked"
+ );
+
+ // We pass "url filters" which can be only part of a URL string
+ await networkCommand.blockRequestForUrl("bar");
+ blockedUrls = await networkCommand.getBlockedUrls();
+ Assert.deepEqual(
+ blockedUrls,
+ ["https://foo.com", "bar"],
+ "The second blocked URL is also reported as blocked"
+ );
+
+ await networkCommand.setBlockedUrls(["https://mozilla.org"]);
+ blockedUrls = await networkCommand.getBlockedUrls();
+ Assert.deepEqual(
+ blockedUrls,
+ ["https://mozilla.org"],
+ "setBlockedUrls replace the whole list of blocked URLs"
+ );
+
+ await networkCommand.unblockRequestForUrl("https://mozilla.org");
+ blockedUrls = await networkCommand.getBlockedUrls();
+ Assert.deepEqual(
+ blockedUrls,
+ [],
+ "The unblocked URL disappear from the list of blocked URLs"
+ );
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js b/devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js
new file mode 100644
index 0000000000..1d84a8a668
--- /dev/null
+++ b/devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the NetworkCommand's sendHTTPRequest
+
+add_task(async function () {
+ info("Test NetworkCommand.sendHTTPRequest");
+ const tab = await addTab("data:text/html,foo");
+ const commands = await CommandsFactory.forTab(tab);
+
+ // We have to ensure TargetCommand is initialized to have access to the top level target
+ // from NetworkCommand.sendHTTPRequest
+ await commands.targetCommand.startListening();
+
+ const { networkCommand } = commands;
+
+ const httpServer = createTestHTTPServer();
+ const onRequest = new Promise(resolve => {
+ httpServer.registerPathHandler(
+ "/http-request.html",
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("Response body");
+ resolve(request);
+ }
+ );
+ });
+ const url = `http://localhost:${httpServer.identity.primaryPort}/http-request.html`;
+
+ info("Call NetworkCommand.sendHTTPRequest");
+ const { resourceCommand } = commands;
+ const { onResource } = await resourceCommand.waitForNextResource(
+ resourceCommand.TYPES.NETWORK_EVENT
+ );
+ const { channelId } = await networkCommand.sendHTTPRequest({
+ url,
+ method: "POST",
+ headers: [{ name: "Request", value: "Header" }],
+ body: "Hello",
+ cause: {
+ loadingDocumentUri: "https://example.com",
+ stacktraceAvailable: true,
+ type: "xhr",
+ },
+ });
+ ok(channelId, "Received a channel id in response");
+ const resource = await onResource;
+ is(
+ resource.resourceId,
+ channelId,
+ "NETWORK_EVENT resource channelId is the same as the one returned by sendHTTPRequest"
+ );
+
+ const request = await onRequest;
+ is(request.method, "POST", "Request method is correct");
+ is(request.getHeader("Request"), "Header", "The custom header was passed");
+ is(fetchRequestBody(request), "Hello", "The request POST's body is correct");
+
+ await commands.destroy();
+});
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function fetchRequestBody(request) {
+ let body = "";
+ const bodyStream = new BinaryInputStream(request.bodyInputStream);
+ let avail = 0;
+ while ((avail = bodyStream.available()) > 0) {
+ body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail));
+ }
+ return body;
+}
diff --git a/devtools/shared/commands/network/tests/head.js b/devtools/shared/commands/network/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/network/tests/head.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/object/moz.build b/devtools/shared/commands/object/moz.build
new file mode 100644
index 0000000000..151750907c
--- /dev/null
+++ b/devtools/shared/commands/object/moz.build
@@ -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/.
+
+DevToolsModules(
+ "object-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/object/object-command.js b/devtools/shared/commands/object/object-command.js
new file mode 100644
index 0000000000..0396b6167a
--- /dev/null
+++ b/devtools/shared/commands/object/object-command.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";
+
+/**
+ * The ObjectCommand helps inspecting and managing lifecycle
+ * of all inspected JavaScript objects.
+ */
+class ObjectCommand {
+ constructor({ commands, descriptorFront, watcherFront }) {
+ this.#commands = commands;
+ }
+ #commands = null;
+
+ /**
+ * Release a set of object actors all at once.
+ *
+ * @param {Array<ObjectFront>} frontsToRelease
+ * List of fronts for the object to release.
+ */
+ async releaseObjects(frontsToRelease) {
+ // @backward-compat { version 123 } A new Objects Manager front has a new "releaseActors" method.
+ // Only supportsReleaseActors=true codepath can be kept once 123 is the release channel.
+ const { supportsReleaseActors } = this.#commands.client.mainRoot.traits;
+
+ // First group all object fronts per target
+ const actorsPerTarget = new Map();
+ const promises = [];
+ for (const frontToRelease of frontsToRelease) {
+ const { targetFront } = frontToRelease;
+ // If the front is already destroyed, its target front will be nullified.
+ if (!targetFront) {
+ continue;
+ }
+
+ let actorIDsToRemove = actorsPerTarget.get(targetFront);
+ if (!actorIDsToRemove) {
+ actorIDsToRemove = [];
+ actorsPerTarget.set(targetFront, actorIDsToRemove);
+ }
+ if (supportsReleaseActors) {
+ actorIDsToRemove.push(frontToRelease.actorID);
+ frontToRelease.destroy();
+ } else {
+ promises.push(frontToRelease.release());
+ }
+ }
+
+ if (supportsReleaseActors) {
+ // Then release all fronts by bulk per target
+ for (const [targetFront, actorIDs] of actorsPerTarget) {
+ const objectsManagerFront = await targetFront.getFront("objects-manager");
+ promises.push(objectsManagerFront.releaseObjects(actorIDs));
+ }
+ }
+
+ await Promise.all(promises);
+ }
+}
+
+module.exports = ObjectCommand;
diff --git a/devtools/shared/commands/object/tests/browser.toml b/devtools/shared/commands/object/tests/browser.toml
new file mode 100644
index 0000000000..4f1dbe830e
--- /dev/null
+++ b/devtools/shared/commands/object/tests/browser.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "head.js",
+]
+
+["browser_object.js"]
diff --git a/devtools/shared/commands/object/tests/browser_object.js b/devtools/shared/commands/object/tests/browser_object.js
new file mode 100644
index 0000000000..9f6d5132d3
--- /dev/null
+++ b/devtools/shared/commands/object/tests/browser_object.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ObjectCommand
+
+add_task(async function testObjectRelease() {
+ const tab = await addTab("data:text/html;charset=utf-8,Test page<script>var foo = { bar: 42 };</script>");
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const { objectCommand } = commands;
+
+ const evaluationResponse = await commands.scriptCommand.execute(
+ "window.foo"
+ );
+
+ // Execute a second time so that the WebConsoleActor set this._lastConsoleInputEvaluation to another value
+ // and so we prevent freeing `window.foo`
+ await commands.scriptCommand.execute("");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ is(content.wrappedJSObject.foo.bar, 42);
+ const weakRef = Cu.getWeakReference(content.wrappedJSObject.foo);
+
+ // Hold off the weak reference on SpecialPowsers so that it can be accessed in the next SpecialPowers.spawn
+ SpecialPowers.weakRef = weakRef;
+
+ // Nullify this variable so that it should be freed
+ // unless the DevTools inspection still hold it in memory
+ content.wrappedJSObject.foo = null;
+
+ Cu.forceGC();
+ Cu.forceCC();
+
+ ok(SpecialPowers.weakRef.get(), "The 'foo' object can't be freed because of DevTools keeping a reference on it");
+ });
+
+ info("Release the server side actors which are keeping the object in memory");
+ const objectFront = evaluationResponse.result;
+ await commands.objectCommand.releaseObjects([objectFront]);
+
+ ok(objectFront.isDestroyed(), "The passed object front has been destroyed");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ Cu.forceGC();
+ Cu.forceCC();
+ return !SpecialPowers.weakRef.get();
+ }, "Wait for JS object to be freed", 500);
+
+ ok(!SpecialPowers.weakRef.get(), "The 'foo' object has been freed");
+ });
+
+ await commands.destroy();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testMultiTargetObjectRelease() {
+ // This test fails with EFT disabled
+ if (!isEveryFrameTargetEnabled()) {
+ return;
+ }
+
+ const tab = await addTab(`data:text/html;charset=utf-8,Test page<iframe src="data:text/html,bar">/iframe>`);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const [,iframeTarget] = commands.targetCommand.getAllTargets(commands.targetCommand.ALL_TYPES);
+ is(iframeTarget.url, "data:text/html,bar");
+
+ const { objectCommand } = commands;
+
+ const evaluationResponse1 = await commands.scriptCommand.execute(
+ "window"
+ );
+ const evaluationResponse2 = await commands.scriptCommand.execute(
+ "window", {
+ selectedTargetFront: iframeTarget,
+ }
+ );
+ const object1 = evaluationResponse1.result;
+ const object2 = evaluationResponse2.result;
+ isnot(object1, object2, "The two window object fronts are different");
+ isnot(object1.targetFront, object2.targetFront, "The two window object fronts relates to two distinct targets");
+ is(object2.targetFront, iframeTarget, "The second object relates to the iframe target");
+
+ await commands.objectCommand.releaseObjects([object1, object2]);
+ ok(object1.isDestroyed(), "The first object front is destroyed");
+ ok(object2.isDestroyed(), "The second object front is destroyed");
+
+ await commands.destroy();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testWorkerObjectRelease() {
+ const workerUrl = `data:text/javascript,const foo = {}`;
+ const tab = await addTab(`data:text/html;charset=utf-8,Test page<script>const worker = new Worker("${workerUrl}")</script>`);
+
+ const commands = await CommandsFactory.forTab(tab);
+ commands.targetCommand.listenForWorkers = true;
+ await commands.targetCommand.startListening();
+
+ const [,workerTarget] = commands.targetCommand.getAllTargets(commands.targetCommand.ALL_TYPES);
+ is(workerTarget.url, workerUrl);
+
+ const { objectCommand } = commands;
+
+ const evaluationResponse = await commands.scriptCommand.execute(
+ "foo", {
+ selectedTargetFront: workerTarget,
+ }
+ );
+ const object = evaluationResponse.result;
+ is(object.targetFront, workerTarget, "The 'foo' object relates to the worker target");
+
+ await commands.objectCommand.releaseObjects([object]);
+ ok(object.isDestroyed(), "The object front is destroyed");
+
+ await commands.destroy();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/shared/commands/object/tests/head.js b/devtools/shared/commands/object/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/object/tests/head.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/resource/legacy-listeners/console-messages.js b/devtools/shared/commands/resource/legacy-listeners/console-messages.js
new file mode 100644
index 0000000000..ae3f81b4df
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/console-messages.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+module.exports = async function ({ targetCommand, targetFront, onAvailable }) {
+ // Allow the top level target unconditionnally.
+ // Also allow frame, but only in content toolbox, i.e. still ignore them in
+ // the context of the browser toolbox as we inspect messages via the process
+ // targets
+ const listenForFrames = targetCommand.descriptorFront.isTabDescriptor;
+
+ // Allow workers when messages aren't dispatched to the main thread.
+ const listenForWorkers =
+ !targetCommand.rootFront.traits
+ .workerConsoleApiMessagesDispatchedToMainThread;
+
+ const acceptTarget =
+ targetFront.isTopLevel ||
+ targetFront.targetType === targetCommand.TYPES.PROCESS ||
+ (targetFront.targetType === targetCommand.TYPES.FRAME && listenForFrames) ||
+ (targetFront.targetType === targetCommand.TYPES.WORKER && listenForWorkers);
+
+ if (!acceptTarget) {
+ return;
+ }
+
+ const webConsoleFront = await targetFront.getFront("console");
+ if (webConsoleFront.isDestroyed()) {
+ return;
+ }
+
+ // Request notifying about new messages
+ await webConsoleFront.startListeners(["ConsoleAPI"]);
+
+ // Fetch already existing messages
+ // /!\ The actor implementation requires to call startListeners(ConsoleAPI) first /!\
+ const { messages } = await webConsoleFront.getCachedMessages(["ConsoleAPI"]);
+
+ for (const message of messages) {
+ message.resourceType = ResourceCommand.TYPES.CONSOLE_MESSAGE;
+ }
+ onAvailable(messages);
+
+ // Forward new message events
+ webConsoleFront.on("consoleAPICall", message => {
+ // Ignore console messages that are cloned from the content process
+ // (they aren't relevant to toolboxes still using legacy listeners)
+ if (message.clonedFromContentProcess) {
+ return;
+ }
+
+ message.resourceType = ResourceCommand.TYPES.CONSOLE_MESSAGE;
+ onAvailable([message]);
+ });
+};
diff --git a/devtools/shared/commands/resource/legacy-listeners/css-changes.js b/devtools/shared/commands/resource/legacy-listeners/css-changes.js
new file mode 100644
index 0000000000..e9f3e17075
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/css-changes.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+module.exports = async function ({ targetFront, onAvailable }) {
+ if (!targetFront.hasActor("changes")) {
+ return;
+ }
+
+ const changesFront = await targetFront.getFront("changes");
+
+ // Get all changes collected up to this point by the ChangesActor on the server,
+ // then fire each change as "add-change".
+ const changes = await changesFront.allChanges();
+ await onAvailable(changes.map(change => toResource(change)));
+
+ changesFront.on("add-change", change => onAvailable([toResource(change)]));
+};
+
+function toResource(change) {
+ return Object.assign(change, {
+ resourceType: ResourceCommand.TYPES.CSS_CHANGE,
+ });
+}
diff --git a/devtools/shared/commands/resource/legacy-listeners/error-messages.js b/devtools/shared/commands/resource/legacy-listeners/error-messages.js
new file mode 100644
index 0000000000..5ba898c917
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/error-messages.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+const { MESSAGE_CATEGORY } = require("resource://devtools/shared/constants.js");
+
+module.exports = async function ({ targetCommand, targetFront, onAvailable }) {
+ // Allow the top level target unconditionnally.
+ // Also allow frame, but only in content toolbox, i.e. still ignore them in
+ // the context of the browser toolbox as we inspect messages via the process
+ // targets
+ // Also ignore workers as they are not supported yet. (see bug 1592584)
+ const listenForFrames = targetCommand.descriptorFront.isTabDescriptor;
+ const isAllowed =
+ targetFront.isTopLevel ||
+ targetFront.targetType === targetCommand.TYPES.PROCESS ||
+ (targetFront.targetType === targetCommand.TYPES.FRAME && listenForFrames);
+
+ if (!isAllowed) {
+ return;
+ }
+
+ const webConsoleFront = await targetFront.getFront("console");
+ if (webConsoleFront.isDestroyed()) {
+ return;
+ }
+
+ // Request notifying about new messages. Here the "PageError" type start listening for
+ // both actual PageErrors (emitted as "pageError" events) as well as LogMessages (
+ // emitted as "logMessage" events). This function only set up the listener on the
+ // webConsoleFront for "pageError".
+ await webConsoleFront.startListeners(["PageError"]);
+
+ // Fetch already existing messages
+ // /!\ The actor implementation requires to call startListeners("PageError") first /!\
+ let { messages } = await webConsoleFront.getCachedMessages(["PageError"]);
+
+ // On server < v79, we're also getting CSS Messages that we need to filter out.
+ messages = messages.filter(
+ message => message.pageError.category !== MESSAGE_CATEGORY.CSS_PARSER
+ );
+
+ messages.forEach(message => {
+ message.resourceType = ResourceCommand.TYPES.ERROR_MESSAGE;
+ });
+ // Cached messages don't have the same shape as live messages,
+ // so we need to transform them.
+ onAvailable(messages);
+
+ webConsoleFront.on("pageError", message => {
+ // On server < v79, we're getting CSS Messages that we need to filter out.
+ if (message.pageError.category === MESSAGE_CATEGORY.CSS_PARSER) {
+ return;
+ }
+
+ message.resourceType = ResourceCommand.TYPES.ERROR_MESSAGE;
+ onAvailable([message]);
+ });
+};
diff --git a/devtools/shared/commands/resource/legacy-listeners/moz.build b/devtools/shared/commands/resource/legacy-listeners/moz.build
new file mode 100644
index 0000000000..6ffb469891
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/moz.build
@@ -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/.
+
+DevToolsModules(
+ "console-messages.js",
+ "css-changes.js",
+ "error-messages.js",
+ "platform-messages.js",
+ "reflow.js",
+ "root-node.js",
+ "source.js",
+ "thread-states.js",
+)
diff --git a/devtools/shared/commands/resource/legacy-listeners/platform-messages.js b/devtools/shared/commands/resource/legacy-listeners/platform-messages.js
new file mode 100644
index 0000000000..729696275e
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/platform-messages.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+module.exports = async function ({ targetCommand, targetFront, onAvailable }) {
+ // Only allow the top level target and processes.
+ // Frames can be ignored as logMessage are never sent to them anyway.
+ // Also ignore workers as they are not supported yet. (see bug 1592584)
+ const isAllowed =
+ targetFront.isTopLevel ||
+ targetFront.targetType === targetCommand.TYPES.PROCESS;
+ if (!isAllowed) {
+ return;
+ }
+
+ const webConsoleFront = await targetFront.getFront("console");
+ if (webConsoleFront.isDestroyed()) {
+ return;
+ }
+
+ // Request notifying about new messages. Here the "PageError" type start listening for
+ // both actual PageErrors (emitted as "pageError" events) as well as LogMessages (
+ // emitted as "logMessage" events). This function only set up the listener on the
+ // webConsoleFront for "logMessage".
+ await webConsoleFront.startListeners(["PageError"]);
+
+ // Fetch already existing messages
+ // /!\ The actor implementation requires to call startListeners("PageError") first /!\
+ const { messages } = await webConsoleFront.getCachedMessages(["LogMessage"]);
+
+ for (const message of messages) {
+ message.resourceType = ResourceCommand.TYPES.PLATFORM_MESSAGE;
+ }
+ onAvailable(messages);
+
+ webConsoleFront.on("logMessage", message => {
+ message.resourceType = ResourceCommand.TYPES.PLATFORM_MESSAGE;
+ onAvailable([message]);
+ });
+};
diff --git a/devtools/shared/commands/resource/legacy-listeners/reflow.js b/devtools/shared/commands/resource/legacy-listeners/reflow.js
new file mode 100644
index 0000000000..63802f510d
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/reflow.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+module.exports = async function ({ targetFront, onAvailable }) {
+ if (!targetFront.getTrait("isBrowsingContext")) {
+ // The reflows only work with BrowsingContext targets
+ return;
+ }
+ const reflowFront = await targetFront.getFront("reflow");
+ reflowFront.on("reflows", reflows =>
+ onAvailable([
+ {
+ resourceType: ResourceCommand.TYPES.REFLOW,
+ reflows,
+ },
+ ])
+ );
+ await reflowFront.start();
+};
diff --git a/devtools/shared/commands/resource/legacy-listeners/root-node.js b/devtools/shared/commands/resource/legacy-listeners/root-node.js
new file mode 100644
index 0000000000..6fa2bcbf22
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/root-node.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+module.exports = async function ({ targetFront, onAvailable, onDestroyed }) {
+ // XXX: When watching root node for a non top-level target, this will also
+ // ensure the inspector & walker fronts for the target are initialized.
+ // This also implies that we call reparentRemoteFrame on the new walker, which
+ // will create the link between the parent frame NodeFront and the inner
+ // document NodeFront.
+ //
+ // This is not something that will work when the resource is moved to the
+ // server. When it becomes a server side resource, a RootNode would be emitted
+ // directly by the target actor.
+ //
+ // This probably means that the root node resource cannot remain a NodeFront.
+ // It should not be a front and the client should be responsible for
+ // retrieving the corresponding NodeFront.
+ //
+ // The other thing that we are missing with this patch is that we should only
+ // create inspector & walker fronts (and call reparentRemoteFrame) when we get
+ // a RootNode which is directly under an iframe node which is currently
+ // visible and tracked in the markup view.
+ //
+ // For instance, with the following markup:
+ // html
+ // body
+ // div
+ // iframe
+ // remote doc
+ //
+ // If the markup view only sees nodes down to `div`, then the client is not
+ // currently tracking the nodeFront for the `iframe`, and getting a new root
+ // node for the remote document should NOT force the iframe to be tracked on
+ // on the client.
+ //
+ // When we get a RootNode resource, we will need a way to check this before
+ // initializing & reparenting the walker.
+ //
+ if (!targetFront.getTrait("isBrowsingContext")) {
+ // The root-node resource is only available on browsing-context targets.
+ return;
+ }
+
+ const inspectorFront = await targetFront.getFront("inspector");
+ inspectorFront.walker.on("root-available", node => {
+ node.resourceType = ResourceCommand.TYPES.ROOT_NODE;
+ return onAvailable([node]);
+ });
+
+ inspectorFront.walker.on("root-destroyed", node => {
+ node.resourceType = ResourceCommand.TYPES.ROOT_NODE;
+ return onDestroyed([node]);
+ });
+
+ await inspectorFront.walker.watchRootNode();
+};
diff --git a/devtools/shared/commands/resource/legacy-listeners/source.js b/devtools/shared/commands/resource/legacy-listeners/source.js
new file mode 100644
index 0000000000..45ee62f70f
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/source.js
@@ -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/. */
+
+"use strict";
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+/**
+ * Emit SOURCE resources, which represents a Javascript source and has the following attributes set on "available":
+ *
+ * - introductionType {null|String}: A string indicating how this source code was introduced into the system.
+ * This will typically be set to "scriptElement", "eval", ...
+ * But this may have many other values:
+ * https://searchfox.org/mozilla-central/rev/ac142717cc067d875e83e4b1316f004f6e063a46/dom/script/ScriptLoader.cpp#2628-2639
+ * https://searchfox.org/mozilla-central/search?q=symbol:_ZN2JS14CompileOptions19setIntroductionTypeEPKc&redirect=false
+ * https://searchfox.org/mozilla-central/rev/ac142717cc067d875e83e4b1316f004f6e063a46/devtools/server/actors/source.js#160-169
+ * - sourceMapBaseURL {String}: Base URL where to look for a source map.
+ * This isn't the source map URL.
+ * - sourceMapURL {null|String}: URL of the source map, if there is one.
+ * - url {null|String}: URL of the source, if it relates to a particular URL.
+ * Evaled sources won't have any related URL.
+ * - isBlackBoxed {Boolean}: Specifying whether the source actor's 'black-boxed' flag is set.
+ * - extensionName {null|String}: If the source comes from an add-on, the add-on name.
+ */
+module.exports = async function ({ targetCommand, targetFront, onAvailable }) {
+ const isBrowserToolbox =
+ targetCommand.descriptorFront.isBrowserProcessDescriptor;
+ const isNonTopLevelFrameTarget =
+ !targetFront.isTopLevel &&
+ targetFront.targetType === targetCommand.TYPES.FRAME;
+
+ if (isBrowserToolbox && isNonTopLevelFrameTarget) {
+ // In the BrowserToolbox, non-top-level frame targets are already
+ // debugged via content-process targets.
+ return;
+ }
+
+ const threadFront = await targetFront.getFront("thread");
+
+ // Use a list of all notified SourceFront as we don't have a newSource event for all sources
+ // but we sometime get sources notified both via newSource event *and* sources() method...
+ // We store actor ID instead of SourceFront as it appears that multiple SourceFront for the same
+ // actor are created...
+ const sourcesActorIDCache = new Set();
+
+ // Forward new sources (but also existing ones, see next comment)
+ threadFront.on("newSource", ({ source }) => {
+ if (sourcesActorIDCache.has(source.actor)) {
+ return;
+ }
+ sourcesActorIDCache.add(source.actor);
+ // source is a SourceActor's form, add the resourceType attribute on it
+ source.resourceType = ResourceCommand.TYPES.SOURCE;
+ onAvailable([source]);
+ });
+
+ // Forward already existing sources
+ // Note that calling `sources()` will end up emitting `newSource` event for all existing sources.
+ // But not in some cases, for example, when the thread is already paused.
+ // (And yes, it means that already existing sources can be transfered twice over the wire)
+ //
+ // Also, browser_ext_devtools_inspectedWindow_targetSwitch.js creates many top level targets,
+ // for which the SourceMapURLService will fetch sources. But these targets are destroyed while
+ // the test is running and when they are, we purge all pending requests, including this one.
+ // So ignore any error if this request failed on destruction.
+ let sources;
+ try {
+ sources = await threadFront.sources();
+ } catch (e) {
+ if (threadFront.isDestroyed()) {
+ return;
+ }
+ throw e;
+ }
+
+ // Note that `sources()` doesn't encapsulate SourceFront into a `source` attribute
+ // while `newSource` event does.
+ sources = sources.filter(source => {
+ return !sourcesActorIDCache.has(source.actor);
+ });
+ for (const source of sources) {
+ sourcesActorIDCache.add(source.actor);
+ // source is a SourceActor's form, add the resourceType attribute on it
+ source.resourceType = ResourceCommand.TYPES.SOURCE;
+ }
+ onAvailable(sources);
+};
diff --git a/devtools/shared/commands/resource/legacy-listeners/thread-states.js b/devtools/shared/commands/resource/legacy-listeners/thread-states.js
new file mode 100644
index 0000000000..42c922072a
--- /dev/null
+++ b/devtools/shared/commands/resource/legacy-listeners/thread-states.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+module.exports = async function ({ targetCommand, targetFront, onAvailable }) {
+ const isBrowserToolbox =
+ targetCommand.descriptorFront.isBrowserProcessDescriptor;
+ const isNonTopLevelFrameTarget =
+ !targetFront.isTopLevel &&
+ targetFront.targetType === targetCommand.TYPES.FRAME;
+
+ if (isBrowserToolbox && isNonTopLevelFrameTarget) {
+ // In the BrowserToolbox, non-top-level frame targets are already
+ // debugged via content-process targets.
+ return;
+ }
+
+ // Wait for the thread actor to be attached, otherwise getFront(thread) will throw for worker targets
+ // This is because worker target are still kind of descriptors and are only resolved into real target
+ // after being attached. And the thread actor ID is only retrieved and available after being attached.
+ await targetFront.onThreadAttached;
+
+ if (targetFront.isDestroyed()) {
+ return;
+ }
+ const threadFront = await targetFront.getFront("thread");
+
+ let isInterrupted = false;
+ const onPausedPacket = packet => {
+ // If paused by an explicit interrupt, which are generated by the
+ // slow script dialog and internal events such as setting
+ // breakpoints, ignore the event.
+ const { why } = packet;
+ if (why.type === "interrupted" && !why.onNext) {
+ isInterrupted = true;
+ return;
+ }
+
+ // Ignore attached events because they are not useful to the user.
+ if (why.type == "alreadyPaused" || why.type == "attached") {
+ return;
+ }
+
+ onAvailable([
+ {
+ resourceType: ResourceCommand.TYPES.THREAD_STATE,
+ state: "paused",
+ why,
+ frame: packet.frame,
+ },
+ ]);
+ };
+ threadFront.on("paused", onPausedPacket);
+
+ threadFront.on("resumed", packet => {
+ // NOTE: the client suppresses resumed events while interrupted
+ // to prevent unintentional behavior.
+ // see [client docs](devtools/client/debugger/src/client/README.md#interrupted) for more information.
+ if (isInterrupted) {
+ isInterrupted = false;
+ return;
+ }
+
+ onAvailable([
+ {
+ resourceType: ResourceCommand.TYPES.THREAD_STATE,
+ state: "resumed",
+ },
+ ]);
+ });
+
+ // Notify about already paused thread
+ const pausedPacket = threadFront.getLastPausePacket();
+ if (pausedPacket) {
+ onPausedPacket(pausedPacket);
+ }
+};
diff --git a/devtools/shared/commands/resource/moz.build b/devtools/shared/commands/resource/moz.build
new file mode 100644
index 0000000000..190589df4b
--- /dev/null
+++ b/devtools/shared/commands/resource/moz.build
@@ -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/.
+
+DIRS += [
+ "legacy-listeners",
+ "transformers",
+]
+
+DevToolsModules(
+ "resource-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/resource/resource-command.js b/devtools/shared/commands/resource/resource-command.js
new file mode 100644
index 0000000000..c45dc6a584
--- /dev/null
+++ b/devtools/shared/commands/resource/resource-command.js
@@ -0,0 +1,1367 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { throttle } = require("resource://devtools/shared/throttle.js");
+
+let gLastResourceId = 0;
+
+function cacheKey(resourceType, resourceId) {
+ return `${resourceType}:${resourceId}`;
+}
+
+class ResourceCommand {
+ /**
+ * This class helps retrieving existing and listening to resources.
+ * A resource is something that:
+ * - the target you are debugging exposes
+ * - can be created as early as the process/worker/page starts loading
+ * - can already exist, or will be created later on
+ * - doesn't require any user data to be fetched, only a type/category
+ *
+ * @param object commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ */
+ constructor({ commands }) {
+ this.targetCommand = commands.targetCommand;
+
+ // Public attribute set by tests to disable throttling
+ this.throttlingDisabled = false;
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this._onResourceDestroyed = this._onResourceDestroyed.bind(this);
+
+ // Array of all the currently registered watchers, which contains object with attributes:
+ // - {String} resources: list of all resource watched by this one watcher
+ // - {Function} onAvailable: watcher's function to call when a new resource is available
+ // - {Function} onUpdated: watcher's function to call when a resource has been updated
+ // - {Function} onDestroyed: watcher's function to call when a resource is destroyed
+ this._watchers = [];
+
+ // Set of watchers currently going through watchResources, only used to handle
+ // early calls to unwatchResources. Using a Set instead of an array for easier
+ // delete operations.
+ this._pendingWatchers = new Set();
+
+ // Caches for all resources by the order that the resource was taken.
+ this._cache = new Map();
+ this._listenedResources = new Set();
+
+ // WeakMap used to avoid starting a legacy listener twice for the same
+ // target + resource-type pair. Legacy listener creation can be subject to
+ // race conditions.
+ // Maps a target front to an array of resource types.
+ this._existingLegacyListeners = new WeakMap();
+ this._processingExistingResources = new Set();
+
+ // List of targetFront event listener unregistration functions keyed by target front.
+ // These are called when unwatching resources, so if a consumer starts watching resources again,
+ // we don't have listeners registered twice.
+ this._offTargetFrontListeners = new Map();
+
+ this._notifyWatchers = this._notifyWatchers.bind(this);
+ this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100);
+ }
+
+ get watcherFront() {
+ return this.targetCommand.watcherFront;
+ }
+
+ addResourceToCache(resource) {
+ const { resourceId, resourceType } = resource;
+ this._cache.set(cacheKey(resourceType, resourceId), resource);
+ }
+
+ /**
+ * Clear all the resources related to specifed resource types.
+ * Should also trigger clearing of the caches that exists on the related
+ * serverside resource watchers.
+ *
+ * @param {Array:string} resourceTypes
+ * A list of all the resource types whose
+ * resources shouled be cleared.
+ */
+ async clearResources(resourceTypes) {
+ if (!Array.isArray(resourceTypes)) {
+ throw new Error("clearResources expects a list of resources types");
+ }
+ // Clear the cached resources of the type.
+ for (const [key, resource] of this._cache) {
+ if (resourceTypes.includes(resource.resourceType)) {
+ // NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
+ this._cache.delete(key);
+ }
+ }
+
+ const resourcesToClear = resourceTypes.filter(resourceType =>
+ this.hasResourceCommandSupport(resourceType)
+ );
+ if (resourcesToClear.length) {
+ this.watcherFront.clearResources(resourcesToClear);
+ }
+ }
+ /**
+ * Return all specified resources cached in this watcher.
+ *
+ * @param {String} resourceType
+ * @return {Array} resources cached in this watcher
+ */
+ getAllResources(resourceType) {
+ const result = [];
+ for (const resource of this._cache.values()) {
+ if (resource.resourceType === resourceType) {
+ result.push(resource);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return the specified resource cached in this watcher.
+ *
+ * @param {String} resourceType
+ * @param {String} resourceId
+ * @return {Object} resource cached in this watcher
+ */
+ getResourceById(resourceType, resourceId) {
+ return this._cache.get(cacheKey(resourceType, resourceId));
+ }
+
+ /**
+ * Request to start retrieving all already existing instances of given
+ * type of resources and also start watching for the one to be created after.
+ *
+ * @param {Array:string} resources
+ * List of all resources which should be fetched and observed.
+ * @param {Object} options
+ * - {Function} onAvailable: This attribute is mandatory.
+ * Function which will be called with an array of resources
+ * each time resource(s) are created.
+ * A second dictionary argument with `areExistingResources` boolean
+ * attribute helps knowing if that's live resources, or some coming
+ * from ResourceCommand cache.
+ * - {Function} onUpdated: This attribute is optional.
+ * Function which will be called with an array of updates resources
+ * each time resource(s) are updated.
+ * These resources were previously notified via onAvailable.
+ * - {Function} onDestroyed: This attribute is optional.
+ * Function which will be called with an array of deleted resources
+ * each time resource(s) are destroyed.
+ * - {boolean} ignoreExistingResources:
+ * This attribute is optional. Default value is false.
+ * If set to true, onAvailable won't be called with
+ * existing resources.
+ */
+ async watchResources(resources, options) {
+ const {
+ onAvailable,
+ onUpdated,
+ onDestroyed,
+ ignoreExistingResources = false,
+ } = options;
+
+ if (typeof onAvailable !== "function") {
+ throw new Error(
+ "ResourceCommand.watchResources expects an onAvailable function as argument"
+ );
+ }
+
+ for (const type of resources) {
+ if (!this._isValidResourceType(type)) {
+ throw new Error(
+ `ResourceCommand.watchResources invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ // Pending watchers are used in unwatchResources to remove watchers which
+ // are not fully registered yet. Store `onAvailable` which is the unique key
+ // for a watcher, as well as the resources array, so that unwatchResources
+ // can update the array if we stop watching a specific resource.
+ const pendingWatcher = {
+ resources,
+ onAvailable,
+ };
+ this._pendingWatchers.add(pendingWatcher);
+
+ // Bug 1675763: Watcher actor is not available in all situations yet.
+ if (!this._listenerRegistered && this.watcherFront) {
+ this._listenerRegistered = true;
+ // Resources watched from the parent process will be emitted on the Watcher Actor.
+ // So that we also have to listen for this event on it, in addition to all targets.
+ this.watcherFront.on(
+ "resource-available-form",
+ this._onResourceAvailable.bind(this, {
+ watcherFront: this.watcherFront,
+ })
+ );
+ this.watcherFront.on(
+ "resource-updated-form",
+ this._onResourceUpdated.bind(this, { watcherFront: this.watcherFront })
+ );
+ this.watcherFront.on(
+ "resource-destroyed-form",
+ this._onResourceDestroyed.bind(this, {
+ watcherFront: this.watcherFront,
+ })
+ );
+ }
+
+ const promises = [];
+ for (const resource of resources) {
+ promises.push(this._startListening(resource));
+ }
+ await Promise.all(promises);
+
+ // The resource cache is immediately filled when receiving the sources, but they are
+ // emitted with a delay due to throttling. Since the cache can contain resources that
+ // will soon be emitted, we have to flush it before adding the new listeners.
+ // Otherwise _forwardExistingResources might emit resources that will also be emitted by
+ // the next `_notifyWatchers` call done when calling `_startListening`, which will pull the
+ // "already existing" resources.
+ this._notifyWatchers();
+
+ // Update the _pendingWatchers set before adding the watcher to _watchers.
+ this._pendingWatchers.delete(pendingWatcher);
+
+ // If unwatchResources was called in the meantime, use pendingWatcher's
+ // resources to get the updated list of watched resources.
+ const watchedResources = pendingWatcher.resources;
+
+ // If no resource needs to be watched anymore, do not add an empty watcher
+ // to _watchers, and do not notify about cached resources.
+ if (!watchedResources.length) {
+ return;
+ }
+
+ // Register the watcher just after calling _startListening in order to avoid it being called
+ // for already existing resources, which will optionally be notified via _forwardExistingResources
+ this._watchers.push({
+ resources: watchedResources,
+ onAvailable,
+ onUpdated,
+ onDestroyed,
+ pendingEvents: [],
+ });
+
+ if (!ignoreExistingResources) {
+ await this._forwardExistingResources(watchedResources, onAvailable);
+ }
+ }
+
+ /**
+ * Stop watching for given type of resources.
+ * See `watchResources` for the arguments as both methods receive the same.
+ * Note that `onUpdated` and `onDestroyed` attributes of `options` aren't used here.
+ * Only `onAvailable` attribute is looked up and we unregister all the other registered callbacks
+ * when a matching available callback is found.
+ */
+ unwatchResources(resources, options) {
+ const { onAvailable } = options;
+
+ if (typeof onAvailable !== "function") {
+ throw new Error(
+ "ResourceCommand.unwatchResources expects an onAvailable function as argument"
+ );
+ }
+
+ for (const type of resources) {
+ if (!this._isValidResourceType(type)) {
+ throw new Error(
+ `ResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ // Unregister the callbacks from the watchers registries.
+ // Check _watchers for the fully initialized watchers, as well as
+ // `_pendingWatchers` for new watchers still being created by `watchResources`
+ const allWatchers = [...this._watchers, ...this._pendingWatchers];
+ for (const watcherEntry of allWatchers) {
+ // onAvailable is the only mandatory argument which ends up being used to match
+ // the right watcher entry.
+ if (watcherEntry.onAvailable == onAvailable) {
+ // Remove all resources that we stop watching. We may still watch for some others.
+ watcherEntry.resources = watcherEntry.resources.filter(resourceType => {
+ return !resources.includes(resourceType);
+ });
+ }
+ }
+ this._watchers = this._watchers.filter(entry => {
+ // Remove entries entirely if it isn't watching for any resource type
+ return !!entry.resources.length;
+ });
+
+ // Stop listening to all resources for which we removed the last watcher
+ for (const resource of resources) {
+ const isResourceWatched = allWatchers.some(watcherEntry =>
+ watcherEntry.resources.includes(resource)
+ );
+
+ // Also check in _listenedResources as we may call unwatchResources
+ // for resources that we haven't started watching for.
+ if (!isResourceWatched && this._listenedResources.has(resource)) {
+ this._stopListening(resource);
+ }
+ }
+
+ // Stop watching for targets if we removed the last listener.
+ if (this._listenedResources.size == 0) {
+ this._unwatchAllTargets();
+ }
+ }
+
+ /**
+ * Wait for a single resource of the provided resourceType.
+ *
+ * @param {String} resourceType
+ * One of ResourceCommand.TYPES, type of the expected resource.
+ * @param {Object} additional options
+ * - {Boolean} ignoreExistingResources: ignore existing resources or not.
+ * - {Function} predicate: if provided, will wait until a resource makes
+ * predicate(resource) return true.
+ * @return {Promise<Object>}
+ * Return a promise which resolves once we fully settle the resource listener.
+ * You should await for its resolution before doing the action which may fire
+ * your resource.
+ * This promise will expose an object with `onResource` attribute,
+ * itself being a promise, which will resolve once a matching resource is received.
+ */
+ async waitForNextResource(
+ resourceType,
+ { ignoreExistingResources = false, predicate } = {}
+ ) {
+ // If no predicate was provided, convert to boolean to avoid resolving for
+ // empty `resources` arrays.
+ predicate = predicate || (resource => !!resource);
+
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+ const onAvailable = async resources => {
+ const matchingResource = resources.find(resource => predicate(resource));
+ if (matchingResource) {
+ this.unwatchResources([resourceType], { onAvailable });
+ resolve(matchingResource);
+ }
+ };
+
+ await this.watchResources([resourceType], {
+ ignoreExistingResources,
+ onAvailable,
+ });
+ return { onResource: promise };
+ }
+
+ /**
+ * Check if there are any watchers for the specified resource.
+ *
+ * @param {String} resourceType
+ * One of ResourceCommand.TYPES
+ * @return {Boolean}
+ * If the resources type is beibg watched.
+ */
+ isResourceWatched(resourceType) {
+ return this._listenedResources.has(resourceType);
+ }
+
+ /**
+ * Start watching for all already existing and future targets.
+ *
+ * We are using ALL_TYPES, but this won't force listening to all types.
+ * It will only listen for types which are defined by `TargetCommand.startListening`.
+ */
+ async _watchAllTargets() {
+ if (!this._watchTargetsPromise) {
+ // If this is the very first listener registered, of all kind of resource types:
+ // * we want to start observing targets via TargetCommand
+ // * _onTargetAvailable will be called for each already existing targets and the next one to come
+ this._watchTargetsPromise = this.targetCommand.watchTargets({
+ types: this.targetCommand.ALL_TYPES,
+ onAvailable: this._onTargetAvailable,
+ onDestroyed: this._onTargetDestroyed,
+ });
+ }
+ return this._watchTargetsPromise;
+ }
+
+ _unwatchAllTargets() {
+ if (!this._watchTargetsPromise) {
+ return;
+ }
+
+ for (const offList of this._offTargetFrontListeners.values()) {
+ offList.forEach(off => off());
+ }
+ this._offTargetFrontListeners.clear();
+
+ this._watchTargetsPromise = null;
+ this.targetCommand.unwatchTargets({
+ types: this.targetCommand.ALL_TYPES,
+ onAvailable: this._onTargetAvailable,
+ onDestroyed: this._onTargetDestroyed,
+ });
+ }
+
+ /**
+ * For a given resource type, start the legacy listeners for all already existing targets.
+ * Do that only if we have to. If this resourceType requires legacy listeners.
+ */
+ async _startLegacyListenersForExistingTargets(resourceType) {
+ // If we were already listening to targets, we want to start the legacy listeners
+ // for all already existing targets.
+ //
+ // Only try instantiating the legacy listener, if this resource type:
+ // - has legacy listener implementation
+ // (new resource types may not be supported by old runtime and just not be received without breaking anything)
+ // - isn't supported by the server, or, the target type requires the a legacy listener implementation.
+ const shouldRunLegacyListeners =
+ resourceType in LegacyListeners &&
+ (!this.hasResourceCommandSupport(resourceType) ||
+ this._shouldRunLegacyListenerEvenWithWatcherSupport(resourceType));
+ if (shouldRunLegacyListeners) {
+ const promises = [];
+ const targets = this.targetCommand.getAllTargets(
+ this.targetCommand.ALL_TYPES
+ );
+ for (const targetFront of targets) {
+ // We disable warning in case we already registered the legacy listener for this target
+ // as this code may race with the call from onTargetAvailable if we end up having multiple
+ // calls to _startListening in parallel.
+ promises.push(
+ this._watchResourcesForTarget({
+ targetFront,
+ resourceType,
+ disableWarning: true,
+ })
+ );
+ }
+ await Promise.all(promises);
+ }
+ }
+
+ /**
+ * Method called by the TargetCommand for each already existing or target which has just been created.
+ *
+ * @param {Object} arg
+ * @param {Front} arg.targetFront
+ * The Front of the target that is available.
+ * This Front inherits from TargetMixin and is typically
+ * composed of a WindowGlobalTargetFront or ContentProcessTargetFront.
+ * @param {Boolean} arg.isTargetSwitching
+ * true when the new target was created because of a target switching.
+ */
+ async _onTargetAvailable({ targetFront, isTargetSwitching }) {
+ const resources = [];
+ if (isTargetSwitching) {
+ // WatcherActor currently only watches additional frame targets and
+ // explicitely ignores top level one that may be created when navigating
+ // to a new process.
+ // In order to keep working resources that are being watched via the
+ // Watcher actor, we have to unregister and re-register the resource
+ // types. This will force calling `Resources.watchResources` on the new top
+ // level target.
+ for (const resourceType of Object.values(ResourceCommand.TYPES)) {
+ // ...which has at least one listener...
+ if (!this._listenedResources.has(resourceType)) {
+ continue;
+ }
+
+ if (this._shouldRestartListenerOnTargetSwitching(resourceType)) {
+ this._stopListening(resourceType, {
+ bypassListenerCount: true,
+ });
+ resources.push(resourceType);
+ }
+ }
+ }
+
+ if (targetFront.isDestroyed()) {
+ return;
+ }
+
+ // If we are target switching, we already stop & start listening to all the
+ // currently monitored resources.
+ if (!isTargetSwitching) {
+ // For each resource type...
+ for (const resourceType of Object.values(ResourceCommand.TYPES)) {
+ // ...which has at least one listener...
+ if (!this._listenedResources.has(resourceType)) {
+ continue;
+ }
+ // ...request existing resource and new one to come from this one target
+ // *but* only do that for backward compat, where we don't have the watcher API
+ // (See bug 1626647)
+ await this._watchResourcesForTarget({ targetFront, resourceType });
+ }
+ }
+
+ // Compared to the TargetCommand and Watcher.watchTargets,
+ // We do call Watcher.watchResources, but the events are fired on the target.
+ // That's because the Watcher runs in the parent process/main thread, while resources
+ // are available from the target's process/thread.
+ const offResourceAvailable = targetFront.on(
+ "resource-available-form",
+ this._onResourceAvailable.bind(this, { targetFront })
+ );
+ const offResourceUpdated = targetFront.on(
+ "resource-updated-form",
+ this._onResourceUpdated.bind(this, { targetFront })
+ );
+ const offResourceDestroyed = targetFront.on(
+ "resource-destroyed-form",
+ this._onResourceDestroyed.bind(this, { targetFront })
+ );
+
+ const offList = this._offTargetFrontListeners.get(targetFront) || [];
+ offList.push(
+ offResourceAvailable,
+ offResourceUpdated,
+ offResourceDestroyed
+ );
+
+ if (isTargetSwitching) {
+ await Promise.all(
+ resources.map(resourceType =>
+ this._startListening(resourceType, {
+ bypassListenerCount: true,
+ })
+ )
+ );
+ }
+
+ // DOCUMENT_EVENT's will-navigate should replace target actor's will-navigate event,
+ // but only for targets provided by the watcher actor.
+ // Emit a fake DOCUMENT_EVENT's "will-navigate" out of target actor's will-navigate
+ // until watcher actor is supported by all descriptors (bug 1675763).
+ if (!this.targetCommand.hasTargetWatcherSupport()) {
+ const offWillNavigate = targetFront.on(
+ "will-navigate",
+ ({ url, isFrameSwitching }) => {
+ targetFront.emit("resource-available-form", [
+ {
+ resourceType: this.TYPES.DOCUMENT_EVENT,
+ name: "will-navigate",
+ time: Date.now(), // will-navigate was not passing any timestamp
+ isFrameSwitching,
+ newURI: url,
+ },
+ ]);
+ }
+ );
+ offList.push(offWillNavigate);
+ }
+
+ this._offTargetFrontListeners.set(targetFront, offList);
+ }
+
+ _shouldRestartListenerOnTargetSwitching(resourceType) {
+ // Note that we aren't using isServerTargetSwitchingEnabled, nor checking the
+ // server side target switching preference as we may have server side targets
+ // even when this is false/disabled.
+ // This will happen for bfcache navigations, even with server side targets disabled.
+ // `followWindowGlobalLifeCycle` will be false for the first top level target
+ // and only become true when doing a bfcache navigation.
+ // (only server side targets follow the WindowGlobal lifecycle)
+ // When server side targets are enabled, this will always be true.
+ const isServerSideTarget =
+ this.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
+ if (isServerSideTarget) {
+ // For top-level targets created from the server, only restart legacy
+ // listeners.
+ return !this.hasResourceCommandSupport(resourceType);
+ }
+
+ // For top-level targets created from the client we should always restart
+ // listeners.
+ return true;
+ }
+
+ /**
+ * Method called by the TargetCommand when a target has just been destroyed
+ * @param {Object} arg
+ * @param {Front} arg.targetFront
+ * The Front of the target that was destroyed
+ * @param {Boolean} arg.isModeSwitching
+ * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref.
+ */
+ _onTargetDestroyed({ targetFront, isModeSwitching }) {
+ // Clear the map of legacy listeners for this target.
+ this._existingLegacyListeners.set(targetFront, []);
+ this._offTargetFrontListeners.delete(targetFront);
+
+ // Purge the cache from any resource related to the destroyed target.
+ // Top level BrowsingContext target will be purge via DOCUMENT_EVENT will-navigate events.
+ // If we were to clean resources from target-destroyed, we will clear resources
+ // happening between will-navigate and target-destroyed. Typically the navigation request
+ // At the moment, isModeSwitching can only be true when targetFront.isTopLevel isn't true,
+ // so we don't need to add a specific check for isModeSwitching.
+ if (!targetFront.isTopLevel || !targetFront.isBrowsingContext) {
+ for (const [key, resource] of this._cache) {
+ if (resource.targetFront === targetFront) {
+ // NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
+ this._cache.delete(key);
+ }
+ }
+ }
+
+ // Purge "available" pendingEvents for resources from the destroyed target when switching
+ // mode as we want to ignore those.
+ if (isModeSwitching) {
+ for (const watcherEntry of this._watchers) {
+ for (const pendingEvent of watcherEntry.pendingEvents) {
+ if (pendingEvent.callbackType == "available") {
+ pendingEvent.updates = pendingEvent.updates.filter(
+ update => update.targetFront !== targetFront
+ );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Method called either by:
+ * - the backward compatibility code (LegacyListeners)
+ * - target actors RDP events
+ * whenever an already existing resource is being listed or when a new one
+ * has been created.
+ *
+ * @param {Object} source
+ * A dictionary object with only one of these two attributes:
+ * - targetFront: a Target Front, if the resource is watched from the target process or thread
+ * - watcherFront: a Watcher Front, if the resource is watched from the parent process
+ * @param {Array<json/Front>} resources
+ * Depending on the resource Type, it can be an Array composed of either JSON objects or Fronts,
+ * which describes the resource.
+ */
+ async _onResourceAvailable({ targetFront, watcherFront }, resources) {
+ let includesDocumentEventWillNavigate = false;
+ let includesDocumentEventDomLoading = false;
+ for (let resource of resources) {
+ const { resourceType } = resource;
+
+ if (watcherFront) {
+ targetFront = await this._getTargetForWatcherResource(resource);
+ // When we receive resources from the Watcher actor,
+ // there is no guarantee that the target front is fully initialized.
+ // The Target Front is initialized by the TargetCommand, by calling TargetFront.attachAndInitThread.
+ // We have to wait for its completion as resources watchers are expecting it to be completed.
+ //
+ // But when navigating, we may receive resources packets for a destroyed target.
+ // Or, in the context of the browser toolbox, they may not relate to any target.
+ if (targetFront) {
+ await targetFront.initialized;
+ }
+ }
+
+ // isAlreadyExistingResource indicates that the resources already existed before
+ // the resource command started watching for this type of resource.
+ resource.isAlreadyExistingResource =
+ this._processingExistingResources.has(resourceType);
+
+ // Put the targetFront on the resource for easy retrieval.
+ // (Resources from the legacy listeners may already have the attribute set)
+ if (!resource.targetFront) {
+ resource.targetFront = targetFront;
+ }
+
+ if (ResourceTransformers[resourceType]) {
+ resource = ResourceTransformers[resourceType]({
+ resource,
+ targetCommand: this.targetCommand,
+ targetFront,
+ watcherFront: this.watcherFront,
+ });
+ }
+
+ if (!resource.resourceId) {
+ resource.resourceId = `auto:${++gLastResourceId}`;
+ }
+
+ // Only consider top level document, and ignore remote iframes top document
+ const isWillNavigate =
+ resourceType == ResourceCommand.TYPES.DOCUMENT_EVENT &&
+ resource.name == "will-navigate";
+ if (isWillNavigate && resource.targetFront.isTopLevel) {
+ includesDocumentEventWillNavigate = true;
+ this._onWillNavigate(resource.targetFront);
+ }
+
+ if (
+ resourceType == ResourceCommand.TYPES.DOCUMENT_EVENT &&
+ resource.name == "dom-loading" &&
+ resource.targetFront.isTopLevel
+ ) {
+ includesDocumentEventDomLoading = true;
+ }
+
+ this._queueResourceEvent("available", resourceType, resource);
+
+ // Avoid storing will-navigate resource and consider it as a transcient resource.
+ // We do that to prevent leaking this resource (and its target) on navigation.
+ // We do clear the cache in _onWillNavigate, that we call a few lines before this.
+ if (!isWillNavigate) {
+ this.addResourceToCache(resource);
+ }
+ }
+
+ // If we receive the DOCUMENT_EVENT for:
+ // - will-navigate
+ // - dom-loading + we're using the service worker legacy listener
+ // then flush immediately the resources to notify about the navigation sooner than later.
+ // (this is especially useful for tests, even if they should probably avoid depending on this...)
+ if (
+ includesDocumentEventWillNavigate ||
+ (includesDocumentEventDomLoading &&
+ !this.targetCommand.hasTargetWatcherSupport("service_worker")) ||
+ this.throttlingDisabled
+ ) {
+ this._notifyWatchers();
+ } else {
+ this._throttledNotifyWatchers();
+ }
+ }
+
+ /**
+ * Method called either by:
+ * - the backward compatibility code (LegacyListeners)
+ * - target actors RDP events
+ * Called everytime a resource is updated in the remote target.
+ *
+ * @param {Object} source
+ * Please see _onResourceAvailable for this parameter.
+ * @param {Array<Object>} updates
+ * Depending on the listener.
+ *
+ * Among the element in the array, the following attributes are given special handling.
+ * - resourceType {String}:
+ * The type of resource to be updated.
+ * - resourceId {String}:
+ * The id of resource to be updated.
+ * - resourceUpdates {Object}:
+ * If resourceUpdates is in the element, a cached resource specified by resourceType
+ * and resourceId is updated by Object.assign(cachedResource, resourceUpdates).
+ * - nestedResourceUpdates {Object}:
+ * If `nestedResourceUpdates` is passed, update one nested attribute with a new value
+ * This allows updating one attribute of an object stored in a resource's attribute,
+ * as well as adding new elements to arrays.
+ * `path` is an array mentioning all nested attribute to walk through.
+ * `value` is the new nested attribute value to set.
+ *
+ * And also, the element is passed to the listener as it is as “update” object.
+ * So if we don't want to update a cached resource but have information want to
+ * pass on to the listener, can pass it on using attributes other than the ones
+ * listed above.
+ * For example, if the element consists of like
+ * "{ resourceType:… resourceId:…, testValue: “test”, }”,
+ * the listener can receive the value as follows.
+ *
+ * onResourceUpdate({ update }) {
+ * console.log(update.testValue); // “test” should be displayed
+ * }
+ */
+ async _onResourceUpdated({ targetFront, watcherFront }, updates) {
+ for (const update of updates) {
+ const {
+ resourceType,
+ resourceId,
+ resourceUpdates,
+ nestedResourceUpdates,
+ } = update;
+
+ if (!resourceId) {
+ console.warn(`Expected resource ${resourceType} to have a resourceId`);
+ }
+
+ // See _onResourceAvailable()
+ // We also need to wait for the related targetFront to be initialized
+ // otherwise we would notify about the udpate *before* the available
+ // and the resource won't be in _cache.
+ if (watcherFront) {
+ targetFront = await this._getTargetForWatcherResource(update);
+ // When we receive the navigation request, the target front has already been
+ // destroyed, but this is fine. The cached resource has the reference to
+ // the (destroyed) target front and it is fully initialized.
+ if (targetFront) {
+ await targetFront.initialized;
+ }
+ }
+
+ const existingResource = this._cache.get(
+ cacheKey(resourceType, resourceId)
+ );
+ if (!existingResource) {
+ continue;
+ }
+
+ if (resourceUpdates) {
+ Object.assign(existingResource, resourceUpdates);
+ }
+
+ if (nestedResourceUpdates) {
+ for (const { path, value } of nestedResourceUpdates) {
+ let target = existingResource;
+
+ for (let i = 0; i < path.length - 1; i++) {
+ target = target[path[i]];
+ }
+
+ target[path[path.length - 1]] = value;
+ }
+ }
+ this._queueResourceEvent("updated", resourceType, {
+ resource: existingResource,
+ update,
+ });
+ }
+ this._throttledNotifyWatchers();
+ }
+
+ /**
+ * Called everytime a resource is destroyed in the remote target.
+ * See _onResourceAvailable for the argument description.
+ */
+ async _onResourceDestroyed({ targetFront, watcherFront }, resources) {
+ for (const resource of resources) {
+ const { resourceType, resourceId } = resource;
+ this._cache.delete(cacheKey(resourceType, resourceId));
+ if (!resource.targetFront) {
+ resource.targetFront = targetFront;
+ }
+ this._queueResourceEvent("destroyed", resourceType, resource);
+ }
+ this._throttledNotifyWatchers();
+ }
+
+ _queueResourceEvent(callbackType, resourceType, update) {
+ for (const { resources, pendingEvents } of this._watchers) {
+ // This watcher doesn't listen to this type of resource
+ if (!resources.includes(resourceType)) {
+ continue;
+ }
+ // If we receive a new event of the same type, accumulate the new update in the last event
+ if (pendingEvents.length) {
+ const lastEvent = pendingEvents[pendingEvents.length - 1];
+ if (lastEvent.callbackType == callbackType) {
+ lastEvent.updates.push(update);
+ continue;
+ }
+ }
+ // Otherwise, pile up a new event, which will force calling watcher
+ // callback a new time
+ pendingEvents.push({
+ callbackType,
+ updates: [update],
+ });
+ }
+ }
+
+ /**
+ * Flush the pending event and notify all the currently registered watchers
+ * about all the available, updated and destroyed events that have been accumulated in
+ * `_watchers`'s `pendingEvents` arrays.
+ */
+ _notifyWatchers() {
+ for (const watcherEntry of this._watchers) {
+ const { onAvailable, onUpdated, onDestroyed, pendingEvents } =
+ watcherEntry;
+ // Immediately clear the buffer in order to avoid possible races, where an event listener
+ // would end up somehow adding a new throttled resource
+ watcherEntry.pendingEvents = [];
+
+ for (const { callbackType, updates } of pendingEvents) {
+ try {
+ if (callbackType == "available") {
+ onAvailable(updates, { areExistingResources: false });
+ } else if (callbackType == "updated" && onUpdated) {
+ onUpdated(updates);
+ } else if (callbackType == "destroyed" && onDestroyed) {
+ onDestroyed(updates);
+ }
+ } catch (e) {
+ console.error(
+ "Exception while calling a ResourceCommand",
+ callbackType,
+ "callback",
+ ":",
+ e
+ );
+ }
+ }
+ }
+ }
+
+ // Compute the target front if the resource comes from the Watcher Actor.
+ // (`targetFront` will be null as the watcher is in the parent process
+ // and targets are in distinct processes)
+ _getTargetForWatcherResource(resource) {
+ const { browsingContextID, innerWindowId, resourceType } = resource;
+
+ // Some privileged resources aren't related to any BrowsingContext
+ // and so aren't bound to any Target Front.
+ // Server watchers should pass an explicit "-1" value in order to prevent
+ // silently ignoring an undefined browsingContextID attribute.
+ if (browsingContextID == -1) {
+ return null;
+ }
+
+ if (innerWindowId && this.targetCommand.isServerTargetSwitchingEnabled()) {
+ return this.watcherFront.getWindowGlobalTargetByInnerWindowId(
+ innerWindowId
+ );
+ } else if (browsingContextID) {
+ return this.watcherFront.getWindowGlobalTarget(browsingContextID);
+ }
+ console.error(
+ `Resource of ${resourceType} is missing a browsingContextID or innerWindowId attribute`
+ );
+ return null;
+ }
+
+ _onWillNavigate(targetFront) {
+ // Special case for toolboxes debugging a document,
+ // purge the cache entirely when we start navigating to a new document.
+ // Other toolboxes and additional target for remote iframes or content process
+ // will be purge from onTargetDestroyed.
+
+ // NOTE: we could `clear` the cache here, but technically if anything is
+ // currently iterating over resources provided by getAllResources, that
+ // would interfere with their iteration. We just assign a new Map here to
+ // leave those iterators as is.
+ this._cache = new Map();
+ }
+
+ /**
+ * Tells if the server supports listening to the given resource type
+ * via the watcher actor's watchResources method.
+ *
+ * @return {Boolean} True, if the server supports this type.
+ */
+ hasResourceCommandSupport(resourceType) {
+ return this.watcherFront?.traits?.resources?.[resourceType];
+ }
+
+ /**
+ * Tells if the server supports listening to the given resource type
+ * via the watcher actor's watchResources method, and that, for a specific
+ * target.
+ *
+ * @return {Boolean} True, if the server supports this type.
+ */
+ _hasResourceCommandSupportForTarget(resourceType, targetFront) {
+ // First check if the watcher supports this target type.
+ // If it doesn't, no resource type can be listened via the Watcher actor for this target.
+ if (!this.targetCommand.hasTargetWatcherSupport(targetFront.targetType)) {
+ return false;
+ }
+
+ return this.hasResourceCommandSupport(resourceType);
+ }
+
+ _isValidResourceType(type) {
+ return this.ALL_TYPES.includes(type);
+ }
+
+ /**
+ * Start listening for a given type of resource.
+ * For backward compatibility code, we register the legacy listeners on
+ * each individual target
+ *
+ * @param {String} resourceType
+ * One string of ResourceCommand.TYPES, which designates the types of resources
+ * to be listened.
+ * @param {Object}
+ * - {Boolean} bypassListenerCount
+ * Pass true to avoid checking/updating the listenersCount map.
+ * Exclusively used when target switching, to stop & start listening
+ * to all resources.
+ */
+ async _startListening(resourceType, { bypassListenerCount = false } = {}) {
+ if (!bypassListenerCount) {
+ if (this._listenedResources.has(resourceType)) {
+ return;
+ }
+ this._listenedResources.add(resourceType);
+ }
+
+ this._processingExistingResources.add(resourceType);
+
+ // Ensuring enabling listening to targets.
+ // This will be a no-op expect for the very first call to `_startListening`,
+ // where it is going to call `onTargetAvailable` for all already existing targets,
+ // as well as for those who will be created later.
+ //
+ // Do this *before* calling WatcherActor.watchResources in order to register "resource-available"
+ // listeners on targets before these events start being emitted.
+ await this._watchAllTargets(resourceType);
+
+ // When we are calling _startListening for the first time, _watchAllTargets
+ // will register legacylistener when it will call onTargetAvailable for all existing targets.
+ // But for any next calls to _startListening, _watchAllTargets will be a no-op,
+ // and nothing will start legacy listener for each already registered targets.
+ await this._startLegacyListenersForExistingTargets(resourceType);
+
+ // If the server supports the Watcher API and the Watcher supports
+ // this resource type, use this API
+ if (this.hasResourceCommandSupport(resourceType)) {
+ await this.watcherFront.watchResources([resourceType]);
+ }
+ this._processingExistingResources.delete(resourceType);
+ }
+
+ /**
+ * Return true if the resource should be watched via legacy listener,
+ * even when watcher supports this resource type.
+ *
+ * Bug 1678385: In order to support watching for JS Source resource
+ * for service workers and parent process workers, which aren't supported yet
+ * by the watcher actor, we do not bail out here and allow to execute
+ * the legacy listener for these targets.
+ * Once bug 1608848 is fixed, we can remove this and never trigger
+ * the legacy listeners codepath for these resource types.
+ *
+ * If this isn't fixed soon, we may add other resources we want to see
+ * being fetched from these targets.
+ */
+ _shouldRunLegacyListenerEvenWithWatcherSupport(resourceType) {
+ return WORKER_RESOURCE_TYPES.includes(resourceType);
+ }
+
+ async _forwardExistingResources(resourceTypes, onAvailable) {
+ const existingResources = [];
+ for (const resource of this._cache.values()) {
+ if (resourceTypes.includes(resource.resourceType)) {
+ existingResources.push(resource);
+ }
+ }
+ if (existingResources.length) {
+ await onAvailable(existingResources, { areExistingResources: true });
+ }
+ }
+
+ /**
+ * Call backward compatibility code from `LegacyListeners` in order to listen for a given
+ * type of resource from a given target.
+ */
+ async _watchResourcesForTarget({
+ targetFront,
+ resourceType,
+ disableWarning = false,
+ }) {
+ if (this._hasResourceCommandSupportForTarget(resourceType, targetFront)) {
+ // This resource / target pair should already be handled by the watcher,
+ // no need to start legacy listeners.
+ return;
+ }
+
+ // All workers target types are still not supported by the watcher
+ // so that we have to spawn legacy listener for all their resources.
+ // But some resources are irrelevant to workers, like network events.
+ // And we removed the related legacy listener as they are no longer used.
+ if (
+ targetFront.targetType.endsWith("worker") &&
+ !WORKER_RESOURCE_TYPES.includes(resourceType)
+ ) {
+ return;
+ }
+
+ if (targetFront.isDestroyed()) {
+ return;
+ }
+
+ const onAvailable = this._onResourceAvailable.bind(this, { targetFront });
+ const onUpdated = this._onResourceUpdated.bind(this, { targetFront });
+ const onDestroyed = this._onResourceDestroyed.bind(this, { targetFront });
+
+ if (!(resourceType in LegacyListeners)) {
+ throw new Error(`Missing legacy listener for ${resourceType}`);
+ }
+
+ const legacyListeners =
+ this._existingLegacyListeners.get(targetFront) || [];
+ if (legacyListeners.includes(resourceType)) {
+ if (!disableWarning) {
+ console.warn(
+ `Already started legacy listener for ${resourceType} on ${targetFront.actorID}`
+ );
+ }
+ return;
+ }
+ this._existingLegacyListeners.set(
+ targetFront,
+ legacyListeners.concat(resourceType)
+ );
+
+ try {
+ await LegacyListeners[resourceType]({
+ targetCommand: this.targetCommand,
+ targetFront,
+ onAvailable,
+ onDestroyed,
+ onUpdated,
+ });
+ } catch (e) {
+ // Swallow the error to avoid breaking calls to watchResources which will
+ // loop on all existing targets to create legacy listeners.
+ // If a legacy listener fails to handle a target for some reason, we
+ // should still try to process other targets as much as possible.
+ // See Bug 1687645.
+ console.error(
+ `Failed to start [${resourceType}] legacy listener for target ${targetFront.actorID}`,
+ e
+ );
+ }
+ }
+
+ /**
+ * Reverse of _startListening. Stop listening for a given type of resource.
+ * For backward compatibility, we unregister from each individual target.
+ *
+ * See _startListening for parameters description.
+ */
+ _stopListening(resourceType, { bypassListenerCount = false } = {}) {
+ if (!bypassListenerCount) {
+ if (!this._listenedResources.has(resourceType)) {
+ throw new Error(
+ `Stopped listening for resource '${resourceType}' that isn't being listened to`
+ );
+ }
+ this._listenedResources.delete(resourceType);
+ }
+
+ // Clear the cached resources of the type.
+ for (const [key, resource] of this._cache) {
+ if (resource.resourceType == resourceType) {
+ // NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
+ this._cache.delete(key);
+ }
+ }
+
+ // If the server supports the Watcher API and the Watcher supports
+ // this resource type, use this API
+ if (this.hasResourceCommandSupport(resourceType)) {
+ if (!this.watcherFront.isDestroyed()) {
+ this.watcherFront.unwatchResources([resourceType]);
+ }
+
+ const shouldRunLegacyListeners =
+ this._shouldRunLegacyListenerEvenWithWatcherSupport(resourceType);
+ if (!shouldRunLegacyListeners) {
+ return;
+ }
+ }
+ // Otherwise, fallback on backward compat mode and use LegacyListeners.
+
+ // If this was the last listener, we should stop watching these events from the actors
+ // and the actors should stop watching things from the platform
+ const targets = this.targetCommand.getAllTargets(
+ this.targetCommand.ALL_TYPES
+ );
+ for (const target of targets) {
+ this._unwatchResourcesForTarget(target, resourceType);
+ }
+ }
+
+ /**
+ * Backward compatibility code, reverse of _watchResourcesForTarget.
+ */
+ _unwatchResourcesForTarget(targetFront, resourceType) {
+ if (this._hasResourceCommandSupportForTarget(resourceType, targetFront)) {
+ // This resource / target pair should already be handled by the watcher,
+ // no need to stop legacy listeners.
+ }
+ // Is there really a point in:
+ // - unregistering `onAvailable` RDP event callbacks from target-scoped actors?
+ // - calling `stopListeners()` as we are most likely closing the toolbox and destroying everything?
+ //
+ // It is important to keep this method synchronous and do as less as possible
+ // in the case of toolbox destroy.
+ //
+ // We are aware of one case where that might be useful.
+ // When a panel is disabled via the options panel, after it has been opened.
+ // Would that justify doing this? Is there another usecase?
+
+ // XXX: This is most likely only needed to avoid growing the Map infinitely.
+ // Unless in the "disabled panel" use case mentioned in the comment above,
+ // we should not see the same target actorID again.
+ const listeners = this._existingLegacyListeners.get(targetFront);
+ if (listeners && listeners.includes(resourceType)) {
+ const remainingListeners = listeners.filter(l => l !== resourceType);
+ this._existingLegacyListeners.set(targetFront, remainingListeners);
+ }
+ }
+}
+
+ResourceCommand.TYPES = ResourceCommand.prototype.TYPES = {
+ CONSOLE_MESSAGE: "console-message",
+ CSS_CHANGE: "css-change",
+ CSS_MESSAGE: "css-message",
+ CSS_REGISTERED_PROPERTIES: "css-registered-properties",
+ ERROR_MESSAGE: "error-message",
+ PLATFORM_MESSAGE: "platform-message",
+ DOCUMENT_EVENT: "document-event",
+ ROOT_NODE: "root-node",
+ STYLESHEET: "stylesheet",
+ NETWORK_EVENT: "network-event",
+ WEBSOCKET: "websocket",
+ COOKIE: "cookies",
+ LOCAL_STORAGE: "local-storage",
+ SESSION_STORAGE: "session-storage",
+ CACHE_STORAGE: "Cache",
+ EXTENSION_STORAGE: "extension-storage",
+ INDEXED_DB: "indexed-db",
+ NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
+ REFLOW: "reflow",
+ SOURCE: "source",
+ THREAD_STATE: "thread-state",
+ JSTRACER_TRACE: "jstracer-trace",
+ JSTRACER_STATE: "jstracer-state",
+ SERVER_SENT_EVENT: "server-sent-event",
+ LAST_PRIVATE_CONTEXT_EXIT: "last-private-context-exit",
+};
+ResourceCommand.ALL_TYPES = ResourceCommand.prototype.ALL_TYPES = Object.values(
+ ResourceCommand.TYPES
+);
+module.exports = ResourceCommand;
+
+// This is the list of resource types supported by workers.
+// We need such list to know when forcing to run the legacy listeners
+// and when to avoid try to spawn some unsupported ones for workers.
+const WORKER_RESOURCE_TYPES = [
+ ResourceCommand.TYPES.CONSOLE_MESSAGE,
+ ResourceCommand.TYPES.ERROR_MESSAGE,
+ ResourceCommand.TYPES.SOURCE,
+ ResourceCommand.TYPES.THREAD_STATE,
+];
+
+// Backward compat code for each type of resource.
+// Each section added here should eventually be removed once the equivalent server
+// code is implement in Firefox, in its release channel.
+const LegacyListeners = {
+ async [ResourceCommand.TYPES.DOCUMENT_EVENT]({
+ targetCommand,
+ targetFront,
+ onAvailable,
+ }) {
+ // DocumentEventsListener of webconsole handles only top level document.
+ if (!targetFront.isTopLevel) {
+ return;
+ }
+
+ const webConsoleFront = await targetFront.getFront("console");
+ webConsoleFront.on("documentEvent", event => {
+ event.resourceType = ResourceCommand.TYPES.DOCUMENT_EVENT;
+ onAvailable([event]);
+ });
+ await webConsoleFront.startListeners(["DocumentEvents"]);
+ },
+};
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.CONSOLE_MESSAGE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/console-messages.js"
+);
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.CSS_CHANGE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/css-changes.js"
+);
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.CSS_MESSAGE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/css-messages.js"
+);
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.ERROR_MESSAGE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/error-messages.js"
+);
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/platform-messages.js"
+);
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.ROOT_NODE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/root-node.js"
+);
+
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.SOURCE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/source.js"
+);
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.THREAD_STATE,
+ "resource://devtools/shared/commands/resource/legacy-listeners/thread-states.js"
+);
+
+loader.lazyRequireGetter(
+ LegacyListeners,
+ ResourceCommand.TYPES.REFLOW,
+ "resource://devtools/shared/commands/resource/legacy-listeners/reflow.js"
+);
+
+// Optional transformers for each type of resource.
+// Each module added here should be a function that will receive the resource, the target, …
+// and perform some transformation on the resource before it will be emitted.
+// This is a good place to handle backward compatibility and manual resource marshalling.
+const ResourceTransformers = {};
+
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.CONSOLE_MESSAGE,
+ "resource://devtools/shared/commands/resource/transformers/console-messages.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.ERROR_MESSAGE,
+ "resource://devtools/shared/commands/resource/transformers/error-messages.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.CACHE_STORAGE,
+ "resource://devtools/shared/commands/resource/transformers/storage-cache.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.COOKIE,
+ "resource://devtools/shared/commands/resource/transformers/storage-cookie.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.EXTENSION_STORAGE,
+ "resource://devtools/shared/commands/resource/transformers/storage-extension.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.INDEXED_DB,
+ "resource://devtools/shared/commands/resource/transformers/storage-indexed-db.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.LOCAL_STORAGE,
+ "resource://devtools/shared/commands/resource/transformers/storage-local-storage.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.SESSION_STORAGE,
+ "resource://devtools/shared/commands/resource/transformers/storage-session-storage.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.NETWORK_EVENT,
+ "resource://devtools/shared/commands/resource/transformers/network-events.js"
+);
+loader.lazyRequireGetter(
+ ResourceTransformers,
+ ResourceCommand.TYPES.THREAD_STATE,
+ "resource://devtools/shared/commands/resource/transformers/thread-states.js"
+);
diff --git a/devtools/shared/commands/resource/tests/breakpoint_document.html b/devtools/shared/commands/resource/tests/breakpoint_document.html
new file mode 100644
index 0000000000..2291094646
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/breakpoint_document.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Test breakpoint document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <script>
+ "use strict";
+ /* eslint-disable */
+ function testFunction() {
+ console.log("test Function ran");
+ }
+ function runDebuggerStatement() {
+ debugger;
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/browser.toml b/devtools/shared/commands/resource/tests/browser.toml
new file mode 100644
index 0000000000..def009b710
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser.toml
@@ -0,0 +1,128 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+ "head.js",
+ "breakpoint_document.html",
+ "doc_console.html",
+ "doc_console_iframe.html",
+ "empty.html",
+ "network_document.html",
+ "network_document_navigation.html",
+ "network_navigation.js",
+ "early_console_document.html",
+ "fission_document.html",
+ "fission_document_workers.html",
+ "fission_iframe.html",
+ "fission_iframe_workers.html",
+ "service-worker-sources.js",
+ "sources.html",
+ "sources.js",
+ "sse_backend.sjs",
+ "sse_frontend_iframe.html",
+ "sse_frontend.html",
+ "style_document.css",
+ "style_document.html",
+ "style_iframe.css",
+ "style_iframe.html",
+ "stylesheets-nested-iframes.html",
+ "test_image.png",
+ "test_service_worker.js",
+ "test_worker.js",
+ "websocket_backend_wsh.py",
+ "websocket_frontend_iframe.html",
+ "websocket_frontend.html",
+ "worker-sources.js",
+]
+
+["browser_browser_resources_console_messages.js"]
+
+["browser_resources_clear_resources.js"]
+
+["browser_resources_client_caching.js"]
+
+["browser_resources_console_messages.js"]
+
+["browser_resources_console_messages_navigation.js"]
+
+["browser_resources_console_messages_workers.js"]
+
+["browser_resources_css_changes.js"]
+
+["browser_resources_css_messages.js"]
+
+["browser_resources_css_registered_properties.js"]
+skip-if = ["!fission"]
+
+["browser_resources_document_events.js"]
+skip-if = [
+ "os == 'linux' && bits == 64", # Bug 1715878
+]
+
+["browser_resources_error_messages.js"]
+
+["browser_resources_getAllResources.js"]
+
+["browser_resources_invalid_api_usage.js"]
+
+["browser_resources_last_private_context_exit.js"]
+
+["browser_resources_network_event_stacktraces.js"]
+
+["browser_resources_network_events.js"]
+
+["browser_resources_network_events_cache.js"]
+
+["browser_resources_network_events_navigation.js"]
+
+["browser_resources_network_events_parent_process.js"]
+
+["browser_resources_platform_messages.js"]
+
+["browser_resources_reflows.js"]
+
+["browser_resources_root_node.js"]
+
+["browser_resources_scope_flag.js"]
+
+["browser_resources_server_sent_events.js"]
+
+["browser_resources_several_resources.js"]
+
+["browser_resources_sources.js"]
+skip-if = [
+ "os == 'linux' && bits == 64", # Bug 1744565
+ "win11_2009", # Bug 1767772
+ "apple_catalina", # Bug 1767772
+]
+
+["browser_resources_stylesheets.js"]
+
+["browser_resources_stylesheets_header.js"]
+
+["browser_resources_stylesheets_import.js"]
+
+["browser_resources_stylesheets_navigation.js"]
+
+["browser_resources_stylesheets_nested_iframes.js"]
+
+["browser_resources_target_destroy.js"]
+
+["browser_resources_target_resources_race.js"]
+
+["browser_resources_target_switching.js"]
+
+["browser_resources_thread_states.js"]
+
+["browser_resources_unwatch_early.js"]
+
+["browser_resources_watch_unwatch_multiple.js"]
+
+["browser_resources_websocket.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
diff --git a/devtools/shared/commands/resource/tests/browser_browser_resources_console_messages.js b/devtools/shared/commands/resource/tests/browser_browser_resources_console_messages.js
new file mode 100644
index 0000000000..1c6c776e64
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_browser_resources_console_messages.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around CONSOLE_MESSAGE for the whole browser
+
+const TEST_URL = URL_ROOT_SSL + "early_console_document.html";
+
+add_task(async function () {
+ // Enable Multiprocess Browser Toolbox.
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const { client, resourceCommand, targetCommand } =
+ await initMultiProcessResourceCommand();
+
+ const d = Date.now();
+ const CACHED_MESSAGE_TEXT = `cached-${d}`;
+ const LIVE_MESSAGE_TEXT = `live-${d}`;
+
+ info(
+ "Log some messages *before* calling ResourceCommand.watchResources in order to " +
+ "assert the behavior of already existing messages."
+ );
+ console.log(CACHED_MESSAGE_TEXT);
+
+ info("Wait for existing browser mochitest log");
+ const { onResource } = await resourceCommand.waitForNextResource(
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ {
+ ignoreExistingResources: false,
+ predicate({ message }) {
+ return message.arguments[0] === CACHED_MESSAGE_TEXT;
+ },
+ }
+ );
+ const existingMsg = await onResource;
+ ok(existingMsg, "The existing log was retrieved");
+ is(
+ existingMsg.isAlreadyExistingResource,
+ true,
+ "isAlreadyExistingResource is true for the existing message"
+ );
+
+ const { onResource: onMochitestRuntimeLog } =
+ await resourceCommand.waitForNextResource(
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ {
+ ignoreExistingResources: false,
+ predicate({ message }) {
+ return message.arguments[0] === LIVE_MESSAGE_TEXT;
+ },
+ }
+ );
+ console.log(LIVE_MESSAGE_TEXT);
+
+ info("Wait for runtime browser mochitest log");
+ const runtimeLogResource = await onMochitestRuntimeLog;
+ ok(runtimeLogResource, "The runtime log was retrieved");
+ is(
+ runtimeLogResource.isAlreadyExistingResource,
+ false,
+ "isAlreadyExistingResource is false for the runtime message"
+ );
+
+ const { onResource: onEarlyLog } = await resourceCommand.waitForNextResource(
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ {
+ ignoreExistingResources: true,
+ predicate({ message }) {
+ return message.arguments[0] === "early-page-log";
+ },
+ }
+ );
+ await addTab(TEST_URL);
+ info("Wait for early page log");
+ const earlyResource = await onEarlyLog;
+ ok(earlyResource, "The early page log was retrieved");
+ is(
+ earlyResource.isAlreadyExistingResource,
+ false,
+ "isAlreadyExistingResource is false for the early message"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_clear_resources.js b/devtools/shared/commands/resource/tests/browser_resources_clear_resources.js
new file mode 100644
index 0000000000..44068cb141
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_clear_resources.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the clearResources function of the ResourceCommand
+
+add_task(async () => {
+ const tab = await addTab(`${URL_ROOT_SSL}empty.html`);
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Assert the initial no of resources");
+ assertNoOfResources(resourceCommand, 0, 0);
+
+ const onAvailable = () => {};
+ const onUpdated = () => {};
+
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ ],
+ { onAvailable, onUpdated }
+ );
+
+ info("Log some messages");
+ await logConsoleMessages(tab.linkedBrowser, ["log1", "log2", "log3"]);
+
+ info("Trigger some network requests");
+ const EXAMPLE_DOMAIN = "https://example.com/";
+ await triggerNetworkRequests(tab.linkedBrowser, [
+ `await fetch("${EXAMPLE_DOMAIN}/request1.html", { method: "GET" });`,
+ `await fetch("${EXAMPLE_DOMAIN}/request2.html", { method: "GET" });`,
+ ]);
+
+ assertNoOfResources(resourceCommand, 3, 2);
+
+ info("Clear the network event resources");
+ await resourceCommand.clearResources([resourceCommand.TYPES.NETWORK_EVENT]);
+ assertNoOfResources(resourceCommand, 3, 0);
+
+ info("Clear the console message resources");
+ await resourceCommand.clearResources([resourceCommand.TYPES.CONSOLE_MESSAGE]);
+ assertNoOfResources(resourceCommand, 0, 0);
+
+ resourceCommand.unwatchResources(
+ [
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ ],
+ { onAvailable, onUpdated, ignoreExistingResources: true }
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+function assertNoOfResources(
+ resourceCommand,
+ expectedNoOfConsoleMessageResources,
+ expectedNoOfNetworkEventResources
+) {
+ const actualNoOfConsoleMessageResources = resourceCommand.getAllResources(
+ resourceCommand.TYPES.CONSOLE_MESSAGE
+ ).length;
+ is(
+ actualNoOfConsoleMessageResources,
+ expectedNoOfConsoleMessageResources,
+ `There are ${actualNoOfConsoleMessageResources} console messages resources`
+ );
+
+ const actualNoOfNetworkEventResources = resourceCommand.getAllResources(
+ resourceCommand.TYPES.NETWORK_EVENT
+ ).length;
+ is(
+ actualNoOfNetworkEventResources,
+ expectedNoOfNetworkEventResources,
+ `There are ${actualNoOfNetworkEventResources} network event resources`
+ );
+}
+
+function logConsoleMessages(browser, messages) {
+ return SpecialPowers.spawn(browser, [messages], innerMessages => {
+ for (const message of innerMessages) {
+ content.console.log(message);
+ }
+ });
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_client_caching.js b/devtools/shared/commands/resource/tests/browser_resources_client_caching.js
new file mode 100644
index 0000000000..ae398f73cc
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_client_caching.js
@@ -0,0 +1,380 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the cache mechanism of the ResourceCommand.
+
+const TEST_URI = "data:text/html;charset=utf-8,<!DOCTYPE html>Cache Test";
+
+add_task(async function () {
+ info("Test whether multiple listener can get same cached resources");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const messages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, messages);
+
+ info("Register first listener");
+ const cachedResources1 = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable(resources, { areExistingResources }) {
+ ok(areExistingResources, "All resources are already existing ones");
+ cachedResources1.push(...resources);
+ },
+ }
+ );
+
+ info("Register second listener");
+ const cachedResources2 = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable(resources, { areExistingResources }) {
+ ok(areExistingResources, "All resources are already existing ones");
+ cachedResources2.push(...resources);
+ },
+ }
+ );
+
+ assertContents(cachedResources1, messages);
+ assertResources(cachedResources2, cachedResources1);
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+add_task(async function () {
+ info(
+ "Test whether the cache is reflecting existing resources and additional resources"
+ );
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const existingMessages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, existingMessages);
+
+ info("Register first listener to get all available resources");
+ const availableResources = [];
+ // We first get notified about existing resources
+ let shouldBeExistingResources = true;
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable(resources, { areExistingResources }) {
+ is(
+ areExistingResources,
+ shouldBeExistingResources,
+ "areExistingResources flag is correct"
+ );
+ availableResources.push(...resources);
+ },
+ }
+ );
+ // Then, we are notified about, new, live ones
+ shouldBeExistingResources = false;
+
+ info("Add messages as additional resources");
+ const additionalMessages = ["d", "e"];
+ await logMessages(tab.linkedBrowser, additionalMessages);
+
+ info("Wait until onAvailable is called expected times");
+ const allMessages = [...existingMessages, ...additionalMessages];
+ await waitUntil(() => availableResources.length === allMessages.length);
+
+ info("Register second listener to get the cached resources");
+ const cachedResources = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable(resources, { areExistingResources }) {
+ ok(areExistingResources, "All resources are already existing ones");
+ cachedResources.push(...resources);
+ },
+ }
+ );
+
+ assertContents(availableResources, allMessages);
+ assertResources(cachedResources, availableResources);
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+add_task(async function () {
+ info("Test whether the cache is cleared when navigation");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const existingMessages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, existingMessages);
+
+ info("Register first listener");
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: () => {},
+ }
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab);
+
+ info("Register second listener");
+ const cachedResources = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources.push(...resources),
+ }
+ );
+
+ is(cachedResources.length, 0, "The cache in ResourceCommand is cleared");
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+add_task(async function () {
+ info("Test with multiple resource types");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Register first listener to get all available resources");
+ const availableResources = [];
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ resourceCommand.TYPES.ERROR_MESSAGE,
+ ],
+ {
+ onAvailable: resources => availableResources.push(...resources),
+ }
+ );
+
+ info("Add messages as console message");
+ const consoleMessages1 = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, consoleMessages1);
+
+ info("Add message as error message");
+ const errorMessages = ["document.doTheImpossible();"];
+ await triggerErrors(tab.linkedBrowser, errorMessages);
+
+ info("Add messages as console message again");
+ const consoleMessages2 = ["d", "e"];
+ await logMessages(tab.linkedBrowser, consoleMessages2);
+
+ info("Wait until the getting all available resources");
+ const totalResourceCount =
+ consoleMessages1.length + errorMessages.length + consoleMessages2.length;
+ await waitUntil(() => {
+ return availableResources.length === totalResourceCount;
+ });
+
+ info("Register listener to get the cached resources");
+ const cachedResources = [];
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ resourceCommand.TYPES.ERROR_MESSAGE,
+ ],
+ {
+ onAvailable: resources => cachedResources.push(...resources),
+ }
+ );
+
+ assertResources(cachedResources, availableResources);
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+add_task(async function () {
+ info("Test multiple listeners with/without ignoreExistingResources");
+ await testIgnoreExistingResources(true);
+ await testIgnoreExistingResources(false);
+});
+
+async function testIgnoreExistingResources(isFirstListenerIgnoreExisting) {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const existingMessages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, existingMessages);
+
+ info("Register first listener");
+ const cachedResources1 = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources1.push(...resources),
+ ignoreExistingResources: isFirstListenerIgnoreExisting,
+ }
+ );
+
+ info("Register second listener");
+ const cachedResources2 = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources2.push(...resources),
+ ignoreExistingResources: !isFirstListenerIgnoreExisting,
+ }
+ );
+
+ const cachedResourcesWithFlag = isFirstListenerIgnoreExisting
+ ? cachedResources1
+ : cachedResources2;
+ const cachedResourcesWithoutFlag = isFirstListenerIgnoreExisting
+ ? cachedResources2
+ : cachedResources1;
+
+ info("Check the existing resources both listeners got");
+ assertContents(cachedResourcesWithFlag, []);
+ assertContents(cachedResourcesWithoutFlag, existingMessages);
+
+ info("Add messages as additional resources");
+ const additionalMessages = ["d", "e"];
+ await logMessages(tab.linkedBrowser, additionalMessages);
+
+ info("Wait until onAvailable is called expected times");
+ await waitUntil(
+ () => cachedResourcesWithFlag.length === additionalMessages.length
+ );
+ const allMessages = [...existingMessages, ...additionalMessages];
+ await waitUntil(
+ () => cachedResourcesWithoutFlag.length === allMessages.length
+ );
+
+ info("Check the resources after adding messages");
+ assertContents(cachedResourcesWithFlag, additionalMessages);
+ assertContents(cachedResourcesWithoutFlag, allMessages);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+add_task(async function () {
+ info("Test that onAvailable is not called with an empty resources array");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Register first listener to get all available resources");
+ const availableResources = [];
+ let onAvailableCallCount = 0;
+ const onAvailable = resources => {
+ ok(
+ !!resources.length,
+ "onAvailable is called with a non empty resources array"
+ );
+ availableResources.push(...resources);
+ onAvailableCallCount++;
+ };
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ { onAvailable }
+ );
+ is(availableResources.length, 0, "availableResources array is empty");
+ is(onAvailableCallCount, 0, "onAvailable was never called");
+
+ info("Add messages as console message");
+ await logMessages(tab.linkedBrowser, ["expected message"]);
+
+ await waitUntil(() => availableResources.length === 1);
+ is(availableResources.length, 1, "availableResources array has one item");
+ is(onAvailableCallCount, 1, "onAvailable was called only once");
+ is(
+ availableResources[0].message.arguments[0],
+ "expected message",
+ "onAvailable was called with the expected resource"
+ );
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+ targetCommand.destroy();
+ await client.close();
+});
+
+function assertContents(resources, expectedMessages) {
+ is(
+ resources.length,
+ expectedMessages.length,
+ "Number of the resources is correct"
+ );
+
+ for (let i = 0; i < expectedMessages.length; i++) {
+ const resource = resources[i];
+ const message = resource.message.arguments[0];
+ const expectedMessage = expectedMessages[i];
+ is(message, expectedMessage, `The ${i}th content is correct`);
+ }
+}
+
+function assertResources(resources, expectedResources) {
+ is(
+ resources.length,
+ expectedResources.length,
+ "Number of the resources is correct"
+ );
+
+ for (let i = 0; i < resources.length; i++) {
+ const resource = resources[i];
+ const expectedResource = expectedResources[i];
+ Assert.strictEqual(
+ resource,
+ expectedResource,
+ `The ${i}th resource is correct`
+ );
+ }
+}
+
+function logMessages(browser, messages) {
+ return ContentTask.spawn(browser, { messages }, args => {
+ for (const message of args.messages) {
+ content.console.log(message);
+ }
+ });
+}
+
+async function triggerErrors(browser, errorScripts) {
+ for (const errorScript of errorScripts) {
+ await ContentTask.spawn(browser, errorScript, expr => {
+ const document = content.document;
+ const container = document.createElement("script");
+ document.body.appendChild(container);
+ container.textContent = expr;
+ container.remove();
+ });
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_console_messages.js b/devtools/shared/commands/resource/tests/browser_resources_console_messages.js
new file mode 100644
index 0000000000..6f02cd5a77
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_console_messages.js
@@ -0,0 +1,623 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around CONSOLE_MESSAGE
+//
+// Reproduces assertions from: devtools/shared/webconsole/test/chrome/test_cached_messages.html
+// And now more. Once we remove the console actor's startListeners in favor of watcher class
+// We could remove that other old test.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const IFRAME_URL = URL_ROOT_ORG_SSL + "fission_iframe.html";
+
+add_task(async function () {
+ info("Execute test in top level document");
+ await testTabConsoleMessagesResources(false);
+ await testTabConsoleMessagesResourcesWithIgnoreExistingResources(false);
+
+ info("Execute test in an iframe document, possibly remote with fission");
+ await testTabConsoleMessagesResources(true);
+ await testTabConsoleMessagesResourcesWithIgnoreExistingResources(true);
+});
+
+async function testTabConsoleMessagesResources(executeInIframe) {
+ const tab = await addTab(FISSION_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info(
+ "Log some messages *before* calling ResourceCommand.watchResources in order to " +
+ "assert the behavior of already existing messages."
+ );
+ await logExistingMessages(tab.linkedBrowser, executeInIframe);
+
+ const targetDocumentUrl = executeInIframe ? IFRAME_URL : FISSION_TEST_URL;
+
+ let runtimeDoneResolve;
+ const expectedExistingCalls =
+ getExpectedExistingConsoleCalls(targetDocumentUrl);
+ const expectedRuntimeCalls =
+ getExpectedRuntimeConsoleCalls(targetDocumentUrl);
+ const onRuntimeDone = new Promise(resolve => (runtimeDoneResolve = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.resourceType,
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ "Received a message"
+ );
+ ok(resource.message, "message is wrapped into a message attribute");
+ const isCachedMessage = !!expectedExistingCalls.length;
+ const expected = (
+ isCachedMessage ? expectedExistingCalls : expectedRuntimeCalls
+ ).shift();
+ checkConsoleAPICall(resource.message, expected);
+ is(
+ resource.isAlreadyExistingResource,
+ isCachedMessage,
+ "isAlreadyExistingResource has the expected value"
+ );
+
+ if (!expectedRuntimeCalls.length) {
+ runtimeDoneResolve();
+ }
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+ is(
+ expectedExistingCalls.length,
+ 0,
+ "Got the expected number of existing messages"
+ );
+
+ info(
+ "Now log messages *after* the call to ResourceCommand.watchResources and after having received all existing messages"
+ );
+ await logRuntimeMessages(tab.linkedBrowser, executeInIframe);
+
+ info("Waiting for all runtime messages");
+ await onRuntimeDone;
+
+ is(
+ expectedRuntimeCalls.length,
+ 0,
+ "Got the expected number of runtime messages"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testTabConsoleMessagesResourcesWithIgnoreExistingResources(
+ executeInIframe
+) {
+ info("Test ignoreExistingResources option for console messages");
+ const tab = await addTab(FISSION_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info(
+ "Check whether onAvailable will not be called with existing console messages"
+ );
+ await logExistingMessages(tab.linkedBrowser, executeInIframe);
+
+ const availableResources = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => availableResources.push(...resources),
+ ignoreExistingResources: true,
+ }
+ );
+ is(
+ availableResources.length,
+ 0,
+ "onAvailable wasn't called for existing console messages"
+ );
+
+ info(
+ "Check whether onAvailable will be called with the future console messages"
+ );
+ await logRuntimeMessages(tab.linkedBrowser, executeInIframe);
+ const targetDocumentUrl = executeInIframe ? IFRAME_URL : FISSION_TEST_URL;
+ const expectedRuntimeConsoleCalls =
+ getExpectedRuntimeConsoleCalls(targetDocumentUrl);
+ await waitUntil(
+ () => availableResources.length === expectedRuntimeConsoleCalls.length
+ );
+ const expectedTargetFront =
+ executeInIframe && (isFissionEnabled() || isEveryFrameTargetEnabled())
+ ? targetCommand
+ .getAllTargets([targetCommand.TYPES.FRAME])
+ .find(target => target.url == IFRAME_URL)
+ : targetCommand.targetFront;
+ for (let i = 0; i < expectedRuntimeConsoleCalls.length; i++) {
+ const resource = availableResources[i];
+ const { message, targetFront } = resource;
+ is(
+ targetFront,
+ expectedTargetFront,
+ "The targetFront property is the expected one"
+ );
+ const expected = expectedRuntimeConsoleCalls[i];
+ checkConsoleAPICall(message, expected);
+ is(
+ resource.isAlreadyExistingResource,
+ false,
+ "isAlreadyExistingResource is false since we're ignoring existing resources"
+ );
+ }
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function logExistingMessages(browser, executeInIframe) {
+ let browsingContext = browser.browsingContext;
+ if (executeInIframe) {
+ browsingContext = await SpecialPowers.spawn(
+ browser,
+ [],
+ function frameScript() {
+ return content.document.querySelector("iframe").browsingContext;
+ }
+ );
+ }
+ return evalInBrowsingContext(browsingContext, function pageScript() {
+ console.log("foobarBaz-log", undefined);
+ console.info("foobarBaz-info", null);
+ console.warn("foobarBaz-warn", document.body);
+ });
+}
+
+/**
+ * Helper function similar to spawn, but instead of executing the script
+ * as a Frame Script, with privileges and including test harness in stacktraces,
+ * execute the script as a regular page script, without privileges and without any
+ * preceding stack.
+ *
+ * @param {BrowsingContext} The browsing context into which the script should be evaluated
+ * @param {Function|String} The JS to execute in the browsing context
+ *
+ * @return {Promise} Which resolves once the JS is done executing in the page
+ */
+function evalInBrowsingContext(browsingContext, script) {
+ return SpecialPowers.spawn(browsingContext, [String(script)], expr => {
+ const document = content.document;
+ const scriptEl = document.createElement("script");
+ document.body.appendChild(scriptEl);
+ // Force the immediate execution of the stringified JS function passed in `expr`
+ scriptEl.textContent = "new " + expr;
+ scriptEl.remove();
+ });
+}
+
+// For both existing and runtime messages, we execute console API
+// from a page script evaluated via evalInBrowsingContext.
+// Records here the function used to execute the script in the page.
+const EXPECTED_FUNCTION_NAME = "pageScript";
+
+const NUMBER_REGEX = /^\d+$/;
+// timeStamp are the result of a number in microsecond divided by 1000.
+// so we can't expect a precise number of decimals, or even if there would
+// be decimals at all.
+const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;
+
+function getExpectedExistingConsoleCalls(documentFilename) {
+ const defaultProperties = {
+ filename: documentFilename,
+ columnNumber: NUMBER_REGEX,
+ lineNumber: NUMBER_REGEX,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ chromeContext: undefined,
+ counter: undefined,
+ prefix: undefined,
+ private: undefined,
+ stacktrace: undefined,
+ styles: undefined,
+ timer: undefined,
+ };
+
+ return [
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ ...defaultProperties,
+ level: "info",
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ ...defaultProperties,
+ level: "warn",
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ ];
+}
+
+const longString = new Array(DevToolsServer.LONG_STRING_LENGTH + 2).join("a");
+function getExpectedRuntimeConsoleCalls(documentFilename) {
+ const defaultStackFrames = [
+ // This is the usage of "new " + expr from `evalInBrowsingContext`
+ {
+ filename: documentFilename,
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ },
+ ];
+
+ const defaultProperties = {
+ filename: documentFilename,
+ columnNumber: NUMBER_REGEX,
+ lineNumber: NUMBER_REGEX,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ chromeContext: undefined,
+ counter: undefined,
+ prefix: undefined,
+ private: undefined,
+ stacktrace: undefined,
+ styles: undefined,
+ timer: undefined,
+ };
+
+ return [
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["Float from not a number: NaN"],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["Float from string: 1.200000"],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["Float from number: 1.300000"],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["BigInt 123 and 456"],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: ["message with ", "style"],
+ styles: ["color: blue;", "background: red; font-size: 2em;"],
+ },
+ {
+ ...defaultProperties,
+ level: "info",
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ ...defaultProperties,
+ level: "warn",
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ {
+ ...defaultProperties,
+ level: "debug",
+ arguments: [{ type: "null" }],
+ },
+ {
+ ...defaultProperties,
+ level: "trace",
+ stacktrace: [
+ {
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ },
+ ...defaultStackFrames,
+ ],
+ },
+ {
+ ...defaultProperties,
+ level: "dir",
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "HTMLDocument",
+ },
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "Location",
+ },
+ ],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ arguments: [
+ "foo",
+ {
+ type: "longString",
+ initial: longString.substring(
+ 0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ ),
+ length: longString.length,
+ actor: /[a-z]/,
+ },
+ ],
+ },
+ {
+ ...defaultProperties,
+ level: "count",
+ arguments: ["myCounter"],
+ counter: {
+ count: 1,
+ label: "myCounter",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "count",
+ arguments: ["myCounter"],
+ counter: {
+ count: 2,
+ label: "myCounter",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "count",
+ arguments: ["default"],
+ counter: {
+ count: 1,
+ label: "default",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "countReset",
+ arguments: ["myCounter"],
+ counter: {
+ count: 0,
+ label: "myCounter",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "countReset",
+ arguments: ["unknownCounter"],
+ counter: {
+ error: "counterDoesntExist",
+ label: "unknownCounter",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "time",
+ arguments: ["myTimer"],
+ timer: {
+ name: "myTimer",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "time",
+ arguments: ["myTimer"],
+ timer: {
+ name: "myTimer",
+ error: "timerAlreadyExists",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "timeLog",
+ arguments: ["myTimer"],
+ timer: {
+ name: "myTimer",
+ duration: NUMBER_REGEX,
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "timeEnd",
+ arguments: ["myTimer"],
+ timer: {
+ name: "myTimer",
+ duration: NUMBER_REGEX,
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "time",
+ arguments: ["default"],
+ timer: {
+ name: "default",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "timeLog",
+ arguments: ["default"],
+ timer: {
+ name: "default",
+ duration: NUMBER_REGEX,
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "timeEnd",
+ arguments: ["default"],
+ timer: {
+ name: "default",
+ duration: NUMBER_REGEX,
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "timeLog",
+ arguments: ["unknownTimer"],
+ timer: {
+ name: "unknownTimer",
+ error: "timerDoesntExist",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "timeEnd",
+ arguments: ["unknownTimer"],
+ timer: {
+ name: "unknownTimer",
+ error: "timerDoesntExist",
+ },
+ },
+ {
+ ...defaultProperties,
+ level: "error",
+ arguments: ["foobarBaz-asmjs-error", { type: "undefined" }],
+
+ stacktrace: [
+ {
+ filename: documentFilename,
+ functionName: "fromAsmJS",
+ },
+ {
+ filename: documentFilename,
+ functionName: "inAsmJS2",
+ },
+ {
+ filename: documentFilename,
+ functionName: "inAsmJS1",
+ },
+ {
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ },
+ ...defaultStackFrames,
+ ],
+ },
+ {
+ ...defaultProperties,
+ level: "log",
+ filename:
+ "chrome://mochitests/content/browser/devtools/shared/commands/resource/tests/browser_resources_console_messages.js",
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "Restricted",
+ },
+ ],
+ chromeContext: true,
+ },
+ ];
+}
+
+async function logRuntimeMessages(browser, executeInIframe) {
+ let browsingContext = browser.browsingContext;
+ if (executeInIframe) {
+ browsingContext = await SpecialPowers.spawn(
+ browser,
+ [],
+ function frameScript() {
+ return content.document.querySelector("iframe").browsingContext;
+ }
+ );
+ }
+ // First inject LONG_STRING_LENGTH in global scope it order to easily use it after
+ await evalInBrowsingContext(
+ browsingContext,
+ `function () {window.LONG_STRING_LENGTH = ${DevToolsServer.LONG_STRING_LENGTH};}`
+ );
+ await evalInBrowsingContext(browsingContext, function pageScript() {
+ const _longString = new Array(window.LONG_STRING_LENGTH + 2).join("a");
+
+ console.log("foobarBaz-log", undefined);
+
+ console.log("Float from not a number: %f", "foo");
+ console.log("Float from string: %f", "1.2");
+ console.log("Float from number: %f", 1.3);
+ console.log("BigInt %d and %i", 123n, 456n);
+ console.log(
+ "%cmessage with %cstyle",
+ "color: blue;",
+ "background: red; font-size: 2em;"
+ );
+
+ console.info("foobarBaz-info", null);
+ console.warn("foobarBaz-warn", document.documentElement);
+ console.debug(null);
+ console.trace();
+ console.dir(document, location);
+ console.log("foo", _longString);
+
+ console.count("myCounter");
+ console.count("myCounter");
+ console.count();
+ console.countReset("myCounter");
+ // will cause warnings because unknownCounter doesn't exist
+ console.countReset("unknownCounter");
+
+ console.time("myTimer");
+ // will cause warning because myTimer already exist
+ console.time("myTimer");
+ console.timeLog("myTimer");
+ console.timeEnd("myTimer");
+ console.time();
+ console.timeLog();
+ console.timeEnd();
+ // // will cause warnings because unknownTimer doesn't exist
+ console.timeLog("unknownTimer");
+ console.timeEnd("unknownTimer");
+
+ function fromAsmJS() {
+ console.error("foobarBaz-asmjs-error", undefined);
+ }
+
+ (function (global, foreign) {
+ "use asm";
+ function inAsmJS2() {
+ foreign.fromAsmJS();
+ }
+ function inAsmJS1() {
+ inAsmJS2();
+ }
+ return inAsmJS1;
+ })(null, { fromAsmJS })();
+ });
+ await SpecialPowers.spawn(browsingContext, [], function frameScript() {
+ const sandbox = new Cu.Sandbox(null, { invisibleToDebugger: true });
+ const sandboxObj = sandbox.eval("new Object");
+ content.console.log(sandboxObj);
+ });
+}
+
+// Copied from devtools/shared/webconsole/test/chrome/common.js
+function checkConsoleAPICall(call, expected) {
+ is(
+ call.arguments?.length || 0,
+ expected.arguments?.length || 0,
+ "number of arguments"
+ );
+
+ checkObject(call, expected);
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_console_messages_navigation.js b/devtools/shared/commands/resource/tests/browser_resources_console_messages_navigation.js
new file mode 100644
index 0000000000..3d6fc697da
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_console_messages_navigation.js
@@ -0,0 +1,190 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the resource command API around CONSOLE_MESSAGE when navigating
+// tab and inner iframes to distinct origin/processes.
+
+const TEST_URL = URL_ROOT_COM_SSL + "doc_console.html";
+const TEST_IFRAME_URL = URL_ROOT_ORG_SSL + "doc_console_iframe.html";
+const TEST_DOMAIN = "https://example.org";
+add_task(async function () {
+ const START_URL = "data:text/html;charset=utf-8,foo";
+ const tab = await addTab(START_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ await testCrossProcessTabNavigation(tab.linkedBrowser, resourceCommand);
+ await testCrossProcessIframeNavigation(tab.linkedBrowser, resourceCommand);
+
+ targetCommand.destroy();
+ await client.close();
+ BrowserTestUtils.removeTab(tab);
+});
+
+async function testCrossProcessTabNavigation(browser, resourceCommand) {
+ info(
+ "Navigate the top level document from data: URI to a https document including remote iframes"
+ );
+
+ let doneResolve;
+ const messages = [];
+ const onConsoleLogsComplete = new Promise(resolve => (doneResolve = resolve));
+
+ const onAvailable = resources => {
+ messages.push(...resources);
+ if (messages.length == 2) {
+ doneResolve();
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ { onAvailable }
+ );
+
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL);
+ await onLoaded;
+
+ info("Wait for log message");
+ await onConsoleLogsComplete;
+
+ // messages are coming from different targets so the order isn't guaranteed
+ const topLevelMessageResource = messages.find(resource =>
+ resource.message.filename.startsWith(URL_ROOT_COM_SSL)
+ );
+ const iframeMessage = messages.find(resource =>
+ resource.message.filename.startsWith("data:")
+ );
+
+ assertConsoleMessage(resourceCommand, topLevelMessageResource, {
+ targetFront: resourceCommand.targetCommand.targetFront,
+ messageText: "top-level document log",
+ });
+ assertConsoleMessage(resourceCommand, iframeMessage, {
+ targetFront: isEveryFrameTargetEnabled
+ ? resourceCommand.targetCommand
+ .getAllTargets([resourceCommand.targetCommand.TYPES.FRAME])
+ .find(t => t.url.startsWith("data:"))
+ : resourceCommand.targetCommand.targetFront,
+ messageText: "data url data log",
+ });
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+}
+
+async function testCrossProcessIframeNavigation(browser, resourceCommand) {
+ info("Navigate an inner iframe from data: URI to a https remote URL");
+
+ let doneResolve;
+ const messages = [];
+ const onConsoleLogsComplete = new Promise(resolve => (doneResolve = resolve));
+
+ const onAvailable = resources => {
+ messages.push(
+ ...resources.filter(
+ r => !r.message.arguments[0].startsWith("[WORKER] started")
+ )
+ );
+ if (messages.length == 3) {
+ doneResolve();
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+
+ // messages are coming from different targets so the order isn't guaranteed
+ const topLevelMessageResource = messages.find(resource =>
+ resource.message.arguments[0].startsWith("top-level")
+ );
+ const dataUrlMessageResource = messages.find(resource =>
+ resource.message.arguments[0].startsWith("data url")
+ );
+
+ // Assert cached messages from the previous top document
+ assertConsoleMessage(resourceCommand, topLevelMessageResource, {
+ messageText: "top-level document log",
+ });
+ assertConsoleMessage(resourceCommand, dataUrlMessageResource, {
+ messageText: "data url data log",
+ });
+
+ // Navigate the iframe to another origin/process
+ await SpecialPowers.spawn(browser, [TEST_IFRAME_URL], function (iframeUrl) {
+ const iframe = content.document.querySelector("iframe");
+ iframe.src = iframeUrl;
+ });
+
+ info("Wait for log message");
+ await onConsoleLogsComplete;
+
+ // iframeTarget will be different if Fission is on or off
+ const iframeTarget = await getIframeTargetFront(
+ resourceCommand.targetCommand
+ );
+
+ const iframeMessageResource = messages.find(resource =>
+ resource.message.arguments[0].endsWith("iframe log")
+ );
+ assertConsoleMessage(resourceCommand, iframeMessageResource, {
+ messageText: `${TEST_DOMAIN} iframe log`,
+ targetFront: iframeTarget,
+ });
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+}
+
+function assertConsoleMessage(resourceCommand, messageResource, expected) {
+ is(
+ messageResource.resourceType,
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ "Resource is a console message"
+ );
+ ok(messageResource.message, "message is wrapped into a message attribute");
+ if (expected.targetFront) {
+ is(
+ messageResource.targetFront,
+ expected.targetFront,
+ "Message has the correct target front"
+ );
+ }
+ is(
+ messageResource.message.arguments[0],
+ expected.messageText,
+ "The correct type of message"
+ );
+}
+
+async function getIframeTargetFront(targetCommand) {
+ // If Fission/EFT is enabled, the iframe will have a dedicated target,
+ // otherwise it will be debuggable via the top level target.
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ return targetCommand.targetFront;
+ }
+ const frameTargets = targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ const browsingContextID = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.document.querySelector("iframe").browsingContext.id;
+ }
+ );
+ const iframeTarget = frameTargets.find(target => {
+ return target.browsingContextID == browsingContextID;
+ });
+ ok(iframeTarget, "Found the iframe target front");
+ return iframeTarget;
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_console_messages_workers.js b/devtools/shared/commands/resource/tests/browser_resources_console_messages_workers.js
new file mode 100644
index 0000000000..4b10f1d2e4
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_console_messages_workers.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around CONSOLE_MESSAGE in workers
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document_workers.html";
+const WORKER_FILE = "test_worker.js";
+const IFRAME_FILE = `${URL_ROOT_ORG_SSL}fission_iframe_workers.html`;
+
+add_task(async function () {
+ // Set the following pref to false as it's the one that enables direct connection
+ // to the worker targets
+ await pushPref("dom.worker.console.dispatch_events_to_main_thread", false);
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab,
+ { listenForWorkers: true }
+ );
+
+ info("Wait for the workers (from the main page and the iframe) to be ready");
+ const targets = [];
+ await new Promise(resolve => {
+ const onAvailable = async ({ targetFront }) => {
+ targets.push(targetFront);
+ if (targets.length === 2) {
+ resolve();
+ }
+ };
+ targetCommand.watchTargets({
+ types: [targetCommand.TYPES.WORKER],
+ onAvailable,
+ });
+ });
+
+ // The worker logs a message right when it starts, containing its location, so we can
+ // assert that we get the logs from the worker spawned in the content page and from the
+ // worker spawned in the iframe.
+ info("Check that we receive the cached messages");
+
+ const resources = [];
+ const onAvailable = innerResources => {
+ for (const resource of innerResources) {
+ // Ignore resources from non worker targets
+ if (!resource.targetFront.isWorkerTarget) {
+ continue;
+ }
+
+ resources.push(resource);
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+
+ is(resources.length, 2, "Got the expected number of existing messages");
+ const startLogFromWorkerInMainPage = resources.find(
+ ({ message }) =>
+ message.arguments[1] === `${URL_ROOT_SSL}${WORKER_FILE}#simple-worker`
+ );
+ const startLogFromWorkerInIframe = resources.find(
+ ({ message }) =>
+ message.arguments[1] ===
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#simple-worker-in-iframe`
+ );
+
+ checkStartWorkerLogMessage(startLogFromWorkerInMainPage, {
+ expectedUrl: `${URL_ROOT_SSL}${WORKER_FILE}#simple-worker`,
+ isAlreadyExistingResource: true,
+ });
+ checkStartWorkerLogMessage(startLogFromWorkerInIframe, {
+ expectedUrl: `${URL_ROOT_ORG_SSL}${WORKER_FILE}#simple-worker-in-iframe`,
+ isAlreadyExistingResource: true,
+ });
+ let messageCount = resources.length;
+
+ info(
+ "Now log messages *after* the call to ResourceCommand.watchResources and after having received all existing messages"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.wrappedJSObject.logMessageInWorker("live message from main page");
+
+ const iframe = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframe, [], () => {
+ content.wrappedJSObject.logMessageInWorker("live message from iframe");
+ });
+ });
+
+ // Wait until the 2 new logs are available
+ await waitUntil(() => resources.length === messageCount + 2);
+ const liveMessageFromWorkerInMainPage = resources.find(
+ ({ message }) => message.arguments[1] === "live message from main page"
+ );
+ const liveMessageFromWorkerInIframe = resources.find(
+ ({ message }) => message.arguments[1] === "live message from iframe"
+ );
+
+ checkLogInWorkerMessage(
+ liveMessageFromWorkerInMainPage,
+ "live message from main page"
+ );
+
+ checkLogInWorkerMessage(
+ liveMessageFromWorkerInIframe,
+ "live message from iframe"
+ );
+
+ // update the current number of resources received
+ messageCount = resources.length;
+
+ info("Now spawn new workers and log messages in main page and iframe");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [WORKER_FILE],
+ async workerUrl => {
+ const spawnedWorker = new content.Worker(`${workerUrl}#spawned-worker`);
+ spawnedWorker.postMessage({
+ type: "log-in-worker",
+ message: "live message in spawned worker from main page",
+ });
+
+ const iframe = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframe, [workerUrl], async innerWorkerUrl => {
+ const spawnedWorkerInIframe = new content.Worker(
+ `${innerWorkerUrl}#spawned-worker-in-iframe`
+ );
+ spawnedWorkerInIframe.postMessage({
+ type: "log-in-worker",
+ message: "live message in spawned worker from iframe",
+ });
+ });
+ }
+ );
+
+ info(
+ "Wait until the 4 new logs are available (the ones logged at worker creation + the ones from postMessage"
+ );
+ await waitUntil(
+ () => resources.length === messageCount + 4,
+ `Couldn't get the expected number of resources (expected ${
+ messageCount + 4
+ }, got ${resources.length})`
+ );
+ const startLogFromSpawnedWorkerInMainPage = resources.find(
+ ({ message }) =>
+ message.arguments[1] === `${URL_ROOT_SSL}${WORKER_FILE}#spawned-worker`
+ );
+ const startLogFromSpawnedWorkerInIframe = resources.find(
+ ({ message }) =>
+ message.arguments[1] ===
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#spawned-worker-in-iframe`
+ );
+ const liveMessageFromSpawnedWorkerInMainPage = resources.find(
+ ({ message }) =>
+ message.arguments[1] === "live message in spawned worker from main page"
+ );
+ const liveMessageFromSpawnedWorkerInIframe = resources.find(
+ ({ message }) =>
+ message.arguments[1] === "live message in spawned worker from iframe"
+ );
+
+ checkStartWorkerLogMessage(startLogFromSpawnedWorkerInMainPage, {
+ expectedUrl: `${URL_ROOT_SSL}${WORKER_FILE}#spawned-worker`,
+ });
+ checkStartWorkerLogMessage(startLogFromSpawnedWorkerInIframe, {
+ expectedUrl: `${URL_ROOT_ORG_SSL}${WORKER_FILE}#spawned-worker-in-iframe`,
+ });
+ checkLogInWorkerMessage(
+ liveMessageFromSpawnedWorkerInMainPage,
+ "live message in spawned worker from main page"
+ );
+ checkLogInWorkerMessage(
+ liveMessageFromSpawnedWorkerInIframe,
+ "live message in spawned worker from iframe"
+ );
+ // update the current number of resources received
+ messageCount = resources.length;
+
+ info(
+ "Add a remote iframe on the same origin we already have an iframe and check we get the messages"
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [IFRAME_FILE],
+ async iframeUrl => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = `${iframeUrl}?hashSuffix=in-second-iframe`;
+ content.document.body.append(iframe);
+ }
+ );
+
+ info("Wait until the new log is available");
+ await waitUntil(
+ () => resources.length === messageCount + 1,
+ `Couldn't get the expected number of resources (expected ${
+ messageCount + 1
+ }, got ${resources.length})`
+ );
+ const startLogFromWorkerInSecondIframe = resources[resources.length - 1];
+ checkStartWorkerLogMessage(startLogFromWorkerInSecondIframe, {
+ expectedUrl: `${URL_ROOT_ORG_SSL}${WORKER_FILE}#simple-worker-in-second-iframe`,
+ });
+
+ targetCommand.destroy();
+ await client.close();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+function checkStartWorkerLogMessage(
+ resource,
+ { expectedUrl, isAlreadyExistingResource = false }
+) {
+ const { message } = resource;
+ const [firstArg, secondArg, thirdArg] = message.arguments;
+ is(firstArg, "[WORKER] started", "Got the expected first argument");
+ is(secondArg, expectedUrl, "expected url was logged");
+ is(
+ thirdArg?._grip?.class,
+ "DedicatedWorkerGlobalScope",
+ "the global scope was logged as expected"
+ );
+ is(
+ resource.isAlreadyExistingResource,
+ isAlreadyExistingResource,
+ "Resource has expected value for isAlreadyExistingResource"
+ );
+}
+
+function checkLogInWorkerMessage(resource, expectedMessage) {
+ const { message } = resource;
+ const [firstArg, secondArg, thirdArg] = message.arguments;
+ is(firstArg, "[WORKER]", "Got the expected first argument");
+ is(secondArg, expectedMessage, "expected message was logged");
+ is(
+ thirdArg?._grip?.class,
+ "MessageEvent",
+ "the message event object was logged as expected"
+ );
+ is(
+ resource.isAlreadyExistingResource,
+ false,
+ "Resource has expected value for isAlreadyExistingResource"
+ );
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_css_changes.js b/devtools/shared/commands/resource/tests/browser_resources_css_changes.js
new file mode 100644
index 0000000000..22b11a8186
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_css_changes.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around CSS_CHANGE.
+
+add_task(async function () {
+ // Open a test tab
+ const tab = await addTab(
+ "data:text/html,<body style='color: lime;'>CSS Changes</body>"
+ );
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ // CSS_CHANGE watcher doesn't record modification made before watching,
+ // so we have to start watching before doing any DOM mutation.
+ await resourceCommand.watchResources([resourceCommand.TYPES.CSS_CHANGE], {
+ onAvailable: () => {},
+ });
+
+ const { walker } = await targetCommand.targetFront.getFront("inspector");
+ const nodeList = await walker.querySelectorAll(walker.rootNode, "body");
+ const body = (await nodeList.items())[0];
+ const style = (
+ await body.inspectorFront.pageStyle.getApplied(body, {
+ skipPseudo: false,
+ })
+ )[0];
+
+ info(
+ "Check whether ResourceCommand catches CSS change that fired before starting to watch"
+ );
+ await setProperty(style.rule, 0, "color", "black");
+
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.CSS_CHANGE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+ assertResource(
+ availableResources[0],
+ { index: 0, property: "color", value: "black" },
+ { index: 0, property: "color", value: "lime" }
+ );
+
+ info(
+ "Check whether ResourceCommand catches CSS changes after the property was renamed and updated"
+ );
+
+ // RuleRewriter:apply will not support a simultaneous rename + setProperty.
+ // Doing so would send inconsistent arguments to StyleRuleActor:setRuleText,
+ // the CSS text for the rule will not match the list of modifications, which
+ // would desynchronize the Changes view. Thankfully this scenario should not
+ // happen when using the UI to update the rules.
+ await renameProperty(style.rule, 0, "color", "background-color");
+ await waitUntil(() => availableResources.length === 2);
+ assertResource(
+ availableResources[1],
+ { index: 0, property: "background-color", value: "black" },
+ { index: 0, property: "color", value: "black" }
+ );
+
+ await setProperty(style.rule, 0, "background-color", "pink");
+ await waitUntil(() => availableResources.length === 3);
+ assertResource(
+ availableResources[2],
+ { index: 0, property: "background-color", value: "pink" },
+ { index: 0, property: "background-color", value: "black" }
+ );
+
+ info("Check whether ResourceCommand catches CSS change of disabling");
+ await setPropertyEnabled(style.rule, 0, "background-color", false);
+ await waitUntil(() => availableResources.length === 4);
+ assertResource(availableResources[3], null, {
+ index: 0,
+ property: "background-color",
+ value: "pink",
+ });
+
+ info("Check whether ResourceCommand catches CSS change of new property");
+ await createProperty(style.rule, 1, "font-size", "100px");
+ await waitUntil(() => availableResources.length === 5);
+ assertResource(
+ availableResources[4],
+ { index: 1, property: "font-size", value: "100px" },
+ null
+ );
+
+ info("Check whether ResourceCommand sends all resources added in this test");
+ const existingResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.CSS_CHANGE], {
+ onAvailable: resources => existingResources.push(...resources),
+ });
+ await waitUntil(() => existingResources.length === 5);
+ is(availableResources[0], existingResources[0], "1st resource is correct");
+ is(availableResources[1], existingResources[1], "2nd resource is correct");
+ is(availableResources[2], existingResources[2], "3rd resource is correct");
+ is(availableResources[3], existingResources[3], "4th resource is correct");
+ is(availableResources[4], existingResources[4], "4th resource is correct");
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+function assertResource(resource, expectedAddedChange, expectedRemovedChange) {
+ if (expectedAddedChange) {
+ is(resource.add.length, 1, "The number of added changes is correct");
+ assertChange(resource.add[0], expectedAddedChange);
+ } else {
+ is(resource.add, null, "There is no added changes");
+ }
+
+ if (expectedRemovedChange) {
+ is(resource.remove.length, 1, "The number of removed changes is correct");
+ assertChange(resource.remove[0], expectedRemovedChange);
+ } else {
+ is(resource.remove, null, "There is no removed changes");
+ }
+}
+
+function assertChange(change, expected) {
+ is(change.index, expected.index, "The index of change is correct");
+ is(change.property, expected.property, "The property of change is correct");
+ is(change.value, expected.value, "The value of change is correct");
+}
+
+async function setProperty(rule, index, property, value) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.setProperty(index, property, value, "");
+ await modifications.apply();
+}
+
+async function renameProperty(rule, index, oldName, newName, value) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.renameProperty(index, oldName, newName);
+ await modifications.apply();
+}
+
+async function createProperty(rule, index, property, value) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.createProperty(index, property, value, "", true);
+ await modifications.apply();
+}
+
+async function setPropertyEnabled(rule, index, property, isEnabled) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.setPropertyEnabled(index, property, isEnabled);
+ await modifications.apply();
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_css_messages.js b/devtools/shared/commands/resource/tests/browser_resources_css_messages.js
new file mode 100644
index 0000000000..1b4b56cd4f
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_css_messages.js
@@ -0,0 +1,212 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around CSS_MESSAGE
+// Reproduces the CSS message assertions from devtools/shared/webconsole/test/chrome/test_page_errors.html
+
+const { MESSAGE_CATEGORY } = require("resource://devtools/shared/constants.js");
+
+// Create a simple server so we have a nice sourceName in the resources packets.
+const httpServer = createTestHTTPServer();
+httpServer.registerPathHandler(`/test_css_messages.html`, (req, res) => {
+ res.setStatusLine(req.httpVersion, 200, "OK");
+ res.write(`<meta charset=utf8>
+ <style>
+ html {
+ body {
+ color: bloup;
+ }
+ }
+ </style>Test CSS Messages`);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/test_css_messages.html`;
+
+add_task(async function () {
+ await testWatchingCssMessages();
+ await testWatchingCachedCssMessages();
+});
+
+async function testWatchingCssMessages() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // Open a test tab
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const receivedMessages = [];
+ const { onAvailable, onAllMessagesReceived } = setupOnAvailableFunction(
+ targetCommand,
+ receivedMessages,
+ false
+ );
+ await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
+ onAvailable,
+ });
+
+ info(
+ "Now log CSS warning *after* the call to ResourceCommand.watchResources and after " +
+ "having received the existing message"
+ );
+ // We need to wait for the first CSS Warning as it is not a cached message; when we
+ // start watching, the `cssErrorReportingEnabled` is checked on the target docShell, and
+ // if it is false, we re-parse the stylesheets to get the messages.
+ await BrowserTestUtils.waitForCondition(() => receivedMessages.length === 1);
+
+ info("Trigger a CSS Warning");
+ triggerCSSWarning(tab);
+
+ info("Waiting for all expected CSS messages to be received");
+ await onAllMessagesReceived;
+ ok(true, "All the expected CSS messages were received");
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testWatchingCachedCssMessages() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // Open a test tab
+ const tab = await addTab(TEST_URI);
+
+ // By default, the CSS Parser does not emit warnings at all, for performance matter.
+ // Since we actually want the Parser to emit those messages _before_ we start listening
+ // for CSS messages, we need to set the cssErrorReportingEnabled flag on the docShell.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.docShell.cssErrorReportingEnabled = true;
+ });
+
+ // Setting the docShell flag only indicates to the Parser that from now on, it should
+ // emit warnings. But it does not automatically emit warnings for the existing CSS
+ // errors in the stylesheets. So here we reload the tab, which will make the Parser
+ // parse the stylesheets again, this time emitting warnings.
+ await reloadBrowser();
+ // and trigger more CSS warnings
+ await triggerCSSWarning(tab);
+
+ // At this point, all messages should be in the ConsoleService cache, and we can begin
+ // to watch and check that we do retrieve those messages.
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const receivedMessages = [];
+ const { onAvailable } = setupOnAvailableFunction(
+ targetCommand,
+ receivedMessages,
+ true
+ );
+ await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
+ onAvailable,
+ });
+ is(receivedMessages.length, 3, "Cached messages were retrieved as expected");
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+}
+
+function setupOnAvailableFunction(
+ targetCommand,
+ receivedMessages,
+ isAlreadyExistingResource
+) {
+ // timeStamp are the result of a number in microsecond divided by 1000.
+ // so we can't expect a precise number of decimals, or even if there would
+ // be decimals at all.
+ const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;
+
+ // The expected messages are the CSS warnings:
+ // - one for the rule in the style element
+ // - two for the JS modified style we're doing in the test.
+ const expectedMessages = [
+ {
+ pageError: {
+ errorMessage: /Expected color but found ‘bloup’/,
+ sourceName: /test_css_messages/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ },
+ cssSelectors: ":is(html) body",
+ isAlreadyExistingResource,
+ },
+ {
+ pageError: {
+ errorMessage: /Error in parsing value for ‘width’/,
+ sourceName: /test_css_messages/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ },
+ isAlreadyExistingResource,
+ },
+ {
+ pageError: {
+ errorMessage: /Error in parsing value for ‘height’/,
+ sourceName: /test_css_messages/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ },
+ isAlreadyExistingResource,
+ },
+ ];
+
+ let done;
+ const onAllMessagesReceived = new Promise(resolve => (done = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ const { pageError } = resource;
+
+ is(
+ resource.targetFront,
+ targetCommand.targetFront,
+ "The targetFront property is the expected one"
+ );
+
+ if (!pageError.sourceName.includes("test_css_messages")) {
+ info(`Ignore error from unknown source: "${pageError.sourceName}"`);
+ continue;
+ }
+
+ const index = receivedMessages.length;
+ receivedMessages.push(resource);
+
+ info(
+ `checking received css message #${index}: ${pageError.errorMessage}`
+ );
+ ok(pageError, "The resource has a pageError attribute");
+ checkObject(resource, expectedMessages[index]);
+
+ if (receivedMessages.length == expectedMessages.length) {
+ done();
+ }
+ }
+ };
+ return { onAvailable, onAllMessagesReceived };
+}
+
+/**
+ * Sets invalid values for width and height on the document's body style attribute.
+ */
+function triggerCSSWarning(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, null, function frameScript() {
+ content.document.body.style.width = "red";
+ content.document.body.style.height = "blue";
+ });
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_css_registered_properties.js b/devtools/shared/commands/resource/tests/browser_resources_css_registered_properties.js
new file mode 100644
index 0000000000..1429b55167
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_css_registered_properties.js
@@ -0,0 +1,384 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around CSS_REGISTERED_PROPERTIES.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const IFRAME_URL = `https://example.org/document-builder.sjs?html=${encodeURIComponent(`
+ <style>
+ @property --css-a {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: gold;
+ }
+ </style>
+ <script>
+ CSS.registerProperty({
+ name: "--js-a",
+ syntax: "<length>",
+ inherits: true,
+ initialValue: "20px"
+ });
+ </script>
+ <h1>iframe</h1>
+`)}`;
+
+const TEST_URL = `https://example.org/document-builder.sjs?html=
+ <head>
+ <style>
+ @property --css-a {
+ syntax: "*";
+ inherits: false;
+ }
+
+ @property --css-b {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: tomato;
+ }
+ </style>
+ <script>
+ CSS.registerProperty({
+ name: "--js-a",
+ syntax: "*",
+ inherits: false,
+ });
+ CSS.registerProperty({
+ name: "--js-b",
+ syntax: "<length>",
+ inherits: true,
+ initialValue: "10px"
+ });
+ </script>
+ </head>
+ <h1>CSS_REGISTERED_PROPERTIES</h1>
+ <iframe src="${encodeURIComponent(IFRAME_URL)}"></iframe>`;
+
+add_task(async function () {
+ await pushPref("layout.css.properties-and-values.enabled", true);
+ const tab = await addTab(TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ // Wait for targets
+ await targetCommand.startListening();
+ const targets = [];
+ const onAvailable = ({ targetFront }) => targets.push(targetFront);
+ await targetCommand.watchTargets({
+ types: [targetCommand.TYPES.FRAME],
+ onAvailable,
+ });
+ await waitFor(() => targets.length === 2);
+ const [topLevelTarget, iframeTarget] = targets.sort((a, b) =>
+ a.isTopLevel ? -1 : 1
+ );
+
+ // Watching for new stylesheets shouldn't be
+ const stylesheets = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: resources => stylesheets.push(...resources),
+ ignoreExistingResources: true,
+ });
+
+ info("Check that we get existing registered properties");
+ const availableResources = [];
+ const updatedResources = [];
+ const destroyedResources = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES],
+ {
+ onAvailable: resources => availableResources.push(...resources),
+ onUpdated: resources => updatedResources.push(...resources),
+ onDestroyed: resources => destroyedResources.push(...resources),
+ }
+ );
+
+ is(
+ availableResources.length,
+ 6,
+ "The 6 existing registered properties where retrieved"
+ );
+
+ // Sort resources so we get them alphabetically ordered by their name, with the ones for
+ // the top level target displayed first.
+ availableResources.sort((a, b) => {
+ if (a.targetFront !== b.targetFront) {
+ return a.targetFront.isTopLevel ? -1 : 1;
+ }
+ return a.name < b.name ? -1 : 1;
+ });
+
+ assertResource(availableResources[0], {
+ name: "--css-a",
+ syntax: "*",
+ inherits: false,
+ initialValue: null,
+ fromJS: false,
+ targetFront: topLevelTarget,
+ });
+ assertResource(availableResources[1], {
+ name: "--css-b",
+ syntax: "<color>",
+ inherits: true,
+ initialValue: "tomato",
+ fromJS: false,
+ targetFront: topLevelTarget,
+ });
+ assertResource(availableResources[2], {
+ name: "--js-a",
+ syntax: "*",
+ inherits: false,
+ initialValue: null,
+ fromJS: true,
+ targetFront: topLevelTarget,
+ });
+ assertResource(availableResources[3], {
+ name: "--js-b",
+ syntax: "<length>",
+ inherits: true,
+ initialValue: "10px",
+ fromJS: true,
+ targetFront: topLevelTarget,
+ });
+ assertResource(availableResources[4], {
+ name: "--css-a",
+ syntax: "<color>",
+ inherits: true,
+ initialValue: "gold",
+ fromJS: false,
+ targetFront: iframeTarget,
+ });
+ assertResource(availableResources[5], {
+ name: "--js-a",
+ syntax: "<length>",
+ inherits: true,
+ initialValue: "20px",
+ fromJS: true,
+ targetFront: iframeTarget,
+ });
+
+ info("Check that we didn't get notified about existing stylesheets");
+ // wait a bit so we'd have the time to be notified about stylesheet resources
+ await wait(500);
+ is(
+ stylesheets.length,
+ 0,
+ "Watching for registered properties does not notify about existing stylesheets resources"
+ );
+
+ info("Check that we get properties from new stylesheets");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const s = content.document.createElement("style");
+ s.textContent = `
+ @property --css-c {
+ syntax: "<custom-ident>";
+ inherits: true;
+ initial-value: custom;
+ }
+
+ @property --css-d {
+ syntax: "big | bigger";
+ inherits: true;
+ initial-value: big;
+ }
+ `;
+ content.document.head.append(s);
+ });
+
+ info("Wait for registered properties to be available");
+ await waitFor(() => availableResources.length === 8);
+ ok(true, "Got notified about 2 new registered properties");
+ assertResource(availableResources[6], {
+ name: "--css-c",
+ syntax: "<custom-ident>",
+ inherits: true,
+ initialValue: "custom",
+ fromJS: false,
+ targetFront: topLevelTarget,
+ });
+ assertResource(availableResources[7], {
+ name: "--css-d",
+ syntax: "big | bigger",
+ inherits: true,
+ initialValue: "big",
+ fromJS: false,
+ targetFront: topLevelTarget,
+ });
+
+ info("Wait to be notified about the new stylesheet");
+ await waitFor(() => stylesheets.length === 1);
+ ok(true, "we do get notified about stylesheets");
+
+ info(
+ "Check that we get notified about properties registered via CSS.registerProperty"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.CSS.registerProperty({
+ name: "--js-c",
+ syntax: "*",
+ inherits: false,
+ initialValue: 42,
+ });
+ content.CSS.registerProperty({
+ name: "--js-d",
+ syntax: "<color>#",
+ inherits: true,
+ initialValue: "blue,cyan",
+ });
+ });
+
+ await waitFor(() => availableResources.length === 10);
+ ok(true, "Got notified about 2 new registered properties");
+ assertResource(availableResources[8], {
+ name: "--js-c",
+ syntax: "*",
+ inherits: false,
+ initialValue: "42",
+ fromJS: true,
+ targetFront: topLevelTarget,
+ });
+ assertResource(availableResources[9], {
+ name: "--js-d",
+ syntax: "<color>#",
+ inherits: true,
+ initialValue: "blue,cyan",
+ fromJS: true,
+ targetFront: topLevelTarget,
+ });
+
+ info(
+ "Check that we get notified about properties registered via CSS.registerProperty in iframe"
+ );
+ const iframeBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+
+ await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
+ content.CSS.registerProperty({
+ name: "--js-iframe",
+ syntax: "<color>#",
+ inherits: true,
+ initialValue: "red,salmon",
+ });
+ });
+
+ await waitFor(() => availableResources.length === 11);
+ ok(true, "Got notified about 2 new registered properties");
+ assertResource(availableResources[10], {
+ name: "--js-iframe",
+ syntax: "<color>#",
+ inherits: true,
+ initialValue: "red,salmon",
+ fromJS: true,
+ targetFront: iframeTarget,
+ });
+
+ info(
+ "Check that we get notified about destroyed properties when removing stylesheet"
+ );
+ // sanity check
+ is(destroyedResources.length, 0, "No destroyed resources yet");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.querySelector("style").remove();
+ });
+ await waitFor(() => destroyedResources.length == 2);
+ ok(true, "We got notified about destroyed resources");
+ destroyedResources.sort((a, b) => a < b);
+ is(
+ destroyedResources[0].resourceType,
+ ResourceCommand.TYPES.CSS_REGISTERED_PROPERTIES,
+ "resource type is correct"
+ );
+ is(
+ destroyedResources[0].resourceId,
+ `${topLevelTarget.actorID}:css-registered-property:--css-a`,
+ "expected css property was destroyed"
+ );
+ is(
+ destroyedResources[1].resourceType,
+ ResourceCommand.TYPES.CSS_REGISTERED_PROPERTIES,
+ "resource type is correct"
+ );
+ is(
+ destroyedResources[1].resourceId,
+ `${topLevelTarget.actorID}:css-registered-property:--css-b`,
+ "expected css property was destroyed"
+ );
+
+ info(
+ "Check that we get notified about updated properties when modifying stylesheet"
+ );
+ is(updatedResources.length, 0, "No updated resources yet");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.querySelector("style").textContent = `
+ /* not updated */
+ @property --css-c {
+ syntax: "<custom-ident>";
+ inherits: true;
+ initial-value: custom;
+ }
+
+ @property --css-d {
+ syntax: "big | bigger";
+ inherits: true;
+ /* only change initial value (was big) */
+ initial-value: bigger;
+ }
+
+ /* add a new property */
+ @property --css-e {
+ syntax: "<color>";
+ inherits: false;
+ initial-value: green;
+ }
+ `;
+ });
+ await waitFor(() => updatedResources.length === 1);
+ ok(true, "One property was updated");
+ assertResource(updatedResources[0].resource, {
+ name: "--css-d",
+ syntax: "big | bigger",
+ inherits: true,
+ initialValue: "bigger",
+ fromJS: false,
+ targetFront: topLevelTarget,
+ });
+
+ await waitFor(() => availableResources.length === 12);
+ ok(true, "We got notified about the new property");
+ assertResource(availableResources.at(-1), {
+ name: "--css-e",
+ syntax: "<color>",
+ inherits: false,
+ initialValue: "green",
+ fromJS: false,
+ targetFront: topLevelTarget,
+ });
+
+ await client.close();
+});
+
+async function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.CSS_REGISTERED_PROPERTIES,
+ "Resource type is correct"
+ );
+ is(resource.name, expected.name, "name is correct");
+ is(resource.syntax, expected.syntax, "syntax is correct");
+ is(resource.inherits, expected.inherits, "inherits is correct");
+ is(resource.initialValue, expected.initialValue, "initialValue is correct");
+ is(resource.fromJS, expected.fromJS, "fromJS is correct");
+ is(
+ resource.targetFront,
+ expected.targetFront,
+ "resource is associated with expected target"
+ );
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_document_events.js b/devtools/shared/commands/resource/tests/browser_resources_document_events.js
new file mode 100644
index 0000000000..4692cba1ed
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_document_events.js
@@ -0,0 +1,720 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around DOCUMENT_EVENT
+
+add_task(async function () {
+ await testDocumentEventResources();
+ await testDocumentEventResourcesWithIgnoreExistingResources();
+ await testDomCompleteWithOverloadedConsole();
+ await testIframeNavigation();
+ await testBfCacheNavigation();
+ await testDomCompleteWithWindowStop();
+ await testCrossOriginNavigation();
+});
+
+async function testDocumentEventResources() {
+ info("Test ResourceCommand for DOCUMENT_EVENT");
+
+ // Open a test tab
+ const title = "DocumentEventsTitle";
+ const url = `data:text/html,<title>${title}</title>Document Events`;
+ const tab = await addTab(url);
+
+ const listener = new ResourceListener();
+ const { commands } = await initResourceCommand(tab);
+
+ info(
+ "Check whether the document events are fired correctly even when the document was already loaded"
+ );
+ const onLoadingAtInit = listener.once("dom-loading");
+ const onInteractiveAtInit = listener.once("dom-interactive");
+ const onCompleteAtInit = listener.once("dom-complete");
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: parameters => listener.dispatch(parameters),
+ }
+ );
+ await assertPromises(
+ commands,
+ // targetBeforeNavigation is only used when there is a will-navigate and a navigate, but there is none here
+ null,
+ // As we started watching on an already loaded document, and no navigation happened since we called watchResources,
+ // we don't have any will-navigate event
+ null,
+ onLoadingAtInit,
+ onInteractiveAtInit,
+ onCompleteAtInit
+ );
+ ok(
+ true,
+ "Document events are fired even when the document was already loaded"
+ );
+ let domLoadingResource = await onLoadingAtInit;
+
+ is(
+ domLoadingResource.url,
+ url,
+ `resource ${domLoadingResource.name} has expected url`
+ );
+ is(
+ domLoadingResource.title,
+ undefined,
+ `resource ${domLoadingResource.name} does not have a title property`
+ );
+
+ let domInteractiveResource = await onInteractiveAtInit;
+ is(
+ domInteractiveResource.url,
+ url,
+ `resource ${domInteractiveResource.name} has expected url`
+ );
+ is(
+ domInteractiveResource.title,
+ title,
+ `resource ${domInteractiveResource.name} has expected title`
+ );
+ let domCompleteResource = await onCompleteAtInit;
+ is(
+ domCompleteResource.url,
+ undefined,
+ `resource ${domCompleteResource.name} does not have a url property`
+ );
+ is(
+ domCompleteResource.title,
+ undefined,
+ `resource ${domCompleteResource.name} does not have a title property`
+ );
+
+ info("Check whether the document events are fired correctly when reloading");
+ const onWillNavigate = listener.once("will-navigate");
+ const onLoadingAtReloaded = listener.once("dom-loading");
+ const onInteractiveAtReloaded = listener.once("dom-interactive");
+ const onCompleteAtReloaded = listener.once("dom-complete");
+ const targetBeforeNavigation = commands.targetCommand.targetFront;
+ gBrowser.reloadTab(tab);
+ await assertPromises(
+ commands,
+ targetBeforeNavigation,
+ onWillNavigate,
+ onLoadingAtReloaded,
+ onInteractiveAtReloaded,
+ onCompleteAtReloaded
+ );
+ ok(true, "Document events are fired after reloading");
+
+ domLoadingResource = await onLoadingAtReloaded;
+ is(
+ domLoadingResource.url,
+ url,
+ `resource ${domLoadingResource.name} has expected url after reloading`
+ );
+ is(
+ domLoadingResource.title,
+ undefined,
+ `resource ${domLoadingResource.name} does not have a title property after reloading`
+ );
+
+ domInteractiveResource = await onInteractiveAtInit;
+ is(
+ domInteractiveResource.url,
+ url,
+ `resource ${domInteractiveResource.name} has url property after reloading`
+ );
+ is(
+ domInteractiveResource.title,
+ title,
+ `resource ${domInteractiveResource.name} has expected title after reloading`
+ );
+ domCompleteResource = await onCompleteAtInit;
+ is(
+ domCompleteResource.url,
+ undefined,
+ `resource ${domCompleteResource.name} does not have a url property after reloading`
+ );
+ is(
+ domCompleteResource.title,
+ undefined,
+ `resource ${domCompleteResource.name} does not have a title property after reloading`
+ );
+
+ await commands.destroy();
+}
+
+async function testDocumentEventResourcesWithIgnoreExistingResources() {
+ info("Test ignoreExistingResources option for DOCUMENT_EVENT");
+
+ const tab = await addTab("data:text/html,Document Events");
+
+ const { commands } = await initResourceCommand(tab);
+
+ info("Check whether the existing document events will not be fired");
+ const documentEvents = [];
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: resources => documentEvents.push(...resources),
+ ignoreExistingResources: true,
+ }
+ );
+ is(documentEvents.length, 0, "Existing document events are not fired");
+
+ info("Check whether the future document events are fired");
+ const targetBeforeNavigation = commands.targetCommand.targetFront;
+ gBrowser.reloadTab(tab);
+ info(
+ "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events"
+ );
+ await waitFor(() => documentEvents.length === 4);
+ assertEvents({ commands, targetBeforeNavigation, documentEvents });
+
+ await commands.destroy();
+}
+
+async function testIframeNavigation() {
+ info("Test iframe navigations for DOCUMENT_EVENT");
+
+ const tab = await addTab(
+ 'https://example.com/document-builder.sjs?html=<iframe src="https://example.net/document-builder.sjs?html=net"></iframe>'
+ );
+ const secondPageUrl = "https://example.org/document-builder.sjs?html=org";
+
+ const { commands } = await initResourceCommand(tab);
+
+ let documentEvents = [];
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: resources => documentEvents.push(...resources),
+ }
+ );
+ let iframeTarget;
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ is(
+ documentEvents.length,
+ 6,
+ "With fission/EFT, we get two targets and two sets of events: dom-loading, dom-interactive, dom-complete"
+ );
+ [, iframeTarget] = await commands.targetCommand.getAllTargets([
+ commands.targetCommand.TYPES.FRAME,
+ ]);
+ // Filter out each target events as their order to be random between the two targets
+ const topTargetEvents = documentEvents.filter(
+ r => r.targetFront == commands.targetCommand.targetFront
+ );
+ const iframeTargetEvents = documentEvents.filter(
+ r => r.targetFront != commands.targetCommand.targetFront
+ );
+ assertEvents({
+ commands,
+ documentEvents: [null /* no will-navigate */, ...topTargetEvents],
+ });
+ assertEvents({
+ commands,
+ documentEvents: [null /* no will-navigate */, ...iframeTargetEvents],
+ expectedTargetFront: iframeTarget,
+ });
+ } else {
+ assertEvents({
+ commands,
+ documentEvents: [null /* no will-navigate */, ...documentEvents],
+ });
+ }
+
+ info("Navigate the iframe to another process (if fission is enabled)");
+ documentEvents = [];
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [secondPageUrl],
+ function (url) {
+ const iframe = content.document.querySelector("iframe");
+ iframe.src = url;
+ }
+ );
+
+ // We are switching to a new target only when fission is enabled...
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ await waitFor(() => documentEvents.length >= 3);
+ is(
+ documentEvents.length,
+ 3,
+ "With fission/EFT, we switch to a new target and get: dom-loading, dom-interactive, dom-complete (but no will-navigate as that's only for the top BrowsingContext)"
+ );
+ const [, newIframeTarget] = await commands.targetCommand.getAllTargets([
+ commands.targetCommand.TYPES.FRAME,
+ ]);
+ assertEvents({
+ commands,
+ targetBeforeNavigation: iframeTarget,
+ documentEvents: [null /* no will-navigate */, ...documentEvents],
+ expectedTargetFront: newIframeTarget,
+ expectedNewURI: secondPageUrl,
+ });
+ } else {
+ // Wait for some time in order to let a chance to receive some unexpected events
+ await wait(250);
+ is(
+ documentEvents.length,
+ 0,
+ "If fission is disabled, we navigate within the same process, we get no new target and no new resource"
+ );
+ }
+
+ await commands.destroy();
+}
+
+function isBfCacheInParentEnabled() {
+ return (
+ Services.appinfo.sessionHistoryInParent &&
+ Services.prefs.getBoolPref("fission.bfcacheInParent", false)
+ );
+}
+
+async function testBfCacheNavigation() {
+ info("Test bfcache navigations for DOCUMENT_EVENT");
+
+ info("Open a first document and navigate to a second one");
+ const firstLocation = "data:text/html,<title>first</title>first page";
+ const secondLocation = "data:text/html,<title>second</title>second page";
+ const tab = await addTab(firstLocation);
+ const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ secondLocation
+ );
+ await onLoaded;
+
+ const { commands } = await initResourceCommand(tab);
+
+ const documentEvents = [];
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: resources => {
+ documentEvents.push(...resources);
+ },
+ ignoreExistingResources: true,
+ }
+ );
+ // Wait for some time for extra safety
+ await wait(250);
+ is(documentEvents.length, 0, "Existing document events are not fired");
+
+ info("Navigate back to the first page");
+ const onSwitched = commands.targetCommand.once("switched-target");
+ const targetBeforeNavigation = commands.targetCommand.targetFront;
+ gBrowser.goBack();
+
+ // We are switching to a new target only when fission/EFT is enabled...
+ if (
+ (isFissionEnabled() || isEveryFrameTargetEnabled()) &&
+ isBfCacheInParentEnabled()
+ ) {
+ await onSwitched;
+ }
+
+ info(
+ "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events"
+ );
+ await waitFor(() => documentEvents.length >= 4);
+ /* Ignore will-navigate timestamp as all other DOCUMENT_EVENTS will be set at the original load date,
+ which is when we loaded from the network, and not when we loaded from bfcache */
+ assertEvents({
+ commands,
+ targetBeforeNavigation,
+ documentEvents,
+ ignoreWillNavigateTimestamp: true,
+ });
+
+ // Wait for some time in order to let a chance to have duplicated dom-loading events
+ await wait(250);
+
+ is(
+ documentEvents.length,
+ 4,
+ "There is no duplicated event and only the 4 expected DOCUMENT_EVENT states"
+ );
+ const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] =
+ documentEvents;
+
+ is(
+ willNavigateEvent.name,
+ "will-navigate",
+ "The first DOCUMENT_EVENT is will-navigate"
+ );
+ is(
+ loadingEvent.name,
+ "dom-loading",
+ "The second DOCUMENT_EVENT is dom-loading"
+ );
+ is(
+ interactiveEvent.name,
+ "dom-interactive",
+ "The third DOCUMENT_EVENT is dom-interactive"
+ );
+ is(
+ completeEvent.name,
+ "dom-complete",
+ "The fourth DOCUMENT_EVENT is dom-complete"
+ );
+
+ is(
+ loadingEvent.url,
+ firstLocation,
+ `resource ${loadingEvent.name} has expected url after navigation back`
+ );
+ is(
+ loadingEvent.title,
+ undefined,
+ `resource ${loadingEvent.name} does not have a title property after navigating back`
+ );
+
+ is(
+ interactiveEvent.url,
+ firstLocation,
+ `resource ${interactiveEvent.name} has expected url property after navigating back`
+ );
+ is(
+ interactiveEvent.title,
+ "first",
+ `resource ${interactiveEvent.name} has expected title after navigating back`
+ );
+
+ is(
+ completeEvent.url,
+ undefined,
+ `resource ${completeEvent.name} does not have a url property after navigating back`
+ );
+ is(
+ completeEvent.title,
+ undefined,
+ `resource ${completeEvent.name} does not have a title property after navigating back`
+ );
+
+ await commands.destroy();
+}
+
+async function testCrossOriginNavigation() {
+ info("Test cross origin navigations for DOCUMENT_EVENT");
+
+ const tab = await addTab("https://example.com/document-builder.sjs?html=com");
+
+ const { commands } = await initResourceCommand(tab);
+
+ const documentEvents = [];
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: resources => documentEvents.push(...resources),
+ ignoreExistingResources: true,
+ }
+ );
+ // Wait for some time for extra safety
+ await wait(250);
+ is(documentEvents.length, 0, "Existing document events are not fired");
+
+ info("Navigate to another process");
+ const onSwitched = commands.targetCommand.once("switched-target");
+ const netUrl =
+ "https://example.net/document-builder.sjs?html=<head><title>titleNet</title></head>net";
+ const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ const targetBeforeNavigation = commands.targetCommand.targetFront;
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, netUrl);
+ await onLoaded;
+
+ // We are switching to a new target only when fission is enabled...
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ await onSwitched;
+ }
+
+ info(
+ "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events"
+ );
+ await waitFor(() => documentEvents.length >= 4);
+ assertEvents({ commands, targetBeforeNavigation, documentEvents });
+
+ // Wait for some time in order to let a chance to have duplicated dom-loading events
+ await wait(250);
+
+ is(
+ documentEvents.length,
+ 4,
+ "There is no duplicated event and only the 4 expected DOCUMENT_EVENT states"
+ );
+ const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] =
+ documentEvents;
+
+ is(
+ willNavigateEvent.name,
+ "will-navigate",
+ "The first DOCUMENT_EVENT is will-navigate"
+ );
+ is(
+ loadingEvent.name,
+ "dom-loading",
+ "The second DOCUMENT_EVENT is dom-loading"
+ );
+ is(
+ interactiveEvent.name,
+ "dom-interactive",
+ "The third DOCUMENT_EVENT is dom-interactive"
+ );
+ is(
+ completeEvent.name,
+ "dom-complete",
+ "The fourth DOCUMENT_EVENT is dom-complete"
+ );
+
+ is(
+ loadingEvent.url,
+ encodeURI(netUrl),
+ `resource ${loadingEvent.name} has expected url after reloading`
+ );
+ is(
+ loadingEvent.title,
+ undefined,
+ `resource ${loadingEvent.name} does not have a title property after reloading`
+ );
+
+ is(
+ interactiveEvent.url,
+ encodeURI(netUrl),
+ `resource ${interactiveEvent.name} has expected url property after reloading`
+ );
+ is(
+ interactiveEvent.title,
+ "titleNet",
+ `resource ${interactiveEvent.name} has expected title after reloading`
+ );
+
+ is(
+ completeEvent.url,
+ undefined,
+ `resource ${completeEvent.name} does not have a url property after reloading`
+ );
+ is(
+ completeEvent.title,
+ undefined,
+ `resource ${completeEvent.name} does not have a title property after reloading`
+ );
+
+ await commands.destroy();
+}
+
+async function testDomCompleteWithOverloadedConsole() {
+ info("Test dom-complete with an overloaded console object");
+
+ const tab = await addTab(
+ "data:text/html,<script>window.console = {};</script>"
+ );
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check that all DOCUMENT_EVENTS are fired for the already loaded page");
+ const documentEvents = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], {
+ onAvailable: resources => documentEvents.push(...resources),
+ });
+ is(documentEvents.length, 3, "Existing document events are fired");
+
+ const domComplete = documentEvents[2];
+ is(domComplete.name, "dom-complete", "the last resource is the dom-complete");
+ is(
+ domComplete.hasNativeConsoleAPI,
+ false,
+ "the console object is reported to be overloaded"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testDomCompleteWithWindowStop() {
+ info("Test dom-complete with a page calling window.stop()");
+
+ const tab = await addTab("data:text/html,foo");
+
+ const { commands, client, resourceCommand, targetCommand } =
+ await initResourceCommand(tab);
+
+ info("Check that all DOCUMENT_EVENTS are fired for the already loaded page");
+ let documentEvents = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], {
+ onAvailable: resources => documentEvents.push(...resources),
+ });
+ is(documentEvents.length, 3, "Existing document events are fired");
+ documentEvents = [];
+
+ const html = `<!DOCTYPE html><html>
+ <head>
+ <title>stopped page</title>
+ <script>window.stop();</script>
+ </head>
+ <body>Page content that shouldn't be displayed</body>
+</html>`;
+ const secondLocation = "data:text/html," + encodeURIComponent(html);
+ const targetBeforeNavigation = commands.targetCommand.targetFront;
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ secondLocation
+ );
+ info(
+ "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events"
+ );
+ await waitFor(() => documentEvents.length === 4);
+
+ assertEvents({ commands, targetBeforeNavigation, documentEvents });
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function assertPromises(
+ commands,
+ targetBeforeNavigation,
+ onWillNavigate,
+ onLoading,
+ onInteractive,
+ onComplete
+) {
+ const willNavigateEvent = await onWillNavigate;
+ const loadingEvent = await onLoading;
+ const interactiveEvent = await onInteractive;
+ const completeEvent = await onComplete;
+ assertEvents({
+ commands,
+ targetBeforeNavigation,
+ documentEvents: [
+ willNavigateEvent,
+ loadingEvent,
+ interactiveEvent,
+ completeEvent,
+ ],
+ });
+}
+
+function assertEvents({
+ commands,
+ targetBeforeNavigation,
+ documentEvents,
+ expectedTargetFront = commands.targetCommand.targetFront,
+ expectedNewURI = gBrowser.selectedBrowser.currentURI.spec,
+ ignoreWillNavigateTimestamp = false,
+}) {
+ const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] =
+ documentEvents;
+ if (willNavigateEvent) {
+ is(willNavigateEvent.name, "will-navigate", "Received the will-navigate");
+ is(
+ willNavigateEvent.newURI,
+ expectedNewURI,
+ "will-navigate newURI is set to the current tab new location"
+ );
+ }
+ is(
+ loadingEvent.name,
+ "dom-loading",
+ "loading received in the exepected order"
+ );
+ is(
+ interactiveEvent.name,
+ "dom-interactive",
+ "interactive received in the expected order"
+ );
+ is(completeEvent.name, "dom-complete", "complete received last");
+
+ if (willNavigateEvent) {
+ is(
+ typeof willNavigateEvent.time,
+ "number",
+ `Type of time attribute for will-navigate event is correct (${willNavigateEvent.time})`
+ );
+ }
+ is(
+ typeof loadingEvent.time,
+ "number",
+ `Type of time attribute for loading event is correct (${loadingEvent.time})`
+ );
+ is(
+ typeof interactiveEvent.time,
+ "number",
+ `Type of time attribute for interactive event is correct (${interactiveEvent.time})`
+ );
+ is(
+ typeof completeEvent.time,
+ "number",
+ `Type of time attribute for complete event is correct (${completeEvent.time})`
+ );
+
+ if (willNavigateEvent && !ignoreWillNavigateTimestamp) {
+ Assert.lessOrEqual(
+ willNavigateEvent.time,
+ loadingEvent.time,
+ `Timestamp for dom-loading event is greater than will-navigate event (${willNavigateEvent.time} <= ${loadingEvent.time})`
+ );
+ }
+ Assert.lessOrEqual(
+ loadingEvent.time,
+ interactiveEvent.time,
+ `Timestamp for interactive event is greater than loading event (${loadingEvent.time} <= ${interactiveEvent.time})`
+ );
+ Assert.lessOrEqual(
+ interactiveEvent.time,
+ completeEvent.time,
+ `Timestamp for complete event is greater than interactive event (${interactiveEvent.time} <= ${completeEvent.time}).`
+ );
+
+ if (willNavigateEvent) {
+ // If we switched to a new target, this target will be different from currentTargetFront.
+ // This only happen if we navigate to another process or if server target switching is enabled.
+ is(
+ willNavigateEvent.targetFront,
+ targetBeforeNavigation,
+ "will-navigate target was the one before the navigation"
+ );
+ }
+ is(
+ loadingEvent.targetFront,
+ expectedTargetFront,
+ "loading target is the expected one"
+ );
+ is(
+ interactiveEvent.targetFront,
+ expectedTargetFront,
+ "interactive target is the expected one"
+ );
+ is(
+ completeEvent.targetFront,
+ expectedTargetFront,
+ "complete target is the expected one"
+ );
+
+ is(
+ completeEvent.hasNativeConsoleAPI,
+ true,
+ "None of the tests (except the dedicated one) overload the console object"
+ );
+}
+
+class ResourceListener {
+ _listeners = new Map();
+
+ dispatch(resources) {
+ for (const resource of resources) {
+ const resolve = this._listeners.get(resource.name);
+ if (resolve) {
+ resolve(resource);
+ this._listeners.delete(resource.name);
+ }
+ }
+ }
+
+ once(resourceName) {
+ return new Promise(r => this._listeners.set(resourceName, r));
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_error_messages.js b/devtools/shared/commands/resource/tests/browser_resources_error_messages.js
new file mode 100644
index 0000000000..6f94266e4c
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_error_messages.js
@@ -0,0 +1,877 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around ERROR_MESSAGE
+// Reproduces assertions from devtools/shared/webconsole/test/chrome/test_page_errors.html
+
+// Create a simple server so we have a nice sourceName in the resources packets.
+const httpServer = createTestHTTPServer();
+httpServer.registerPathHandler(`/test_page_errors.html`, (req, res) => {
+ res.setStatusLine(req.httpVersion, 200, "OK");
+ res.write(`<!DOCTYPE html><meta charset=utf8>Test Error Messages`);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/test_page_errors.html`;
+
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ await testErrorMessagesResources();
+ await testErrorMessagesResourcesWithIgnoreExistingResources();
+});
+
+async function testErrorMessagesResources() {
+ // Open a test tab
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const receivedMessages = [];
+ // The expected messages are the errors, twice (once for cached messages, once for live messages)
+ const expectedMessages = Array.from(expectedPageErrors.values()).concat(
+ Array.from(expectedPageErrors.values())
+ );
+
+ info(
+ "Log some errors *before* calling ResourceCommand.watchResources in order to assert" +
+ " the behavior of already existing messages."
+ );
+ await triggerErrors(tab);
+
+ let done;
+ const onAllErrorReceived = new Promise(resolve => (done = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ const { pageError } = resource;
+
+ is(
+ resource.targetFront,
+ targetCommand.targetFront,
+ "The targetFront property is the expected one"
+ );
+
+ if (!pageError.sourceName.includes("test_page_errors")) {
+ info(`Ignore error from unknown source: "${pageError.sourceName}"`);
+ continue;
+ }
+
+ const index = receivedMessages.length;
+ receivedMessages.push(resource);
+
+ const isAlreadyExistingResource =
+ receivedMessages.length <= expectedPageErrors.size;
+ is(
+ resource.isAlreadyExistingResource,
+ isAlreadyExistingResource,
+ "isAlreadyExistingResource has expected value"
+ );
+
+ info(`checking received page error #${index}: ${pageError.errorMessage}`);
+ ok(pageError, "The resource has a pageError attribute");
+ checkPageErrorResource(pageError, expectedMessages[index]);
+
+ if (receivedMessages.length == expectedMessages.length) {
+ done();
+ }
+ }
+ };
+
+ await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable,
+ });
+
+ await BrowserTestUtils.waitForCondition(
+ () => receivedMessages.length === expectedPageErrors.size
+ );
+
+ info(
+ "Now log errors *after* the call to ResourceCommand.watchResources and after having" +
+ " received all existing messages"
+ );
+ await triggerErrors(tab);
+
+ info("Waiting for all expected errors to be received");
+ await onAllErrorReceived;
+ ok(true, "All the expected errors were received");
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testErrorMessagesResourcesWithIgnoreExistingResources() {
+ info("Test ignoreExistingResources option for ERROR_MESSAGE");
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info(
+ "Check whether onAvailable will not be called with existing error messages"
+ );
+ await triggerErrors(tab);
+
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable: resources => availableResources.push(...resources),
+ ignoreExistingResources: true,
+ });
+ is(
+ availableResources.length,
+ 0,
+ "onAvailable wasn't called for existing error messages"
+ );
+
+ info(
+ "Check whether onAvailable will be called with the future error messages"
+ );
+ await triggerErrors(tab);
+
+ const expectedMessages = Array.from(expectedPageErrors.values());
+ await waitUntil(() => availableResources.length === expectedMessages.length);
+ for (let i = 0; i < expectedMessages.length; i++) {
+ const resource = availableResources[i];
+ const { pageError } = resource;
+ const expected = expectedMessages[i];
+ checkPageErrorResource(pageError, expected);
+ is(
+ resource.isAlreadyExistingResource,
+ false,
+ "isAlreadyExistingResource is set to false for live messages"
+ );
+ }
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+}
+
+/**
+ * Triggers all the errors in the content page.
+ */
+async function triggerErrors(tab) {
+ for (const [expression, expected] of expectedPageErrors.entries()) {
+ if (
+ !expected[noUncaughtException] &&
+ !Services.appinfo.browserTabsRemoteAutostart
+ ) {
+ expectUncaughtException();
+ }
+
+ await ContentTask.spawn(
+ tab.linkedBrowser,
+ expression,
+ function frameScript(expr) {
+ const document = content.document;
+ const scriptEl = document.createElement("script");
+ scriptEl.textContent = expr;
+ document.body.appendChild(scriptEl);
+ }
+ );
+
+ if (expected.isPromiseRejection) {
+ // Wait a bit after an uncaught promise rejection error, as they are not emitted
+ // right away.
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(res => setTimeout(res, 10));
+ }
+ }
+}
+
+function checkPageErrorResource(pageErrorResource, expected) {
+ // Let's remove test harness related frames in stacktrace
+ const clonedPageErrorResource = { ...pageErrorResource };
+ if (clonedPageErrorResource.stacktrace) {
+ const index = clonedPageErrorResource.stacktrace.findIndex(frame =>
+ frame.filename.startsWith("resource://testing-common/content-task.js")
+ );
+ if (index > -1) {
+ clonedPageErrorResource.stacktrace =
+ clonedPageErrorResource.stacktrace.slice(0, index);
+ }
+ }
+ checkObject(clonedPageErrorResource, expected);
+}
+
+const noUncaughtException = Symbol();
+const NUMBER_REGEX = /^\d+$/;
+// timeStamp are the result of a number in microsecond divided by 1000.
+// so we can't expect a precise number of decimals, or even if there would
+// be decimals at all.
+const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;
+
+const mdnUrl = path =>
+ `https://developer.mozilla.org/${path}?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default`;
+
+const expectedPageErrors = new Map([
+ [
+ "document.doTheImpossible();",
+ {
+ errorMessage: /doTheImpossible/,
+ errorMessageName: "JSMSG_NOT_FUNCTION",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Not_a_function"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 10,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "(42).toString(0);",
+ {
+ errorMessage: /radix/,
+ errorMessageName: "JSMSG_BAD_RADIX",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Bad_radix"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 6,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;",
+ {
+ errorMessage: /read.only/,
+ errorMessageName: "JSMSG_READ_ONLY",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Read-only"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 23,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "([]).length = -1",
+ {
+ errorMessage: /array length/,
+ errorMessageName: "JSMSG_BAD_ARRAY_LENGTH",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Invalid_array_length"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 2,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "'abc'.repeat(-1);",
+ {
+ errorMessage: /repeat count.*non-negative/,
+ errorMessageName: "JSMSG_NEGATIVE_REPETITION_COUNT",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Negative_repetition_count"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: "self-hosted",
+ sourceId: null,
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ functionName: "repeat",
+ },
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "'a'.repeat(2e28);",
+ {
+ errorMessage: /repeat count.*less than infinity/,
+ errorMessageName: "JSMSG_RESULTING_STRING_TOO_LARGE",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Resulting_string_too_large"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: "self-hosted",
+ sourceId: null,
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ functionName: "repeat",
+ },
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 5,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "77.1234.toExponential(-1);",
+ {
+ errorMessage: /out of range/,
+ errorMessageName: "JSMSG_PRECISION_RANGE",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Precision_range"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 9,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "function a() { return; 1 + 1; }",
+ {
+ errorMessage: /unreachable code/,
+ errorMessageName: "JSMSG_STMT_AFTER_RETURN",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ info: false,
+ sourceId: null,
+ lineText: "function a() { return; 1 + 1; }",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Stmt_after_return"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: null,
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "{let a, a;}",
+ {
+ errorMessage: /redeclaration of/,
+ errorMessageName: "JSMSG_REDECLARED_VAR",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ sourceId: null,
+ lineText: "{let a, a;}",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Redeclared_parameter"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [],
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ notes: [
+ {
+ messageBody: /Previously declared at line/,
+ frame: {
+ source: /test_page_errors/,
+ },
+ },
+ ],
+ },
+ ],
+ [
+ `var error = new TypeError("abc");
+ error.name = "MyError";
+ error.message = "here";
+ throw error`,
+ {
+ errorMessage: /MyError: here/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: undefined,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 13,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "DOMTokenList.prototype.contains.call([])",
+ {
+ errorMessage: /does not implement interface/,
+ errorMessageName: "MSG_METHOD_THIS_DOES_NOT_IMPLEMENT_INTERFACE",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: undefined,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 33,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ `
+ function promiseThrow() {
+ var error2 = new TypeError("abc");
+ error2.name = "MyPromiseError";
+ error2.message = "here2";
+ return Promise.reject(error2);
+ }
+ promiseThrow()`,
+ {
+ errorMessage: /MyPromiseError: here2/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: undefined,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ sourceId: null,
+ lineNumber: 6,
+ columnNumber: 24,
+ functionName: "promiseThrow",
+ },
+ {
+ filename: /test_page_errors\.html/,
+ sourceId: null,
+ lineNumber: 8,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: true,
+ isForwardedFromContentProcess: false,
+ [noUncaughtException]: true,
+ },
+ ],
+ [
+ // Error with a cause
+ `var originalError = new TypeError("abc");
+ var error = new Error("something went wrong", { cause: originalError })
+ throw error`,
+ {
+ errorMessage: /Error: something went wrong/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 2,
+ columnNumber: 19,
+ functionName: null,
+ },
+ ],
+ exception: {
+ preview: {
+ cause: {
+ class: "TypeError",
+ preview: {
+ message: "abc",
+ },
+ },
+ },
+ },
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ // Error with a cause chain
+ `var a = new Error("err-a");
+ var b = new Error("err-b", { cause: a });
+ var c = new Error("err-c", { cause: b });
+ var d = new Error("err-d", { cause: c });
+ throw d`,
+ {
+ errorMessage: /Error: err-d/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 4,
+ columnNumber: 14,
+ functionName: null,
+ },
+ ],
+ exception: {
+ preview: {
+ cause: {
+ class: "Error",
+ preview: {
+ message: "err-c",
+ cause: {
+ class: "Error",
+ preview: {
+ message: "err-b",
+ cause: {
+ class: "Error",
+ preview: {
+ message: "err-a",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ // Error with a null cause
+ `throw new Error("something went wrong", { cause: null })`,
+ {
+ errorMessage: /Error: something went wrong/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ exception: {
+ preview: {
+ cause: {
+ type: "null",
+ },
+ },
+ },
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ // Error with an undefined cause
+ `throw new Error("something went wrong", { cause: undefined })`,
+ {
+ errorMessage: /Error: something went wrong/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ exception: {
+ preview: {
+ cause: {
+ type: "undefined",
+ },
+ },
+ },
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ // Error with a number cause
+ `throw new Error("something went wrong", { cause: 0 })`,
+ {
+ errorMessage: /Error: something went wrong/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ exception: {
+ preview: {
+ cause: 0,
+ },
+ },
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ // Error with a string cause
+ `throw new Error("something went wrong", { cause: "ooops" })`,
+ {
+ errorMessage: /Error: something went wrong/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ exception: {
+ preview: {
+ cause: "ooops",
+ },
+ },
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+]);
diff --git a/devtools/shared/commands/resource/tests/browser_resources_getAllResources.js b/devtools/shared/commands/resource/tests/browser_resources_getAllResources.js
new file mode 100644
index 0000000000..10bc8390d9
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_getAllResources.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test getAllResources function of the ResourceCommand.
+
+const TEST_URI = "data:text/html;charset=utf-8,getAllResources test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check the resources gotten from getAllResources at initial");
+ is(
+ resourceCommand.getAllResources(resourceCommand.TYPES.CONSOLE_MESSAGE)
+ .length,
+ 0,
+ "There is no resources at initial"
+ );
+
+ info(
+ "Start to watch the available resources in order to compare with resources gotten from getAllResources"
+ );
+ const availableResources = [];
+ const onAvailable = resources => availableResources.push(...resources);
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ { onAvailable }
+ );
+
+ info("Check the resources after some resources are available");
+ const messages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, messages);
+
+ try {
+ await waitFor(() => availableResources.length === messages.length);
+ } catch (e) {
+ ok(
+ false,
+ `Didn't receive the expected number of resources. Got ${
+ availableResources.length
+ }, expected ${messages.length} - ${availableResources
+ .map(r => r.message.arguments[0])
+ .join(" - ")}`
+ );
+ }
+
+ assertResources(
+ resourceCommand.getAllResources(resourceCommand.TYPES.CONSOLE_MESSAGE),
+ availableResources
+ );
+ assertResources(
+ resourceCommand.getAllResources(resourceCommand.TYPES.STYLESHEET),
+ []
+ );
+
+ info("Check the resources after reloading");
+ await BrowserTestUtils.reloadTab(tab);
+ assertResources(
+ resourceCommand.getAllResources(resourceCommand.TYPES.CONSOLE_MESSAGE),
+ []
+ );
+
+ info("Append some resources again to test unwatching");
+ const newMessages = ["d", "e", "f"];
+ await logMessages(tab.linkedBrowser, messages);
+ try {
+ await waitFor(
+ () =>
+ resourceCommand.getAllResources(resourceCommand.TYPES.CONSOLE_MESSAGE)
+ .length === newMessages.length
+ );
+ } catch (e) {
+ const resources = resourceCommand.getAllResources(
+ resourceCommand.TYPES.CONSOLE_MESSAGE
+ );
+ ok(
+ false,
+ `Didn't receive the expected number of resources. Got ${
+ resources.length
+ }, expected ${messages.length} - ${resources
+ .map(r => r.message.arguments.join(" | "))
+ .join(" - ")}`
+ );
+ }
+
+ info("Check the resources after unwatching");
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+ assertResources(
+ resourceCommand.getAllResources(resourceCommand.TYPES.CONSOLE_MESSAGE),
+ []
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+function assertResources(resources, expectedResources) {
+ is(
+ resources.length,
+ expectedResources.length,
+ "Number of the resources is correct"
+ );
+
+ for (let i = 0; i < resources.length; i++) {
+ const resource = resources[i];
+ const expectedResource = expectedResources[i];
+ Assert.strictEqual(
+ resource,
+ expectedResource,
+ `The ${i}th resource is correct`
+ );
+ }
+}
+
+function logMessages(browser, messages) {
+ return SpecialPowers.spawn(browser, [messages], innerMessages => {
+ for (const message of innerMessages) {
+ content.console.log(message);
+ }
+ });
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_invalid_api_usage.js b/devtools/shared/commands/resource/tests/browser_resources_invalid_api_usage.js
new file mode 100644
index 0000000000..8a1d809f04
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_invalid_api_usage.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test watch/unwatchResources throw when provided with invalid types.
+
+const TEST_URI = "data:text/html;charset=utf-8,invalid api usage test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const onAvailable = function () {};
+
+ await Assert.rejects(
+ resourceCommand.watchResources([null], { onAvailable }),
+ /ResourceCommand\.watchResources invoked with an unknown type/,
+ "watchResources should throw for null type"
+ );
+
+ await Assert.rejects(
+ resourceCommand.watchResources([undefined], { onAvailable }),
+ /ResourceCommand\.watchResources invoked with an unknown type/,
+ "watchResources should throw for undefined type"
+ );
+
+ await Assert.rejects(
+ resourceCommand.watchResources(["NOT_A_RESOURCE"], { onAvailable }),
+ /ResourceCommand\.watchResources invoked with an unknown type/,
+ "watchResources should throw for unknown type"
+ );
+
+ await Assert.rejects(
+ resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE, "NOT_A_RESOURCE"],
+ { onAvailable }
+ ),
+ /ResourceCommand\.watchResources invoked with an unknown type/,
+ "watchResources should throw for unknown type mixed with a correct type"
+ );
+
+ await Assert.throws(
+ () => resourceCommand.unwatchResources([null], { onAvailable }),
+ /ResourceCommand\.unwatchResources invoked with an unknown type/,
+ "unwatchResources should throw for null type"
+ );
+
+ await Assert.throws(
+ () => resourceCommand.unwatchResources([undefined], { onAvailable }),
+ /ResourceCommand\.unwatchResources invoked with an unknown type/,
+ "unwatchResources should throw for undefined type"
+ );
+
+ await Assert.throws(
+ () => resourceCommand.unwatchResources(["NOT_A_RESOURCE"], { onAvailable }),
+ /ResourceCommand\.unwatchResources invoked with an unknown type/,
+ "unwatchResources should throw for unknown type"
+ );
+
+ await Assert.throws(
+ () =>
+ resourceCommand.unwatchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE, "NOT_A_RESOURCE"],
+ { onAvailable }
+ ),
+ /ResourceCommand\.unwatchResources invoked with an unknown type/,
+ "unwatchResources should throw for unknown type mixed with a correct type"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_last_private_context_exit.js b/devtools/shared/commands/resource/tests/browser_resources_last_private_context_exit.js
new file mode 100644
index 0000000000..1e2d894be3
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_last_private_context_exit.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Verify that LAST_PRIVATE_CONTEXT_EXIT fires when closing the last opened private window
+
+"use strict";
+
+const NON_PRIVATE_TEST_URI =
+ "data:text/html;charset=utf8,<!DOCTYPE html>Not private";
+const PRIVATE_TEST_URI = `data:text/html;charset=utf8,<!DOCTYPE html>Test in private windows`;
+
+add_task(async function () {
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ const { commands } = await initMultiProcessResourceCommand();
+ const { resourceCommand } = commands;
+
+ const availableResources = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.LAST_PRIVATE_CONTEXT_EXIT],
+ {
+ onAvailable(resources) {
+ availableResources.push(resources);
+ },
+ }
+ );
+ is(
+ availableResources.length,
+ 0,
+ "We do not get any LAST_PRIVATE_CONTEXT_EXIT after initialization"
+ );
+
+ await addTab(NON_PRIVATE_TEST_URI);
+
+ info("Open a new private window and select the new tab opened in it");
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window is private");
+ const privateBrowser = privateWindow.gBrowser;
+ privateBrowser.selectedTab = BrowserTestUtils.addTab(
+ privateBrowser,
+ PRIVATE_TEST_URI
+ );
+
+ info("private tab opened");
+ ok(
+ PrivateBrowsingUtils.isBrowserPrivate(privateBrowser.selectedBrowser),
+ "tab window is private"
+ );
+
+ info("Open a second tab in the private window");
+ await addTab(PRIVATE_TEST_URI, { window: privateWindow });
+
+ // Let a chance to an unexpected async event to be fired
+ await wait(1000);
+
+ is(
+ availableResources.length,
+ 0,
+ "We do not get any LAST_PRIVATE_CONTEXT_EXIT when opening a private window"
+ );
+
+ info("Open a second private browsing window");
+ const secondPrivateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info("Close the second private window");
+ secondPrivateWindow.BrowserTryToCloseWindow();
+
+ // Let a chance to an unexpected async event to be fired
+ await wait(1000);
+
+ is(
+ availableResources.length,
+ 0,
+ "We do not get any LAST_PRIVATE_CONTEXT_EXIT when closing the second private window only"
+ );
+
+ info(
+ "close the private window and check if LAST_PRIVATE_CONTEXT_EXIT resource is sent"
+ );
+ privateWindow.BrowserTryToCloseWindow();
+
+ info("Wait for LAST_PRIVATE_CONTEXT_EXIT");
+ await waitFor(() => availableResources.length == 1);
+ is(
+ availableResources.length,
+ 1,
+ "We get one LAST_PRIVATE_CONTEXT_EXIT when closing the last opened private window"
+ );
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_network_event_stacktraces.js b/devtools/shared/commands/resource/tests/browser_resources_network_event_stacktraces.js
new file mode 100644
index 0000000000..2200fcad9c
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_network_event_stacktraces.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around NETWORK_EVENT_STACKTRACE
+
+const TEST_URI = `${URL_ROOT_SSL}network_document.html`;
+
+const REQUEST_STUB = {
+ code: `await fetch("/request_post_0.html", { method: "POST" });`,
+ expected: {
+ stacktraceAvailable: true,
+ lastFrame: {
+ filename:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/network_document.html",
+ lineNumber: 1,
+ columnNumber: 40,
+ functionName: "triggerRequest",
+ asyncCause: null,
+ },
+ },
+};
+
+add_task(async function () {
+ info("Test network stacktraces events");
+ const tab = await addTab(TEST_URI);
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const networkEvents = new Map();
+ const stackTraces = new Map();
+
+ function onResourceAvailable(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType === resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE
+ ) {
+ ok(
+ !networkEvents.has(resource.resourceId),
+ "The network event does not exist"
+ );
+
+ is(
+ resource.stacktraceAvailable,
+ REQUEST_STUB.expected.stacktraceAvailable,
+ "The stacktrace is available"
+ );
+ is(
+ JSON.stringify(resource.lastFrame),
+ JSON.stringify(REQUEST_STUB.expected.lastFrame),
+ "The last frame of the stacktrace is available"
+ );
+
+ stackTraces.set(resource.resourceId, true);
+ return;
+ }
+
+ if (resource.resourceType === resourceCommand.TYPES.NETWORK_EVENT) {
+ ok(
+ stackTraces.has(resource.stacktraceResourceId),
+ "The stack trace does exists"
+ );
+
+ networkEvents.set(resource.resourceId, true);
+ }
+ }
+ }
+
+ function onResourceUpdated() {}
+
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ ],
+ {
+ onAvailable: onResourceAvailable,
+ onUpdated: onResourceUpdated,
+ }
+ );
+
+ await triggerNetworkRequests(tab.linkedBrowser, [REQUEST_STUB.code]);
+
+ resourceCommand.unwatchResources(
+ [
+ resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ ],
+ {
+ onAvailable: onResourceAvailable,
+ onUpdated: onResourceUpdated,
+ }
+ );
+
+ targetCommand.destroy();
+ await client.close();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_network_events.js b/devtools/shared/commands/resource/tests/browser_resources_network_events.js
new file mode 100644
index 0000000000..da355fd023
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_network_events.js
@@ -0,0 +1,318 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around NETWORK_EVENT
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+// We are borrowing tests from the netmonitor frontend
+const NETMONITOR_TEST_FOLDER =
+ "https://example.com/browser/devtools/client/netmonitor/test/";
+const CSP_URL = `${NETMONITOR_TEST_FOLDER}html_csp-test-page.html`;
+const JS_CSP_URL = `${NETMONITOR_TEST_FOLDER}js_websocket-worker-test.js`;
+const CSS_CSP_URL = `${NETMONITOR_TEST_FOLDER}internal-loaded.css`;
+
+const CSP_BLOCKED_REASON_CODE = 4000;
+
+add_task(async function testContentProcessRequests() {
+ info(`Tests for NETWORK_EVENT resources fired from the content process`);
+
+ const expectedAvailable = [
+ {
+ url: CSP_URL,
+ method: "GET",
+ isNavigationRequest: true,
+ chromeContext: false,
+ },
+ {
+ url: JS_CSP_URL,
+ method: "GET",
+ blockedReason: CSP_BLOCKED_REASON_CODE,
+ isNavigationRequest: false,
+ chromeContext: false,
+ },
+ {
+ url: CSS_CSP_URL,
+ method: "GET",
+ blockedReason: CSP_BLOCKED_REASON_CODE,
+ isNavigationRequest: false,
+ chromeContext: false,
+ },
+ ];
+ const expectedUpdated = [
+ {
+ url: CSP_URL,
+ method: "GET",
+ isNavigationRequest: true,
+ chromeContext: false,
+ },
+ {
+ url: JS_CSP_URL,
+ method: "GET",
+ blockedReason: CSP_BLOCKED_REASON_CODE,
+ isNavigationRequest: false,
+ chromeContext: false,
+ },
+ {
+ url: CSS_CSP_URL,
+ method: "GET",
+ blockedReason: CSP_BLOCKED_REASON_CODE,
+ isNavigationRequest: false,
+ chromeContext: false,
+ },
+ ];
+
+ await assertNetworkResourcesOnPage(
+ CSP_URL,
+ expectedAvailable,
+ expectedUpdated
+ );
+});
+
+add_task(async function testCanceledRequest() {
+ info(`Tests for NETWORK_EVENT resources with a canceled request`);
+
+ // Do a XHR request that we cancel against a slow loading page
+ const requestUrl =
+ "https://example.org/document-builder.sjs?delay=1000&html=foo";
+ const html =
+ "<!DOCTYPE html><script>(" +
+ function (xhrUrl) {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", xhrUrl);
+ xhr.send(null);
+ } +
+ ")(" +
+ JSON.stringify(requestUrl) +
+ ")</script>";
+ const pageUrl =
+ "https://example.org/document-builder.sjs?html=" + encodeURIComponent(html);
+
+ const expectedAvailable = [
+ {
+ url: pageUrl,
+ method: "GET",
+ isNavigationRequest: true,
+ chromeContext: false,
+ },
+ {
+ url: requestUrl,
+ method: "GET",
+ isNavigationRequest: false,
+ blockedReason: "NS_BINDING_ABORTED",
+ chromeContext: false,
+ },
+ ];
+ const expectedUpdated = [
+ {
+ url: pageUrl,
+ method: "GET",
+ isNavigationRequest: true,
+ chromeContext: false,
+ },
+ {
+ url: requestUrl,
+ method: "GET",
+ isNavigationRequest: false,
+ blockedReason: "NS_BINDING_ABORTED",
+ chromeContext: false,
+ },
+ ];
+
+ // Register a one-off listener to cancel the XHR request
+ // Using XMLHttpRequest's abort() method from the content process
+ // isn't reliable and would introduce many race condition in the test.
+ // Canceling the request via nsIRequest.cancel privileged method,
+ // from the parent process is much more reliable.
+ const observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(subject, topic, data) {
+ subject = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (subject.URI.spec == requestUrl) {
+ subject.cancel(Cr.NS_BINDING_ABORTED);
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ }
+ },
+ };
+ Services.obs.addObserver(observer, "http-on-modify-request");
+
+ await assertNetworkResourcesOnPage(
+ pageUrl,
+ expectedAvailable,
+ expectedUpdated
+ );
+});
+
+add_task(async function testIframeRequest() {
+ info(`Tests for NETWORK_EVENT resources with an iframe`);
+
+ // Do a XHR request that we cancel against a slow loading page
+ const iframeRequestUrl =
+ "https://example.org/document-builder.sjs?html=iframe-request";
+ const iframeHtml = `iframe<script>fetch("${iframeRequestUrl}")</script>`;
+ const iframeUrl =
+ "https://example.org/document-builder.sjs?html=" +
+ encodeURIComponent(iframeHtml);
+ const html = `top-document<iframe src="${iframeUrl}"></iframe>`;
+ const pageUrl =
+ "https://example.org/document-builder.sjs?html=" + encodeURIComponent(html);
+
+ const expectedAvailable = [
+ {
+ url: pageUrl,
+ method: "GET",
+ chromeContext: false,
+ isNavigationRequest: true,
+ // The top level navigation request relates to the previous top level target.
+ // Unfortunately, it is hard to test because it is racy.
+ // The target front might be destroyed and `targetFront.url` will be null.
+ // Or not just yet and be equal to "about:blank".
+ },
+ {
+ url: iframeUrl,
+ method: "GET",
+ isNavigationRequest: false,
+ targetFrontUrl: pageUrl,
+ chromeContext: false,
+ },
+ {
+ url: iframeRequestUrl,
+ method: "GET",
+ isNavigationRequest: false,
+ targetFrontUrl: iframeUrl,
+ chromeContext: false,
+ },
+ ];
+ const expectedUpdated = [
+ {
+ url: pageUrl,
+ method: "GET",
+ isNavigationRequest: true,
+ chromeContext: false,
+ },
+ {
+ url: iframeUrl,
+ method: "GET",
+ isNavigationRequest: false,
+ chromeContext: false,
+ },
+ {
+ url: iframeRequestUrl,
+ method: "GET",
+ isNavigationRequest: false,
+ chromeContext: false,
+ },
+ ];
+
+ await assertNetworkResourcesOnPage(
+ pageUrl,
+ expectedAvailable,
+ expectedUpdated
+ );
+});
+
+async function assertNetworkResourcesOnPage(
+ url,
+ expectedAvailable,
+ expectedUpdated
+) {
+ // First open a blank document to avoid spawning any request
+ const tab = await addTab("about:blank");
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+ const { resourceCommand } = commands;
+
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ // Immediately assert the resource, as the same resource object
+ // will be notified to onUpdated and so if we assert it later
+ // we will not highlight attributes that aren't set yet from onAvailable.
+ const idx = expectedAvailable.findIndex(e => e.url === resource.url);
+ Assert.notEqual(
+ idx,
+ -1,
+ "Found a matching available notification for: " + resource.url
+ );
+ // Remove the match from the list in case there is many requests with the same url
+ const [expected] = expectedAvailable.splice(idx, 1);
+
+ assertResources(resource, expected);
+ }
+ };
+ const onUpdated = updates => {
+ for (const { resource } of updates) {
+ const idx = expectedUpdated.findIndex(e => e.url === resource.url);
+ Assert.notEqual(
+ idx,
+ -1,
+ "Found a matching updated notification for: " + resource.url
+ );
+ // Remove the match from the list in case there is many requests with the same url
+ const [expected] = expectedUpdated.splice(idx, 1);
+
+ assertResources(resource, expected);
+ }
+ };
+
+ // Start observing for network events before loading the test page
+ await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable,
+ onUpdated,
+ });
+
+ // Load the test page that fires network requests
+ const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
+ await onLoaded;
+
+ // Make sure we processed all the expected request updates
+ await waitFor(
+ () => !expectedAvailable.length,
+ "Wait for all expected available notifications"
+ );
+ await waitFor(
+ () => !expectedUpdated.length,
+ "Wait for all expected updated notifications"
+ );
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable,
+ onUpdated,
+ });
+
+ await commands.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+function assertResources(actual, expected) {
+ is(
+ actual.resourceType,
+ ResourceCommand.TYPES.NETWORK_EVENT,
+ "The resource type is correct"
+ );
+ is(
+ typeof actual.innerWindowId,
+ "number",
+ "All requests have an innerWindowId attribute"
+ );
+ ok(
+ actual.targetFront.isTargetFront,
+ "All requests have a targetFront attribute"
+ );
+
+ for (const name in expected) {
+ if (name == "targetFrontUrl") {
+ is(
+ actual.targetFront.url,
+ expected[name],
+ "The request matches the right target front"
+ );
+ } else {
+ is(actual[name], expected[name], `The '${name}' attribute is correct`);
+ }
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_network_events_cache.js b/devtools/shared/commands/resource/tests/browser_resources_network_events_cache.js
new file mode 100644
index 0000000000..6708ef19e1
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_network_events_cache.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API internal cache / ignoreExistingResources around NETWORK_EVENT
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const EXAMPLE_DOMAIN = "https://example.com/";
+const TEST_URI = `${URL_ROOT_SSL}network_document.html`;
+
+add_task(async function () {
+ info("Test basic NETWORK_EVENT resources against ResourceCommand cache");
+ await testNetworkEventResourcesWithExistingResources();
+ await testNetworkEventResourcesWithoutExistingResources();
+});
+
+async function testNetworkEventResourcesWithExistingResources() {
+ info(`Tests for network event resources with the existing resources`);
+ await testNetworkEventResourcesWithCachedRequest({
+ ignoreExistingResources: false,
+ // 1 available event fired, for the existing resource in the cache.
+ // 1 available event fired, when live request is created.
+ expectedResourcesOnAvailable: {
+ [`${EXAMPLE_DOMAIN}cached_post.html`]: {
+ resourceType: ResourceCommand.TYPES.NETWORK_EVENT,
+ method: "POST",
+ isNavigationRequest: false,
+ },
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceCommand.TYPES.NETWORK_EVENT,
+ method: "GET",
+ isNavigationRequest: false,
+ },
+ },
+ // 1 update events fired, when live request is updated.
+ expectedResourcesOnUpdated: {
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceCommand.TYPES.NETWORK_EVENT,
+ method: "GET",
+ },
+ },
+ });
+}
+
+async function testNetworkEventResourcesWithoutExistingResources() {
+ info(`Tests for network event resources without the existing resources`);
+ await testNetworkEventResourcesWithCachedRequest({
+ ignoreExistingResources: true,
+ // 1 available event fired, when live request is created.
+ expectedResourcesOnAvailable: {
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceCommand.TYPES.NETWORK_EVENT,
+ method: "GET",
+ isNavigationRequest: false,
+ },
+ },
+ // 1 update events fired, when live request is updated.
+ expectedResourcesOnUpdated: {
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceCommand.TYPES.NETWORK_EVENT,
+ method: "GET",
+ },
+ },
+ });
+}
+
+/**
+ * This test helper is slightly complex as we workaround the fact
+ * that the server is not able to record network request done in the past.
+ * Because of that we have to start observer requests via ResourceCommand.watchResources
+ * before doing a request, and, before doing the actual call to watchResources
+ * we want to assert the behavior of.
+ */
+async function testNetworkEventResourcesWithCachedRequest(options) {
+ const tab = await addTab(TEST_URI);
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const { resourceCommand } = commands;
+
+ info(
+ `Trigger some network requests *before* calling ResourceCommand.watchResources
+ in order to assert the behavior of already existing network events.`
+ );
+
+ // Register a first empty listener in order to ensure populating ResourceCommand
+ // internal cache of NETWORK_EVENT's. We can't retrieved past network requests
+ // when calling server's `watchResources`.
+ let resolveCachedRequestAvailable;
+ const onCachedRequestAvailable = new Promise(
+ r => (resolveCachedRequestAvailable = r)
+ );
+ const onAvailableToPopulateInternalCache = () => {};
+ const onUpdatedToPopulateInternalCache = resolveCachedRequestAvailable;
+ await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ ignoreExistingResources: true,
+ onAvailable: onAvailableToPopulateInternalCache,
+ onUpdated: onUpdatedToPopulateInternalCache,
+ });
+
+ // We can only trigger the requests once `watchResources` settles,
+ // otherwise we might miss some events and they won't be present in the cache
+ const cachedRequest = `await fetch("/cached_post.html", { method: "POST" });`;
+ await triggerNetworkRequests(tab.linkedBrowser, [cachedRequest]);
+
+ // We have to ensure that ResourceCommand processed the Resource for this first
+ // cached request before calling watchResource a second time and report it.
+ // Wait for the updated notification to avoid receiving it during the next call
+ // to watchResources.
+ await onCachedRequestAvailable;
+
+ const actualResourcesOnAvailable = {};
+ const actualResourcesOnUpdated = {};
+
+ const {
+ expectedResourcesOnAvailable,
+ expectedResourcesOnUpdated,
+
+ ignoreExistingResources,
+ } = options;
+
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.resourceType,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ "Received a network event resource"
+ );
+ actualResourcesOnAvailable[resource.url] = resource;
+ }
+ };
+
+ const onUpdated = updates => {
+ for (const { resource } of updates) {
+ is(
+ resource.resourceType,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ "Received a network update event resource"
+ );
+ actualResourcesOnUpdated[resource.url] = resource;
+ }
+ };
+
+ await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources,
+ });
+
+ info(
+ `Trigger the rest of the requests *after* calling ResourceCommand.watchResources
+ in order to assert the behavior of live network events.`
+ );
+ const liveRequest = `await fetch("/live_get.html", { method: "GET" });`;
+ await triggerNetworkRequests(tab.linkedBrowser, [liveRequest]);
+
+ info("Check the resources on available");
+
+ await waitUntil(
+ () =>
+ Object.keys(actualResourcesOnAvailable).length ==
+ Object.keys(expectedResourcesOnAvailable).length
+ );
+
+ is(
+ Object.keys(actualResourcesOnAvailable).length,
+ Object.keys(expectedResourcesOnAvailable).length,
+ "Got the expected number of network events fired onAvailable"
+ );
+
+ // assert the resources emitted when the network event is created
+ for (const key in expectedResourcesOnAvailable) {
+ const expected = expectedResourcesOnAvailable[key];
+ const actual = actualResourcesOnAvailable[key];
+ assertResources(actual, expected);
+ }
+
+ info("Check the resources on updated");
+
+ await waitUntil(
+ () =>
+ Object.keys(actualResourcesOnUpdated).length ==
+ Object.keys(expectedResourcesOnUpdated).length
+ );
+
+ is(
+ Object.keys(actualResourcesOnUpdated).length,
+ Object.keys(expectedResourcesOnUpdated).length,
+ "Got the expected number of network events fired onUpdated"
+ );
+
+ // assert the resources emitted when the network event is updated
+ for (const key in expectedResourcesOnUpdated) {
+ const expected = expectedResourcesOnUpdated[key];
+ const actual = actualResourcesOnUpdated[key];
+ assertResources(actual, expected);
+ // assert that the resourceId for the the available and updated events match
+ is(
+ actual.resourceId,
+ actualResourcesOnAvailable[key].resourceId,
+ `Available and update resource ids for ${key} are the same`
+ );
+ }
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources,
+ });
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable: onAvailableToPopulateInternalCache,
+ });
+
+ await commands.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+function assertResources(actual, expected) {
+ is(
+ actual.resourceType,
+ expected.resourceType,
+ "The resource type is correct"
+ );
+ is(actual.method, expected.method, "The method is correct");
+ if ("isNavigationRequest" in expected) {
+ is(
+ actual.isNavigationRequest,
+ expected.isNavigationRequest,
+ "The isNavigationRequest attribute is correct"
+ );
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_network_events_navigation.js b/devtools/shared/commands/resource/tests/browser_resources_network_events_navigation.js
new file mode 100644
index 0000000000..44028318a2
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_network_events_navigation.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around NETWORK_EVENT when navigating
+
+const TEST_URI = `${URL_ROOT_SSL}network_document_navigation.html`;
+const JS_URI = TEST_URI.replace(
+ "network_document_navigation.html",
+ "network_navigation.js"
+);
+
+add_task(async () => {
+ const tab = await addTab(TEST_URI);
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+ const { resourceCommand } = commands;
+
+ const receivedResources = [];
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.resourceType,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ "Received a network event resource"
+ );
+ receivedResources.push(resource);
+ }
+ };
+ const onUpdated = updates => {
+ for (const { resource } of updates) {
+ is(
+ resource.resourceType,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ "Received a network update event resource"
+ );
+ }
+ };
+
+ await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ ignoreExistingResources: true,
+ onAvailable,
+ onUpdated,
+ });
+
+ await reloadBrowser();
+
+ await waitFor(() => receivedResources.length == 2);
+
+ const navigationRequest = receivedResources[0];
+ is(
+ navigationRequest.url,
+ TEST_URI,
+ "The first resource is for the navigation request"
+ );
+
+ const jsRequest = receivedResources[1];
+ is(jsRequest.url, JS_URI, "The second resource is for the javascript file");
+
+ async function getResponseContent(networkEvent) {
+ const packet = {
+ to: networkEvent.actor,
+ type: "getResponseContent",
+ };
+ const response = await commands.client.request(packet);
+ return response.content.text;
+ }
+
+ const HTML_CONTENT = await (await fetch(TEST_URI)).text();
+ const JS_CONTENT = await (await fetch(JS_URI)).text();
+
+ const htmlContent = await getResponseContent(navigationRequest);
+ is(htmlContent, HTML_CONTENT);
+ const jsContent = await getResponseContent(jsRequest);
+ is(jsContent, JS_CONTENT);
+
+ await reloadBrowser();
+
+ await waitFor(() => receivedResources.length == 4);
+
+ try {
+ await getResponseContent(navigationRequest);
+ ok(false, "Shouldn't work");
+ } catch (e) {
+ is(
+ e.error,
+ "noSuchActor",
+ "Without persist, we can't fetch previous document network data"
+ );
+ }
+
+ try {
+ await getResponseContent(jsRequest);
+ ok(false, "Shouldn't work");
+ } catch (e) {
+ is(
+ e.error,
+ "noSuchActor",
+ "Without persist, we can't fetch previous document network data"
+ );
+ }
+
+ const navigationRequest2 = receivedResources[2];
+ const jsRequest2 = receivedResources[3];
+ info("But we can fetch data for the last/new document");
+ const htmlContent2 = await getResponseContent(navigationRequest2);
+ is(htmlContent2, HTML_CONTENT);
+ const jsContent2 = await getResponseContent(jsRequest2);
+ is(jsContent2, JS_CONTENT);
+
+ info("Enable persist");
+ const networkParentFront =
+ await commands.watcherFront.getNetworkParentActor();
+ await networkParentFront.setPersist(true);
+
+ await reloadBrowser();
+
+ await waitFor(() => receivedResources.length == 6);
+
+ info("With persist, we can fetch previous document network data");
+ const htmlContent3 = await getResponseContent(navigationRequest2);
+ is(htmlContent3, HTML_CONTENT);
+ const jsContent3 = await getResponseContent(jsRequest2);
+ is(jsContent3, JS_CONTENT);
+
+ await resourceCommand.unwatchResources(
+ [resourceCommand.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ }
+ );
+
+ await commands.destroy();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_network_events_parent_process.js b/devtools/shared/commands/resource/tests/browser_resources_network_events_parent_process.js
new file mode 100644
index 0000000000..c5b3e436db
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_network_events_parent_process.js
@@ -0,0 +1,249 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * !! AFTER MOVING OR RENAMING THIS METHOD, UPDATE `EXPECTED` CONSTANTS BELOW !!
+ */
+const createParentProcessRequests = async () => {
+ info("Do some requests from the parent process");
+ // The line:column for `fetch` should be EXPECTED_REQUEST_LINE_1/COL_1
+ await fetch(FETCH_URI);
+
+ const img = new Image();
+ const onLoad = new Promise(r => img.addEventListener("load", r));
+ // The line:column for `img` below should be EXPECTED_REQUEST_LINE_2/COL_2
+ img.src = IMAGE_URI;
+ await onLoad;
+};
+
+const EXPECTED_METHOD_NAME = "createParentProcessRequests";
+const EXPECTED_REQUEST_LINE_1 = 12;
+const EXPECTED_REQUEST_COL_1 = 9;
+const EXPECTED_REQUEST_LINE_2 = 17;
+const EXPECTED_REQUEST_COL_2 = 3;
+
+// Test the ResourceCommand API around NETWORK_EVENT for the parent process
+
+const FETCH_URI = "https://example.com/document-builder.sjs?html=foo";
+// The img.src request gets cached regardless of `devtools.cache.disabled`.
+// Add a random parameter to the request to bypass the cache.
+const uuid = `${Date.now()}-${Math.random()}`;
+const IMAGE_URI = URL_ROOT_SSL + "test_image.png?" + uuid;
+
+add_task(async function testParentProcessRequests() {
+ // The test expects the main process commands instance to receive resources
+ // for content process requests.
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const commands = await CommandsFactory.forMainProcess();
+ await commands.targetCommand.startListening();
+ const { resourceCommand } = commands;
+
+ const receivedNetworkEvents = [];
+ const receivedStacktraces = [];
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType == resourceCommand.TYPES.NETWORK_EVENT) {
+ receivedNetworkEvents.push(resource);
+ } else if (
+ resource.resourceType == resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE
+ ) {
+ receivedStacktraces.push(resource);
+ }
+ }
+ };
+ const onUpdated = updates => {
+ for (const { resource } of updates) {
+ is(
+ resource.resourceType,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ "Received a network update event resource"
+ );
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.NETWORK_EVENT,
+ resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ ],
+ {
+ ignoreExistingResources: true,
+ onAvailable,
+ onUpdated,
+ }
+ );
+
+ await createParentProcessRequests();
+
+ const img2 = new Image();
+ img2.src = IMAGE_URI;
+
+ info("Wait for the network events");
+ await waitFor(() => receivedNetworkEvents.length == 3);
+ info("Wait for the network events stack traces");
+ // Note that we aren't getting any stacktrace for the second cached request
+ await waitFor(() => receivedStacktraces.length == 2);
+
+ info("Assert the fetch request");
+ const fetchRequest = receivedNetworkEvents[0];
+ is(
+ fetchRequest.url,
+ FETCH_URI,
+ "The first resource is for the fetch request"
+ );
+ ok(fetchRequest.chromeContext, "The fetch request is privileged");
+
+ const fetchStacktrace = receivedStacktraces[0].lastFrame;
+ is(receivedStacktraces[0].resourceId, fetchRequest.stacktraceResourceId);
+ is(fetchStacktrace.filename, gTestPath);
+ is(fetchStacktrace.lineNumber, EXPECTED_REQUEST_LINE_1);
+ is(fetchStacktrace.columnNumber, EXPECTED_REQUEST_COL_1);
+ is(fetchStacktrace.functionName, EXPECTED_METHOD_NAME);
+ is(fetchStacktrace.asyncCause, null);
+
+ async function getResponseContent(networkEvent) {
+ const packet = {
+ to: networkEvent.actor,
+ type: "getResponseContent",
+ };
+ const response = await commands.client.request(packet);
+ return response.content.text;
+ }
+
+ const fetchContent = await getResponseContent(fetchRequest);
+ is(fetchContent, "foo");
+
+ info("Assert the first image request");
+ const firstImageRequest = receivedNetworkEvents[1];
+ is(
+ firstImageRequest.url,
+ IMAGE_URI,
+ "The second resource is for the first image request"
+ );
+ ok(!firstImageRequest.fromCache, "The first image request isn't cached");
+ ok(firstImageRequest.chromeContext, "The first image request is privileged");
+
+ const firstImageStacktrace = receivedStacktraces[1].lastFrame;
+ is(receivedStacktraces[1].resourceId, firstImageRequest.stacktraceResourceId);
+ is(firstImageStacktrace.filename, gTestPath);
+ is(firstImageStacktrace.lineNumber, EXPECTED_REQUEST_LINE_2);
+ is(firstImageStacktrace.columnNumber, EXPECTED_REQUEST_COL_2);
+ is(firstImageStacktrace.functionName, EXPECTED_METHOD_NAME);
+ is(firstImageStacktrace.asyncCause, null);
+
+ info("Assert the second image request");
+ const secondImageRequest = receivedNetworkEvents[2];
+ is(
+ secondImageRequest.url,
+ IMAGE_URI,
+ "The third resource is for the second image request"
+ );
+ ok(secondImageRequest.fromCache, "The second image request is cached");
+ ok(
+ secondImageRequest.chromeContext,
+ "The second image request is privileged"
+ );
+
+ info(
+ "Open a content page to ensure we also receive request from content processes"
+ );
+ const pageUrl = "https://example.org/document-builder.sjs?html=foo";
+ const requestUrl = "https://example.org/document-builder.sjs?html=bar";
+ const tab = await addTab(pageUrl);
+
+ await waitFor(() => receivedNetworkEvents.length == 4);
+ const tabRequest = receivedNetworkEvents[3];
+ is(tabRequest.url, pageUrl, "The 4th resource is for the tab request");
+ ok(!tabRequest.chromeContext, "The 4th request is content");
+
+ info(
+ "Also spawn a privileged request from the content process, not bound to any WindowGlobal"
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [requestUrl],
+ async function (uri) {
+ const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+ );
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ channel.open();
+ }
+ );
+ await removeTab(tab);
+
+ await waitFor(() => receivedNetworkEvents.length == 5);
+ const privilegedContentRequest = receivedNetworkEvents[4];
+ is(
+ privilegedContentRequest.url,
+ requestUrl,
+ "The 5th resource is for the privileged content process request"
+ );
+ ok(privilegedContentRequest.chromeContext, "The 5th request is privileged");
+
+ info("Now focus only on parent process resources");
+ await pushPref("devtools.browsertoolbox.scope", "parent-process");
+
+ info(
+ "Retrigger the two last requests. The tab document request and a privileged request. Both happening in the tab's content process."
+ );
+ const secondTab = await addTab(pageUrl);
+ await SpecialPowers.spawn(
+ secondTab.linkedBrowser,
+ [requestUrl],
+ async function (uri) {
+ const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+ );
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ channel.open();
+ }
+ );
+
+ await waitFor(() => receivedNetworkEvents.length == 6);
+
+ // nsIHttpChannel doesn't expose any attribute allowing to identify
+ // privileged requests done in content processes.
+ // Thus, preventing us from filtering them out correctly.
+ // Ideally, we would need some new attribute to know from which (content) process
+ // any channel originates from.
+ info(
+ "For now, we are still notified about the privileged content process request"
+ );
+ const secondPrivilegedContentRequest = receivedNetworkEvents[5];
+ is(
+ secondPrivilegedContentRequest.url,
+ requestUrl,
+ "The 6th resource is for the second privileged content process request"
+ );
+ ok(privilegedContentRequest.chromeContext, "The 6th request is privileged");
+
+ // Let some time to receive the tab request if that's not correctly filtered out
+ await wait(1000);
+ is(
+ receivedNetworkEvents.length,
+ 6,
+ "But we don't receive the request for the tab request"
+ );
+
+ await removeTab(secondTab);
+
+ await resourceCommand.unwatchResources(
+ [resourceCommand.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ }
+ );
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_platform_messages.js b/devtools/shared/commands/resource/tests/browser_resources_platform_messages.js
new file mode 100644
index 0000000000..4e74a97e38
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_platform_messages.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around PLATFORM_MESSAGE
+// Reproduces assertions from: devtools/shared/webconsole/test/chrome/test_nsiconsolemessage.html
+
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ await testPlatformMessagesResources();
+ await testPlatformMessagesResourcesWithIgnoreExistingResources();
+});
+
+async function testPlatformMessagesResources() {
+ const { client, resourceCommand, targetCommand } =
+ await initMultiProcessResourceCommand();
+
+ const cachedMessages = [
+ "This is a cached message",
+ "This is another cached message",
+ ];
+ const liveMessages = [
+ "This is a live message",
+ "This is another live message",
+ ];
+ const expectedMessages = [...cachedMessages, ...liveMessages];
+ const receivedMessages = [];
+
+ info(
+ "Log some messages *before* calling ResourceCommand.watchResources in order to assert the behavior of already existing messages."
+ );
+ Services.console.logStringMessage(expectedMessages[0]);
+ Services.console.logStringMessage(expectedMessages[1]);
+
+ let done;
+ const onAllMessagesReceived = new Promise(resolve => (done = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (!expectedMessages.includes(resource.message)) {
+ continue;
+ }
+
+ is(
+ resource.targetFront,
+ targetCommand.targetFront,
+ "The targetFront property is the expected one"
+ );
+
+ receivedMessages.push(resource.message);
+ is(
+ resource.message,
+ expectedMessages[receivedMessages.length - 1],
+ `Received the expected «${resource.message}» message, in the expected order`
+ );
+
+ // timeStamp are the result of a number in microsecond divided by 1000.
+ // so we can't expect a precise number of decimals, or even if there would
+ // be decimals at all.
+ ok(
+ resource.timeStamp.toString().match(/^\d+(\.\d{1,3})?$/),
+ `The resource has a timeStamp property ${resource.timeStamp}`
+ );
+
+ const isCachedMessage = receivedMessages.length <= cachedMessages.length;
+ is(
+ resource.isAlreadyExistingResource,
+ isCachedMessage,
+ "isAlreadyExistingResource has the expected value"
+ );
+
+ if (receivedMessages.length == expectedMessages.length) {
+ done();
+ }
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.PLATFORM_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+
+ info(
+ "Now log messages *after* the call to ResourceCommand.watchResources and after having received all existing messages"
+ );
+ Services.console.logStringMessage(expectedMessages[2]);
+ Services.console.logStringMessage(expectedMessages[3]);
+
+ info("Waiting for all expected messages to be received");
+ await onAllMessagesReceived;
+ ok(true, "All the expected messages were received");
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testPlatformMessagesResourcesWithIgnoreExistingResources() {
+ const { client, resourceCommand, targetCommand } =
+ await initMultiProcessResourceCommand();
+
+ info(
+ "Check whether onAvailable will not be called with existing platform messages"
+ );
+ const expectedMessages = ["This is 1st message", "This is 2nd message"];
+ Services.console.logStringMessage(expectedMessages[0]);
+ Services.console.logStringMessage(expectedMessages[1]);
+
+ const availableResources = [];
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.PLATFORM_MESSAGE],
+ {
+ onAvailable: resources => {
+ for (const resource of resources) {
+ if (!expectedMessages.includes(resource.message)) {
+ continue;
+ }
+
+ availableResources.push(resource);
+ }
+ },
+ ignoreExistingResources: true,
+ }
+ );
+ is(
+ availableResources.length,
+ 0,
+ "onAvailable wasn't called for existing platform messages"
+ );
+
+ info(
+ "Check whether onAvailable will be called with the future platform messages"
+ );
+ Services.console.logStringMessage(expectedMessages[0]);
+ Services.console.logStringMessage(expectedMessages[1]);
+
+ await waitUntil(() => availableResources.length === expectedMessages.length);
+ for (let i = 0; i < expectedMessages.length; i++) {
+ const resource = availableResources[i];
+ const { message } = resource;
+ const expected = expectedMessages[i];
+ is(message, expected, `Message[${i}] is correct`);
+ is(
+ resource.isAlreadyExistingResource,
+ false,
+ "isAlreadyExistingResource is false since we ignore existing resources"
+ );
+ }
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_reflows.js b/devtools/shared/commands/resource/tests/browser_resources_reflows.js
new file mode 100644
index 0000000000..70242c826a
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_reflows.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API for reflows
+
+const {
+ TYPES,
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+add_task(async function () {
+ const tab = await addTab(
+ "https://example.com/document-builder.sjs?html=<h1>Test reflow resources</h1>"
+ );
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const resources = [];
+ const onAvailable = _resources => {
+ resources.push(..._resources);
+ };
+ await resourceCommand.watchResources([TYPES.REFLOW], {
+ onAvailable,
+ });
+
+ is(resources.length, 0, "No reflow resource were sent initially");
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const el = content.document.createElement("div");
+ el.textContent = "1";
+ content.document.body.appendChild(el);
+ });
+
+ await waitFor(() => resources.length === 1);
+ checkReflowResource(resources[0]);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const el = content.document.querySelector("div");
+ el.style.display = "inline-grid";
+ });
+
+ await waitFor(() => resources.length === 2);
+ ok(
+ true,
+ "A reflow resource is sent when the display property of an element is modified"
+ );
+ checkReflowResource(resources.at(-1));
+
+ info("Check that adding an iframe does emit a reflow");
+ const iframeBC = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ const el = content.document.createElement("iframe");
+ const onIframeLoaded = new Promise(resolve =>
+ el.addEventListener("load", resolve, { once: true })
+ );
+ content.document.body.appendChild(el);
+ el.src =
+ "https://example.org/document-builder.sjs?html=<h2>remote iframe</h2>";
+ await onIframeLoaded;
+ return el.browsingContext;
+ }
+ );
+
+ await waitFor(() => resources.length === 3);
+ ok(true, "A reflow resource was received when adding a remote iframe");
+ checkReflowResource(resources.at(-1));
+
+ info("Check that we receive reflow resources for the remote iframe");
+ await SpecialPowers.spawn(iframeBC, [], () => {
+ const el = content.document.createElement("section");
+ el.textContent = "remote org iframe";
+ el.style.display = "grid";
+ content.document.body.appendChild(el);
+ });
+
+ await waitFor(() => resources.length === 4);
+ if (isFissionEnabled()) {
+ ok(
+ resources.at(-1).targetFront.url.includes("example.org"),
+ "The reflow resource is linked to the remote target"
+ );
+ }
+ checkReflowResource(resources.at(-1));
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+function checkReflowResource(resource) {
+ is(
+ resource.resourceType,
+ TYPES.REFLOW,
+ "The resource has the expected resourceType"
+ );
+
+ ok(Array.isArray(resource.reflows), "the `reflows` property is an array");
+ for (const reflow of resource.reflows) {
+ is(
+ Number.isFinite(reflow.start),
+ true,
+ "reflow start property is a number"
+ );
+ is(Number.isFinite(reflow.end), true, "reflow end property is a number");
+ Assert.greaterOrEqual(
+ reflow.end,
+ reflow.start,
+ "end is greater than start"
+ );
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_root_node.js b/devtools/shared/commands/resource/tests/browser_resources_root_node.js
new file mode 100644
index 0000000000..67ef5efd90
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_root_node.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around ROOT_NODE
+
+/**
+ * The original test still asserts some scenarios using several watchRootNode
+ * call sites, which is not something we intend to support at the moment in the
+ * resource command.
+ *
+ * Otherwise this test checks the basic behavior of the resource when reloading
+ * an empty page.
+ */
+add_task(async function () {
+ // Open a test tab
+ const tab = await addTab("data:text/html,Root Node tests");
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const browser = gBrowser.selectedBrowser;
+
+ info("Call watchResources([ROOT_NODE], ...)");
+ let onAvailableCounter = 0;
+ const onAvailable = resources => (onAvailableCounter += resources.length);
+ await resourceCommand.watchResources([resourceCommand.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Wait until onAvailable has been called");
+ await waitUntil(() => onAvailableCounter === 1);
+ is(onAvailableCounter, 1, "onAvailable has been called 1 time");
+
+ info("Reload the selected browser");
+ browser.reload();
+
+ info(
+ "Wait until the watchResources([ROOT_NODE], ...) callback has been called"
+ );
+ await waitUntil(() => onAvailableCounter === 2);
+
+ is(onAvailableCounter, 2, "onAvailable has been called 2 times");
+
+ info("Call unwatchResources([ROOT_NODE], ...) for the onAvailable callback");
+ resourceCommand.unwatchResources([resourceCommand.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Reload the selected browser");
+ const reloaded = BrowserTestUtils.browserLoaded(browser);
+ browser.reload();
+ await reloaded;
+
+ is(
+ onAvailableCounter,
+ 2,
+ "onAvailable was not called after calling unwatchResources"
+ );
+
+ // Cleanup
+ targetCommand.destroy();
+ await client.close();
+});
+
+/**
+ * Test that the watchRootNode API provides the expected node fronts.
+ */
+add_task(async function testRootNodeFrontIsCorrect() {
+ const tab = await addTab("data:text/html,<div id=div1>");
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+ const browser = gBrowser.selectedBrowser;
+
+ info("Call watchResources([ROOT_NODE], ...)");
+
+ let rootNodeResolve;
+ let rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ const onAvailable = ([rootNodeFront]) => rootNodeResolve(rootNodeFront);
+ await resourceCommand.watchResources([resourceCommand.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Wait until onAvailable has been called");
+ const root1 = await rootNodePromise;
+ ok(!!root1, "onAvailable has been called with a valid argument");
+ is(
+ root1.resourceType,
+ resourceCommand.TYPES.ROOT_NODE,
+ "The resource has the expected type"
+ );
+
+ info("Check we can query an expected node under the retrieved root");
+ const div1 = await root1.walkerFront.querySelector(root1, "div");
+ is(div1.getAttribute("id"), "div1", "Correct root node retrieved");
+
+ info("Reload the selected browser");
+ rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ browser.reload();
+
+ const root2 = await rootNodePromise;
+ Assert.notStrictEqual(
+ root1,
+ root2,
+ "onAvailable has been called with a different node front after reload"
+ );
+
+ info("Navigate to another URL");
+ rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `data:text/html,<div id=div3>`
+ );
+ const root3 = await rootNodePromise;
+ info("Check we can query an expected node under the retrieved root");
+ const div3 = await root3.walkerFront.querySelector(root3, "div");
+ is(div3.getAttribute("id"), "div3", "Correct root node retrieved");
+
+ // Cleanup
+ resourceCommand.unwatchResources([resourceCommand.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_scope_flag.js b/devtools/shared/commands/resource/tests/browser_resources_scope_flag.js
new file mode 100644
index 0000000000..8537daf161
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_scope_flag.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the ResourceCommand clears its pending events for resources emitted from
+// target destroyed when devtools.browsertoolbox.scope is updated.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Do not run this test when both fission and EFT is disabled as it changes
+ // the number of targets
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ ok(true, "Don't go further is both Fission and EFT are disabled");
+ return;
+ }
+
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ // Start with multiprocess debugging enabled
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+ const { TYPES } = targetCommand;
+
+ const targets = new Set();
+ const onAvailable = async ({ targetFront }) => {
+ targets.add(targetFront);
+ };
+ const onDestroyed = () => {};
+ await targetCommand.watchTargets({
+ types: [TYPES.PROCESS, TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ info("Open a tab in a new content process");
+ const firstTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ const newTabInnerWindowId =
+ firstTab.linkedBrowser.browsingContext.currentWindowGlobal.innerWindowId;
+
+ info("Wait for the tab window global target");
+ const windowGlobalTarget = await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == newTabInnerWindowId
+ )
+ );
+
+ let gotTabResource = false;
+ const onResourceAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.targetFront == windowGlobalTarget) {
+ gotTabResource = true;
+
+ if (resource.targetFront.isDestroyed()) {
+ ok(
+ false,
+ "we shouldn't get resources for the target that was destroyed when switching mode"
+ );
+ }
+ }
+ }
+ };
+
+ info("Start listening for resources");
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: onResourceAvailable,
+ ignoreExistingResources: true,
+ }
+ );
+
+ // Emit logs every ms to fill up the resourceCommand resource queue (pendingEvents)
+ const intervalId = await SpecialPowers.spawn(
+ firstTab.linkedBrowser,
+ [],
+ () => {
+ let counter = 0;
+ return content.wrappedJSObject.setInterval(() => {
+ counter++;
+ content.wrappedJSObject.console.log("STREAM_" + counter);
+ }, 1);
+ }
+ );
+
+ info("Wait until we get the first resource");
+ await waitFor(() => gotTabResource);
+
+ info("Disable multiprocess debugging");
+ await pushPref("devtools.browsertoolbox.scope", "parent-process");
+
+ info("Wait for the tab target to be destroyed");
+ await waitFor(() => windowGlobalTarget.isDestroyed());
+
+ info("Wait for a bit so any throttled action would have the time to occur");
+ await wait(1000);
+
+ // Stop listening for resources
+ await commands.resourceCommand.unwatchResources(
+ [commands.resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: onResourceAvailable,
+ }
+ );
+ // And stop the interval
+ await SpecialPowers.spawn(firstTab.linkedBrowser, [intervalId], id => {
+ content.wrappedJSObject.clearInterval(id);
+ });
+
+ targetCommand.destroy();
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_server_sent_events.js b/devtools/shared/commands/resource/tests/browser_resources_server_sent_events.js
new file mode 100644
index 0000000000..dab6c8d8cc
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_server_sent_events.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around SERVER SENT EVENTS.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const targets = {
+ TOP_LEVEL_DOCUMENT: "top-level-document",
+ IN_PROCESS_IFRAME: "in-process-frame",
+ OUT_PROCESS_IFRAME: "out-process-frame",
+};
+
+add_task(async function () {
+ info("Testing the top-level document");
+ await testServerSentEventResources(targets.TOP_LEVEL_DOCUMENT);
+ info("Testing the in-process iframe");
+ await testServerSentEventResources(targets.IN_PROCESS_IFRAME);
+ info("Testing the out-of-process iframe");
+ await testServerSentEventResources(targets.OUT_PROCESS_IFRAME);
+});
+
+async function testServerSentEventResources(target) {
+ const tab = await addTab(URL_ROOT_SSL + "sse_frontend.html");
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const availableResources = [];
+
+ function onResourceAvailable(resources) {
+ availableResources.push(...resources);
+ }
+
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.SERVER_SENT_EVENT],
+ { onAvailable: onResourceAvailable }
+ );
+
+ openConnectionInContext(tab, target);
+
+ info("Check available resources");
+ // We expect only 2 resources
+ await waitUntil(() => availableResources.length === 2);
+
+ info("Check resource details");
+ // To make sure the channel id are the same
+ const httpChannelId = availableResources[0].httpChannelId;
+
+ ok(httpChannelId, "The channel id is set");
+ is(typeof httpChannelId, "number", "The channel id is a number");
+
+ assertResource(availableResources[0], {
+ messageType: "eventReceived",
+ httpChannelId,
+ data: {
+ payload: "Why so serious?",
+ eventName: "message",
+ lastEventId: "",
+ retry: 5000,
+ },
+ });
+
+ assertResource(availableResources[1], {
+ messageType: "eventSourceConnectionClosed",
+ httpChannelId,
+ });
+
+ await resourceCommand.unwatchResources(
+ [resourceCommand.TYPES.SERVER_SENT_EVENT],
+ { onAvailable: onResourceAvailable }
+ );
+
+ await targetCommand.destroy();
+ await client.close();
+ BrowserTestUtils.removeTab(tab);
+}
+
+function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.SERVER_SENT_EVENT,
+ "Resource type is correct"
+ );
+
+ checkObject(resource, expected);
+}
+
+async function openConnectionInContext(tab, target) {
+ let browsingContext = tab.linkedBrowser.browsingContext;
+ if (target !== targets.TOP_LEVEL_DOCUMENT) {
+ browsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [target],
+ async _target => {
+ const iframe = content.document.getElementById(_target);
+ return iframe.browsingContext;
+ }
+ );
+ }
+ await SpecialPowers.spawn(browsingContext, [], async () => {
+ await content.wrappedJSObject.openConnection();
+ });
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_several_resources.js b/devtools/shared/commands/resource/tests/browser_resources_several_resources.js
new file mode 100644
index 0000000000..c1a151e562
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_several_resources.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the resource command is still properly watching for new targets
+ * after unwatching one resource, if there is still another watched resource.
+ */
+add_task(async function () {
+ // We will create a main process target list here in order to monitor
+ // resources from new tabs as they get created.
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // Open a test tab
+ const tab = await addTab("data:text/html,Root Node tests");
+
+ const { client, resourceCommand, targetCommand } =
+ await initMultiProcessResourceCommand();
+
+ const { CONSOLE_MESSAGE, ROOT_NODE } = resourceCommand.TYPES;
+
+ // We are only interested in console messages as a resource, the ROOT_NODE one
+ // is here to test the ResourceCommand::unwatchResources API with several resources.
+ const receivedMessages = [];
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType === CONSOLE_MESSAGE) {
+ receivedMessages.push(resource);
+ }
+ }
+ };
+
+ info("Call watchResources([CONSOLE_MESSAGE, ROOT_NODE], ...)");
+ await resourceCommand.watchResources([CONSOLE_MESSAGE, ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Use console.log in the content page");
+ logInTab(tab, "test from data-url");
+ info(
+ "Wait until onAvailable received the CONSOLE_MESSAGE resource emitted from the data-url tab"
+ );
+ await waitUntil(() =>
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test from data-url"
+ )
+ );
+
+ // Check that the resource command captures resources from new targets.
+ info("Open a first tab on the example.com domain");
+ const comTab = await addTab(
+ "https://example.com/document-builder.sjs?html=com"
+ );
+ info("Use console.log in the example.com page");
+ logInTab(comTab, "test-from-example-com");
+ info(
+ "Wait until onAvailable received the CONSOLE_MESSAGE resource emitted from the example.com tab"
+ );
+ await waitUntil(() =>
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test-from-example-com"
+ )
+ );
+
+ info("Stop watching ROOT_NODE resources");
+ await resourceCommand.unwatchResources([ROOT_NODE], { onAvailable });
+
+ // Check that messages from new targets are still captured after calling
+ // unwatch for another resource.
+ info("Open a second tab on the example.org domain");
+ const orgTab = await addTab(
+ "https://example.org/document-builder.sjs?html=org"
+ );
+ info("Use console.log in the example.org page");
+ logInTab(orgTab, "test-from-example-org");
+ info(
+ "Wait until onAvailable received the CONSOLE_MESSAGE resource emitted from the example.org tab"
+ );
+ await waitUntil(() =>
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test-from-example-org"
+ )
+ );
+
+ info("Stop watching CONSOLE_MESSAGE resources");
+ await resourceCommand.unwatchResources([CONSOLE_MESSAGE], { onAvailable });
+ await logInTab(tab, "test-again");
+
+ // We don't have a specific event to wait for here, so allow some time for
+ // the message to be received.
+ await wait(1000);
+
+ is(
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test-again"
+ ),
+ undefined,
+ "The resource command should not watch CONSOLE_MESSAGE anymore"
+ );
+
+ // Cleanup
+ targetCommand.destroy();
+ await client.close();
+});
+
+function logInTab(tab, message) {
+ return ContentTask.spawn(tab.linkedBrowser, message, function (_message) {
+ content.console.log(_message);
+ });
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_sources.js b/devtools/shared/commands/resource/tests/browser_resources_sources.js
new file mode 100644
index 0000000000..767f45283a
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_sources.js
@@ -0,0 +1,456 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around SOURCE.
+//
+// We cover each Spidermonkey Debugger Source's `introductionType`:
+// https://searchfox.org/mozilla-central/rev/4c184ca81b28f1ccffbfd08f465709b95bcb4aa1/js/src/doc/Debugger/Debugger.Source.md#172-213
+//
+// And especially cover sources being GC-ed before DevTools are opened
+// which are later recreated by `ThreadActor.resurrectSource`.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const TEST_URL = URL_ROOT_SSL + "sources.html";
+
+const TEST_JS_URL = URL_ROOT_SSL + "sources.js";
+const TEST_WORKER_URL = URL_ROOT_SSL + "worker-sources.js";
+const TEST_SW_URL = URL_ROOT_SSL + "service-worker-sources.js";
+
+async function getExpectedResources(ignoreUnresurrectedSources = false) {
+ const htmlRequest = await fetch(TEST_URL);
+ const htmlContent = await htmlRequest.text();
+
+ // First list sources that aren't GC-ed, or that the thread actor is able to resurrect
+ const expectedSources = [
+ {
+ description: "eval",
+ sourceForm: {
+ introductionType: "eval",
+ sourceMapBaseURL: TEST_URL,
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "this.global = function evalFunction() {}",
+ },
+ },
+ {
+ description: "new Function()",
+ sourceForm: {
+ introductionType: "Function",
+ sourceMapBaseURL: TEST_URL,
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "function anonymous(\n) {\nreturn 42;\n}",
+ },
+ },
+ {
+ description: "Event Handler",
+ sourceForm: {
+ introductionType: "eventHandler",
+ sourceMapBaseURL: TEST_URL,
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "console.log('link')",
+ },
+ },
+ {
+ description: "inline JS inserted at runtime",
+ sourceForm: {
+ introductionType: "scriptElement", // This is an injectedScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form()
+ sourceMapBaseURL: TEST_URL,
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "console.log('inline-script')",
+ },
+ },
+ {
+ description: "inline JS",
+ sourceForm: {
+ introductionType: "scriptElement", // This is an inlineScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form()
+ sourceMapBaseURL: TEST_URL,
+ url: TEST_URL,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: true,
+ },
+ sourceContent: {
+ contentType: "text/html",
+ source: htmlContent,
+ },
+ },
+ {
+ description: "worker script",
+ sourceForm: {
+ introductionType: undefined,
+ sourceMapBaseURL: TEST_WORKER_URL,
+ url: TEST_WORKER_URL,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "/* eslint-disable */\nfunction workerSource() {}\n",
+ },
+ },
+ {
+ description: "service worker script",
+ sourceForm: {
+ introductionType: undefined,
+ sourceMapBaseURL: TEST_SW_URL,
+ url: TEST_SW_URL,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "/* eslint-disable */\nfunction serviceWorkerSource() {}\n",
+ },
+ },
+ {
+ description: "independent js file",
+ sourceForm: {
+ introductionType: "scriptElement", // This is an srcScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form()
+ sourceMapBaseURL: TEST_JS_URL,
+ url: TEST_JS_URL,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "/* eslint-disable */\nfunction scriptSource() {}\n",
+ },
+ },
+ ];
+
+ // Now list the sources that could be GC-ed for which the thread actor isn't able to resurrect.
+ // This is the sources that we can't assert when we fetch sources after the page is already loaded.
+ const unresurrectedSources = [
+ {
+ description: "DOM Timer",
+ sourceForm: {
+ introductionType: "domTimer",
+ sourceMapBaseURL: TEST_URL,
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ /* the domTimer is prefixed by many empty lines in order to be positioned at the same line
+ as in the HTML file where setTimeout is called.
+ This is probably done by SourceActor.actualText().
+ So the array size here, should be updated to match the line number of setTimeout call */
+ source: new Array(39).join("\n") + `console.log("timeout")`,
+ },
+ },
+ {
+ description: "javascript URL",
+ sourceForm: {
+ introductionType: "javascriptURL",
+ sourceMapBaseURL: isEveryFrameTargetEnabled()
+ ? "about:blank"
+ : TEST_URL,
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "666",
+ },
+ },
+ {
+ description: "srcdoc attribute on iframes #1",
+ sourceForm: {
+ introductionType: "scriptElement",
+ // We do not assert url/sourceMapBaseURL as it includes the Debugger.Source.id
+ // which is random
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "console.log('srcdoc')",
+ },
+ },
+ {
+ description: "srcdoc attribute on iframes #2",
+ sourceForm: {
+ introductionType: "scriptElement",
+ // We do not assert url/sourceMapBaseURL as it includes the Debugger.Source.id
+ // which is random
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "console.log('srcdoc 2')",
+ },
+ },
+ ];
+
+ if (ignoreUnresurrectedSources) {
+ return expectedSources;
+ }
+ return expectedSources.concat(unresurrectedSources);
+}
+
+add_task(async function testSourcesOnload() {
+ // Load an blank document first, in order to load the test page only once we already
+ // started watching for sources
+ const tab = await addTab("about:blank");
+
+ const commands = await CommandsFactory.forTab(tab);
+ const { targetCommand, resourceCommand } = commands;
+
+ // Force the target list to cover workers and debug all the targets
+ targetCommand.listenForWorkers = true;
+ targetCommand.listenForServiceWorkers = true;
+ await targetCommand.startListening();
+
+ info("Check already available resources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ const promiseLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ TEST_URL
+ );
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, TEST_URL);
+ await promiseLoad;
+
+ // Some sources may be created after the document is done loading (like eventHandler usecase)
+ // so we may be received *after* watchResource resolved
+ const expectedResources = await getExpectedResources();
+ await waitFor(
+ () => availableResources.length >= expectedResources.length,
+ "Got all the sources"
+ );
+
+ await assertResources(availableResources, expectedResources);
+
+ await commands.destroy();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+add_task(async function testGarbagedCollectedSources() {
+ info(
+ "Assert SOURCES on an already loaded page with some sources that have been GC-ed"
+ );
+ const tab = await addTab(TEST_URL);
+
+ info("Force some GC to free some sources");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ Cu.forceGC();
+ Cu.forceCC();
+ });
+
+ const commands = await CommandsFactory.forTab(tab);
+ const { targetCommand, resourceCommand } = commands;
+
+ // Force the target list to cover workers and debug all the targets
+ targetCommand.listenForWorkers = true;
+ targetCommand.listenForServiceWorkers = true;
+ await targetCommand.startListening();
+
+ info("Check already available resources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ // Some sources may be created after the document is done loading (like eventHandler usecase)
+ // so we may be received *after* watchResource resolved
+ const expectedResources = await getExpectedResources(true);
+ await waitFor(
+ () => availableResources.length >= expectedResources.length,
+ "Got all the sources"
+ );
+
+ await assertResources(availableResources, expectedResources);
+
+ await commands.destroy();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+/**
+ * Assert that evaluating sources for a new global, in the parent process
+ * using the shared system principal will spawn SOURCE resources.
+ *
+ * For this we use a special `commands` which replicate what browser console
+ * and toolbox use.
+ */
+add_task(async function testParentProcessPrivilegedSources() {
+ // Use a custom loader + server + client in order to spawn the server
+ // in a distinct system compartment, so that it can see the system compartment
+ // sandbox we are about to create in this test
+ const client = await CommandsFactory.spawnClientToDebugSystemPrincipal();
+
+ const commands = await CommandsFactory.forMainProcess({ client });
+ await commands.targetCommand.startListening();
+ const { resourceCommand } = commands;
+
+ info("Check already available resources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+ ok(
+ !!availableResources.length,
+ "We get many sources reported from a multiprocess command"
+ );
+
+ // Clear the list of sources
+ availableResources.length = 0;
+
+ // Force the creation of a new privileged source
+ const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+ const sandbox = Cu.Sandbox(systemPrincipal);
+ Cu.evalInSandbox("function foo() {}", sandbox, null, "http://foo.com");
+
+ info("Wait for the sandbox source");
+ await waitFor(() => {
+ return availableResources.some(
+ resource => resource.url == "http://foo.com/"
+ );
+ });
+
+ const expectedResources = [
+ {
+ description: "privileged sandbox script",
+ sourceForm: {
+ introductionType: undefined,
+ sourceMapBaseURL: "http://foo.com/",
+ url: "http://foo.com/",
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ isInlineSource: false,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "function foo() {}",
+ },
+ },
+ ];
+ const matchingResource = availableResources.filter(resource =>
+ resource.url.includes("http://foo.com")
+ );
+ await assertResources(matchingResource, expectedResources);
+
+ await commands.destroy();
+});
+
+async function assertResources(resources, expected) {
+ is(
+ resources.length,
+ expected.length,
+ "Length of existing resources is correct at initial"
+ );
+ for (let i = 0; i < resources.length; i++) {
+ await assertResource(resources[i], expected);
+ }
+}
+
+async function assertResource(source, expected) {
+ is(
+ source.resourceType,
+ ResourceCommand.TYPES.SOURCE,
+ "Resource type is correct"
+ );
+
+ const threadFront = await source.targetFront.getFront("thread");
+ // `source` is SourceActor's form()
+ // so try to instantiate the related SourceFront:
+ const sourceFront = threadFront.source(source);
+ // then fetch source content
+ const sourceContent = await sourceFront.source();
+
+ // Order of sources is random, so we have to find the best expected resource.
+ // The only unique attribute is the JS Source text content.
+ const matchingExpected = expected.find(res => {
+ return res.sourceContent.source == sourceContent.source;
+ });
+ ok(
+ matchingExpected,
+ `This source was expected with source content being "${sourceContent.source}"`
+ );
+ info(`Found "#${matchingExpected.description}"`);
+ assertObject(
+ sourceContent,
+ matchingExpected.sourceContent,
+ matchingExpected.description
+ );
+
+ assertObject(
+ source,
+ matchingExpected.sourceForm,
+ matchingExpected.description
+ );
+}
+
+function assertObject(object, expected, description) {
+ for (const field in expected) {
+ is(
+ object[field],
+ expected[field],
+ `The value of ${field} is correct for "#${description}"`
+ );
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js b/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js
new file mode 100644
index 0000000000..ec81e8118d
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_stylesheets.js
@@ -0,0 +1,713 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around STYLESHEET.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const STYLE_TEST_URL = URL_ROOT_SSL + "style_document.html";
+
+const EXISTING_RESOURCES = [
+ {
+ styleText: "body { color: lime; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "body { margin: 1px; }",
+ href: "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.css",
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "",
+ href: null,
+ nodeHref: null,
+ isNew: false,
+ disabled: false,
+ constructed: true,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "body { background-color: pink; }",
+ href: null,
+ nodeHref:
+ "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+ {
+ styleText: "body { padding: 1px; }",
+ href: "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.css",
+ nodeHref:
+ "https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+ },
+];
+
+const ADDITIONAL_INLINE_RESOURCE = {
+ styleText:
+ "@media all { body { color: red; } } @media print { body { color: cyan; } } body { font-size: 10px; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ constructed: false,
+ ruleCount: 5,
+ atRules: [
+ {
+ type: "media",
+ conditionText: "all",
+ matches: true,
+ line: 1,
+ column: 1,
+ },
+ {
+ type: "media",
+ conditionText: "print",
+ matches: false,
+ line: 1,
+ column: 37,
+ },
+ ],
+};
+
+const ADDITIONAL_CONSTRUCTED_RESOURCE = {
+ styleText: "",
+ href: null,
+ nodeHref: null,
+ isNew: false,
+ disabled: false,
+ constructed: true,
+ ruleCount: 2,
+ atRules: [],
+};
+
+const ADDITIONAL_FROM_ACTOR_RESOURCE = {
+ styleText: "body { font-size: 10px; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/commands/resource/tests/style_document.html",
+ isNew: true,
+ disabled: false,
+ constructed: false,
+ ruleCount: 1,
+ atRules: [],
+};
+
+add_task(async function () {
+ await testResourceAvailableDestroyedFeature();
+ await testResourceUpdateFeature();
+ await testNestedResourceUpdateFeature();
+});
+
+function pushAvailableResource(availableResources) {
+ // TODO(bug 1826538): Find a better way of dealing with these.
+ return function (resources) {
+ for (const resource of resources) {
+ if (resource.href?.startsWith("resource://")) {
+ continue;
+ }
+ availableResources.push(resource);
+ }
+ };
+}
+
+async function testResourceAvailableDestroyedFeature() {
+ info("Check resource available feature of the ResourceCommand");
+
+ const tab = await addTab(STYLE_TEST_URL);
+ let resourceTimingEntryCounts = await getResourceTimingCount(tab);
+ is(
+ resourceTimingEntryCounts,
+ 2,
+ "Should have two entires for resource timing"
+ );
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check whether ResourceCommand gets existing stylesheet");
+ const availableResources = [];
+ const destroyedResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: pushAvailableResource(availableResources),
+ onDestroyed: resources => destroyedResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+ for (let i = 0; i < availableResources.length; i++) {
+ const availableResource = availableResources[i];
+ // We can not expect the resources to always be forwarded in the same order.
+ // See intermittent Bug 1655016.
+ const expectedResource = findMatchingExpectedResource(availableResource);
+ ok(expectedResource, "Found a matching expected resource for the resource");
+ await assertResource(availableResource, expectedResource);
+ }
+
+ resourceTimingEntryCounts = await getResourceTimingCount(tab);
+ is(
+ resourceTimingEntryCounts,
+ 2,
+ "Should still have two entires for resource timing after devtools APIs have been triggered"
+ );
+
+ info("Check whether ResourceCommand gets additonal stylesheet");
+ await ContentTask.spawn(
+ tab.linkedBrowser,
+ ADDITIONAL_INLINE_RESOURCE.styleText,
+ text => {
+ const document = content.document;
+ const stylesheet = document.createElement("style");
+ stylesheet.id = "inline-from-test";
+ stylesheet.textContent = text;
+ document.body.appendChild(stylesheet);
+ }
+ );
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 1
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_INLINE_RESOURCE
+ );
+
+ info("Check whether ResourceCommand gets additonal constructed stylesheet");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ const document = content.document;
+ const s = new content.CSSStyleSheet();
+ // We use the different number of rules to meaningfully differentiate
+ // between constructed stylesheets.
+ s.replaceSync("foo { color: red } bar { color: blue }");
+ // TODO(bug 1751346): wrappedJSObject should be unnecessary.
+ document.wrappedJSObject.adoptedStyleSheets.push(s);
+ });
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 2
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_CONSTRUCTED_RESOURCE
+ );
+
+ info(
+ "Check whether ResourceCommand gets additonal stylesheet which is added by DevTools"
+ );
+ const styleSheetsFront = await targetCommand.targetFront.getFront(
+ "stylesheets"
+ );
+ await styleSheetsFront.addStyleSheet(
+ ADDITIONAL_FROM_ACTOR_RESOURCE.styleText
+ );
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 3
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_FROM_ACTOR_RESOURCE
+ );
+
+ info("Check resource destroyed feature of the ResourceCommand");
+ is(destroyedResources.length, 0, "There was no removed stylesheets yet");
+
+ info("Remove inline stylesheet added in the test");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.document.querySelector("#inline-from-test").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 1);
+ assertDestroyed(destroyedResources[0], {
+ resourceId: availableResources.at(-3).resourceId,
+ });
+
+ info("Remove existing top-level inline stylesheet");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.document.querySelector("style").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 2);
+ assertDestroyed(destroyedResources[1], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[0]
+ ).resourceId,
+ });
+
+ info("Remove existing top-level <link> stylesheet");
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.document.querySelector("link").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 3);
+ assertDestroyed(destroyedResources[2], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[1]
+ ).resourceId,
+ });
+
+ info("Remove existing iframe inline stylesheet");
+ const iframeBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+
+ await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
+ content.document.querySelector("style").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 4);
+ assertDestroyed(destroyedResources[3], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[3]
+ ).resourceId,
+ });
+
+ info("Remove existing iframe <link> stylesheet");
+ await SpecialPowers.spawn(iframeBrowsingContext, [], () => {
+ content.document.querySelector("link").remove();
+ });
+ await waitUntil(() => destroyedResources.length === 5);
+ assertDestroyed(destroyedResources[4], {
+ resourceId: availableResources.find(
+ resource =>
+ findMatchingExpectedResource(resource) === EXISTING_RESOURCES[4]
+ ).resourceId,
+ });
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testResourceUpdateFeature() {
+ info("Check resource update feature of the ResourceCommand");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Setup the watcher");
+ const availableResources = [];
+ const updates = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: pushAvailableResource(availableResources),
+ onUpdated: newUpdates => updates.push(...newUpdates),
+ });
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+ is(updates.length, 0, "there's no update yet");
+
+ info("Check toggleDisabled function");
+ // Retrieve the stylesheet of the top-level target
+ const resource = availableResources.find(
+ innerResource => innerResource.targetFront.isTopLevel
+ );
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ await styleSheetsFront.toggleDisabled(resource.resourceId);
+ await waitUntil(() => updates.length === 1);
+
+ // Check the content of the update object.
+ assertUpdate(updates[0].update, {
+ resourceId: resource.resourceId,
+ updateType: "property-change",
+ });
+ is(
+ updates[0].update.resourceUpdates.disabled,
+ true,
+ "resourceUpdates is correct"
+ );
+
+ // Check whether the cached resource is updated correctly.
+ is(
+ updates[0].resource.disabled,
+ true,
+ "cached resource is updated correctly"
+ );
+
+ // Check whether the actual stylesheet is updated correctly.
+ const styleSheetDisabled = await ContentTask.spawn(
+ tab.linkedBrowser,
+ null,
+ () => {
+ const document = content.document;
+ const stylesheet = document.styleSheets[0];
+ return stylesheet.disabled;
+ }
+ );
+ is(styleSheetDisabled, true, "actual stylesheet was updated correctly");
+
+ info("Check update function");
+ const expectedAtRules = [
+ {
+ type: "media",
+ conditionText: "screen",
+ matches: true,
+ },
+ {
+ type: "media",
+ conditionText: "print",
+ matches: false,
+ },
+ ];
+
+ const updateCause = "updated-by-test";
+ await styleSheetsFront.update(
+ resource.resourceId,
+ "@media screen { color: red; } @media print { color: green; } body { color: cyan; }",
+ false,
+ updateCause
+ );
+ await waitUntil(() => updates.length === 4);
+
+ assertUpdate(updates[1].update, {
+ resourceId: resource.resourceId,
+ updateType: "property-change",
+ });
+ is(
+ updates[1].update.resourceUpdates.ruleCount,
+ 3,
+ "resourceUpdates is correct"
+ );
+ is(updates[1].resource.ruleCount, 3, "cached resource is updated correctly");
+
+ assertUpdate(updates[2].update, {
+ resourceId: resource.resourceId,
+ updateType: "style-applied",
+ event: {
+ cause: updateCause,
+ },
+ });
+ is(
+ updates[2].update.resourceUpdates,
+ undefined,
+ "resourceUpdates is correct"
+ );
+
+ assertUpdate(updates[3].update, {
+ resourceId: resource.resourceId,
+ updateType: "at-rules-changed",
+ });
+ assertAtRules(updates[3].update.resourceUpdates.atRules, expectedAtRules);
+
+ // Check the actual page.
+ const styleSheetResult = await getStyleSheetResult(tab);
+
+ is(
+ styleSheetResult.ruleCount,
+ 3,
+ "ruleCount of actual stylesheet is updated correctly"
+ );
+ assertAtRules(styleSheetResult.atRules, expectedAtRules);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function testNestedResourceUpdateFeature() {
+ info("Check nested resource update feature of the ResourceCommand");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const { outerWidth: originalWindowWidth, outerHeight: originalWindowHeight } =
+ tab.ownerGlobal;
+
+ registerCleanupFunction(() => {
+ tab.ownerGlobal.resizeTo(originalWindowWidth, originalWindowHeight);
+ });
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Setup the watcher");
+ const availableResources = [];
+ const updates = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: pushAvailableResource(availableResources),
+ onUpdated: newUpdates => updates.push(...newUpdates),
+ });
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+
+ info("Apply new media query");
+ // In order to avoid applying the media query (min-height: 400px).
+ if (originalWindowHeight !== 300) {
+ await new Promise(resolve => {
+ tab.ownerGlobal.addEventListener("resize", resolve, { once: true });
+ tab.ownerGlobal.resizeTo(originalWindowWidth, 300);
+ });
+ }
+
+ // Retrieve the stylesheet of the top-level target
+ const resource = availableResources.find(
+ innerResource => innerResource.targetFront.isTopLevel
+ );
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ await styleSheetsFront.update(
+ resource.resourceId,
+ `@media (min-height: 400px) {
+ html {
+ color: red;
+ }
+ @layer myLayer {
+ @supports (container-type) {
+ :root {
+ color: gold;
+ container: root inline-size;
+ }
+
+ @container root (width > 10px) {
+ body {
+ color: gold;
+ }
+ }
+ }
+ }
+ }`,
+ false
+ );
+ await waitUntil(() => updates.length === 3);
+ is(
+ updates.at(-1).resource.ruleCount,
+ 7,
+ "Resource in update has expected ruleCount"
+ );
+
+ is(resource.atRules[0].matches, false, "Media query is not matched yet");
+
+ info("Change window size to fire matches-change event");
+ tab.ownerGlobal.resizeTo(originalWindowWidth, 500);
+ await waitUntil(() => updates.length === 4);
+
+ // Check the update content.
+ const targetUpdate = updates[3];
+ assertUpdate(targetUpdate.update, {
+ resourceId: resource.resourceId,
+ updateType: "matches-change",
+ });
+ Assert.strictEqual(
+ resource,
+ targetUpdate.resource,
+ "Update object has the same resource"
+ );
+
+ is(
+ JSON.stringify(targetUpdate.update.nestedResourceUpdates[0].path),
+ JSON.stringify(["atRules", 0, "matches"]),
+ "path of nestedResourceUpdates is correct"
+ );
+ is(
+ targetUpdate.update.nestedResourceUpdates[0].value,
+ true,
+ "value of nestedResourceUpdates is correct"
+ );
+
+ // Check the resource.
+ const expectedAtRules = [
+ {
+ type: "media",
+ conditionText: "(min-height: 400px)",
+ matches: true,
+ },
+ {
+ type: "layer",
+ layerName: "myLayer",
+ },
+ {
+ type: "support",
+ conditionText: "(container-type)",
+ },
+ {
+ type: "container",
+ conditionText: "root (width > 10px)",
+ },
+ ];
+
+ assertAtRules(targetUpdate.resource.atRules, expectedAtRules);
+
+ // Check the actual page.
+ const styleSheetResult = await getStyleSheetResult(tab);
+ is(
+ styleSheetResult.ruleCount,
+ 7,
+ "ruleCount of actual stylesheet is updated correctly"
+ );
+ assertAtRules(styleSheetResult.atRules, expectedAtRules);
+
+ tab.ownerGlobal.resizeTo(originalWindowWidth, originalWindowHeight);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+function findMatchingExpectedResource(resource) {
+ return EXISTING_RESOURCES.find(
+ expected =>
+ resource.href === expected.href &&
+ resource.nodeHref === expected.nodeHref &&
+ resource.ruleCount === expected.ruleCount &&
+ resource.constructed == expected.constructed
+ );
+}
+
+async function getStyleSheetResult(tab) {
+ const result = await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ const document = content.document;
+ const stylesheet = document.styleSheets[0];
+ let ruleCount = 0;
+ const atRules = [];
+
+ const traverseRules = ruleList => {
+ for (const rule of ruleList) {
+ ruleCount++;
+
+ if (rule.media) {
+ let matches = false;
+ try {
+ const mql = content.matchMedia(rule.media.mediaText);
+ matches = mql.matches;
+ } catch (e) {
+ // Ignored
+ }
+
+ atRules.push({
+ type: "media",
+ conditionText: rule.conditionText,
+ matches,
+ });
+ } else if (rule instanceof content.CSSContainerRule) {
+ atRules.push({
+ type: "container",
+ conditionText: rule.conditionText,
+ });
+ } else if (rule instanceof content.CSSLayerBlockRule) {
+ atRules.push({ type: "layer", layerName: rule.name });
+ } else if (rule instanceof content.CSSSupportsRule) {
+ atRules.push({
+ type: "support",
+ conditionText: rule.conditionText,
+ });
+ }
+
+ if (rule.cssRules) {
+ traverseRules(rule.cssRules);
+ }
+ }
+ };
+ traverseRules(stylesheet.cssRules);
+
+ return { ruleCount, atRules };
+ });
+
+ return result;
+}
+
+function assertAtRules(atRules, expectedAtRules) {
+ is(
+ atRules.length,
+ expectedAtRules.length,
+ "Length of the atRules is correct"
+ );
+
+ for (let i = 0; i < atRules.length; i++) {
+ const atRule = atRules[i];
+ const expected = expectedAtRules[i];
+ is(atRule.type, expected.type, "at-rule is of expected type");
+ is(
+ atRules[i].conditionText,
+ expected.conditionText,
+ "conditionText is correct"
+ );
+ if (expected.type === "media") {
+ is(atRule.matches, expected.matches, "matches is correct");
+ } else if (expected.type === "layer") {
+ is(atRule.layerName, expected.layerName, "layerName is correct");
+ }
+
+ if (expected.line !== undefined) {
+ is(atRule.line, expected.line, "line is correct");
+ }
+
+ if (expected.column !== undefined) {
+ is(atRule.column, expected.column, "column is correct");
+ }
+ }
+}
+
+async function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ const styleText = (await getStyleSheetResourceText(resource)).trim();
+ is(styleText, expected.styleText, "Style text is correct");
+ is(resource.href, expected.href, "href is correct");
+ is(resource.nodeHref, expected.nodeHref, "nodeHref is correct");
+ is(resource.isNew, expected.isNew, "isNew is correct");
+ is(resource.disabled, expected.disabled, "disabled is correct");
+ is(resource.constructed, expected.constructed, "constructed is correct");
+ is(resource.ruleCount, expected.ruleCount, "ruleCount is correct");
+ assertAtRules(resource.atRules, expected.atRules);
+}
+
+function assertUpdate(update, expected) {
+ is(
+ update.resourceType,
+ ResourceCommand.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ is(update.resourceId, expected.resourceId, "resourceId is correct");
+ is(update.updateType, expected.updateType, "updateType is correct");
+ if (expected.event?.cause) {
+ is(update.event?.cause, expected.event.cause, "cause is correct");
+ }
+}
+
+function assertDestroyed(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ is(resource.resourceId, expected.resourceId, "resourceId is correct");
+}
+
+function getResourceTimingCount(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, [], () => {
+ return content.performance.getEntriesByType("resource").length;
+ });
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_stylesheets_header.js b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_header.js
new file mode 100644
index 0000000000..29263d887b
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_header.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we do get the appropriate stylesheet content when the stylesheet is only
+// served based on the Accept: text/css header
+
+add_task(async function () {
+ const httpServer = createTestHTTPServer();
+
+ httpServer.registerContentType("html", "text/html");
+
+ httpServer.registerPathHandler("/index.html", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test stylesheet</title>
+<link href="/test/" rel="stylesheet" type="text/css"/>
+<script src="/test/"></script>
+<h1>Hello</h1>
+ `);
+ });
+
+ let resourceUrlCalls = 0;
+ // The /test/ URL should be called:
+ // - once by the content page to load the <link>
+ // - once by the content page to load the <script>
+ // - once by DevTools to fetch the stylesheet text
+ // (we could probably optimize this so we only call once)
+ const expectedResourceUrlCalls = 3;
+
+ const styleSheetText = `body { background-color: tomato; }`;
+ httpServer.registerPathHandler("/test/", function (request, response) {
+ resourceUrlCalls++;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ if (request.getHeader("Accept").startsWith("text/css")) {
+ response.setHeader("Content-Type", "text/css", false);
+ response.write(styleSheetText);
+ return;
+ }
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write(`/* NOT A STYLESHEET */`);
+ });
+ const port = httpServer.identity.primaryPort;
+ const TEST_URL = `http://localhost:${port}/index.html`;
+
+ info("Check resource available feature of the ResourceCommand");
+ const tab = await addTab(TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check whether ResourceCommand gets existing stylesheet");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+ is(
+ availableResources.length,
+ 1,
+ "We have the expected number of stylesheets"
+ );
+
+ is(
+ await getStyleSheetResourceText(availableResources[0]),
+ styleSheetText,
+ "Got expected text for the stylesheet"
+ );
+
+ is(
+ resourceUrlCalls,
+ expectedResourceUrlCalls,
+ "The /test URL was called the number of time we expected"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_stylesheets_import.js b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_import.js
new file mode 100644
index 0000000000..c58a5162e0
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_import.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API for imported STYLESHEET + iframe.
+
+const styleSheetText = `
+@import "${URL_ROOT_ORG_SSL}/style_document.css";
+body { background-color: tomato; }`;
+
+const IFRAME_URL = `https://example.org/document-builder.sjs?html=${encodeURIComponent(`
+ <style>${styleSheetText}</style>
+ <h1>iframe</h1>
+`)}`;
+
+const TEST_URL = `https://example.org/document-builder.sjs?html=
+ <h1>import stylesheet test</h1>
+ <iframe src="${encodeURIComponent(IFRAME_URL)}"></iframe>`;
+
+add_task(async function () {
+ info("Check resource available feature of the ResourceCommand");
+
+ const tab = await addTab(TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check whether ResourceCommand gets existing stylesheet");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ await waitFor(() => availableResources.length === 2);
+ ok(true, "We're getting the expected stylesheets");
+
+ const styleNodeStyleSheet = availableResources.find(
+ resource => resource.nodeHref
+ );
+ const importedStyleSheet = availableResources.find(
+ resource => resource !== styleNodeStyleSheet
+ );
+
+ is(
+ await getStyleSheetResourceText(styleNodeStyleSheet),
+ styleSheetText,
+ "Got expected text for the <style> stylesheet"
+ );
+
+ is(
+ (await getStyleSheetResourceText(importedStyleSheet)).trim(),
+ `body { margin: 1px; }`,
+ "Got expected text for the imported stylesheet"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_stylesheets_navigation.js b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_navigation.js
new file mode 100644
index 0000000000..1ee8913bda
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_navigation.js
@@ -0,0 +1,254 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around STYLESHEET and navigation (reloading, creation of new browsing context, …)
+
+const ORG_DOC_BUILDER = "https://example.org/document-builder.sjs";
+const COM_DOC_BUILDER = "https://example.com/document-builder.sjs";
+
+// Since the order of resources is not guaranteed, we put a number in the title attribute
+// of the <style> elements so we can sort them in a way that makes it easier for us to assert.
+let currentStyleTitle = 0;
+
+const TEST_URI =
+ `${ORG_DOC_BUILDER}?html=1<h1>top-level example.org</h1>` +
+ `<style title="${currentStyleTitle++}">.top-level-org{}</style>` +
+ `<iframe id="same-origin-1" src="${ORG_DOC_BUILDER}?html=<h2>example.org 1</h2><style title=${currentStyleTitle++}>.frame-org-1{}</style>"></iframe>` +
+ `<iframe id="same-origin-2" src="${ORG_DOC_BUILDER}?html=<h2>example.org 2</h2><style title=${currentStyleTitle++}>.frame-org-2{}</style>"></iframe>` +
+ `<iframe id="remote-origin-1" src="${COM_DOC_BUILDER}?html=<h2>example.com 1</h2><style title=${currentStyleTitle++}>.frame-com-1{}</style>"></iframe>` +
+ `<iframe id="remote-origin-2" src="${COM_DOC_BUILDER}?html=<h2>example.com 2</h2><style title=${currentStyleTitle++}>.frame-com-2{}</style>"></iframe>`;
+
+const COOP_HEADERS = "Cross-Origin-Opener-Policy:same-origin";
+const TEST_URI_NEW_BROWSING_CONTEXT =
+ `${ORG_DOC_BUILDER}?headers=${COOP_HEADERS}` +
+ `&html=<h1>top-level example.org</div>` +
+ `<style>.top-level-org-new-bc{}</style>`;
+
+add_task(async function () {
+ info(
+ "Open a new tab and check that styleSheetChangeEventsEnabled is false by default"
+ );
+ const tab = await addTab(TEST_URI);
+
+ is(
+ await getDocumentStyleSheetChangeEventsEnabled(tab.linkedBrowser),
+ false,
+ `styleSheetChangeEventsEnabled is false at the beginning`
+ );
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ let availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: resources => {
+ availableResources.push(...resources);
+ },
+ });
+
+ info("Wait for all the stylesheets resources of main document and iframes");
+ await waitFor(() => availableResources.length === 5);
+ is(availableResources.length, 5, "Retrieved the expected stylesheets");
+
+ // the order of the resources is not guaranteed.
+ sortResourcesByExpectedOrder(availableResources);
+ await assertResource(availableResources[0], {
+ styleText: `.top-level-org{}`,
+ });
+ await assertResource(availableResources[1], {
+ styleText: `.frame-org-1{}`,
+ });
+ await assertResource(availableResources[2], {
+ styleText: `.frame-org-2{}`,
+ });
+ await assertResource(availableResources[3], {
+ styleText: `.frame-com-1{}`,
+ });
+ await assertResource(availableResources[4], {
+ styleText: `.frame-com-2{}`,
+ });
+
+ // clear availableResources so it's easier to test
+ availableResources = [];
+
+ is(
+ await getDocumentStyleSheetChangeEventsEnabled(tab.linkedBrowser),
+ true,
+ `styleSheetChangeEventsEnabled is true after watching stylesheets`
+ );
+
+ info("Navigate a remote frame to a different page");
+ const iframeNewUrl =
+ `https://example.com/document-builder.sjs?` +
+ `html=<h2>example.com new bc</h2><style title=6>.frame-com-new-bc{}</style>`;
+ await SpecialPowers.spawn(tab.linkedBrowser, [iframeNewUrl], url => {
+ const { browsingContext } =
+ content.document.querySelector("#remote-origin-2");
+ return SpecialPowers.spawn(browsingContext, [url], innerUrl => {
+ content.document.location = innerUrl;
+ });
+ });
+ await waitFor(() => availableResources.length == 1);
+ ok(true, "We're notified about the iframe new document stylesheet");
+ await assertResource(availableResources[0], {
+ styleText: `.frame-com-new-bc{}`,
+ });
+ const iframeNewBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.querySelector("#remote-origin-2").browsingContext
+ );
+
+ is(
+ await getDocumentStyleSheetChangeEventsEnabled(iframeNewBrowsingContext),
+ true,
+ `styleSheetChangeEventsEnabled is still true after navigating the iframe`
+ );
+
+ // clear availableResources so it's easier to test
+ availableResources = [];
+
+ info("Check that styleSheetChangeEventsEnabled persist after reloading");
+ await reloadBrowser();
+
+ // ⚠️ When EFT is disabled, we're only getting the stylesheets for the top-level document
+ // and the remote frames; the same-origin iframes stylesheets are missing.
+ const expectedStylesheetResources = isEveryFrameTargetEnabled() ? 5 : 3;
+ info(
+ "Wait until we're notified about all the stylesheets (top-level document + iframe)"
+ );
+ await waitFor(
+ () => availableResources.length === expectedStylesheetResources
+ );
+ is(
+ availableResources.length,
+ expectedStylesheetResources,
+ "Retrieved the expected stylesheets after the page was reloaded"
+ );
+
+ // the order of the resources is not guaranteed.
+ sortResourcesByExpectedOrder(availableResources);
+ await assertResource(availableResources[0], {
+ styleText: `.top-level-org{}`,
+ });
+ if (isEveryFrameTargetEnabled()) {
+ await assertResource(availableResources[1], {
+ styleText: `.frame-org-1{}`,
+ });
+ await assertResource(availableResources[2], {
+ styleText: `.frame-org-2{}`,
+ });
+ await assertResource(availableResources[3], {
+ styleText: `.frame-com-1{}`,
+ });
+ await assertResource(availableResources[4], {
+ styleText: `.frame-com-new-bc{}`,
+ });
+ } else {
+ await assertResource(availableResources[1], {
+ styleText: `.frame-com-1{}`,
+ });
+ await assertResource(availableResources[2], {
+ styleText: `.frame-com-new-bc{}`,
+ });
+ }
+
+ is(
+ await getDocumentStyleSheetChangeEventsEnabled(tab.linkedBrowser),
+ true,
+ `styleSheetChangeEventsEnabled is still true on the top level document after reloading`
+ );
+
+ if (isEveryFrameTargetEnabled()) {
+ const bc = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.querySelector("#same-origin-1").browsingContext
+ );
+ is(
+ await getDocumentStyleSheetChangeEventsEnabled(bc),
+ true,
+ `styleSheetChangeEventsEnabled is still true on the iframe after reloading`
+ );
+ }
+
+ // clear availableResources so it's easier to test
+ availableResources = [];
+
+ info(
+ "Check that styleSheetChangeEventsEnabled persist when navigating to a page that creates a new browsing context"
+ );
+ const previousBrowsingContextId = tab.linkedBrowser.browsingContext.id;
+ const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ TEST_URI_NEW_BROWSING_CONTEXT
+ );
+ await onLoaded;
+
+ isnot(
+ tab.linkedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ info("Wait to get the stylesheet for the new document");
+ await waitFor(() => availableResources.length === 1);
+ ok(true, "We received the stylesheet for the new document");
+ await assertResource(availableResources[0], {
+ styleText: `.top-level-org-new-bc{}`,
+ });
+ is(
+ await getDocumentStyleSheetChangeEventsEnabled(tab.linkedBrowser),
+ true,
+ `styleSheetChangeEventsEnabled is still true after navigating to a new browsing context`
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+/**
+ * Returns the value of the browser/browsingContext document `styleSheetChangeEventsEnabled`
+ * property.
+ *
+ * @param {Browser|BrowsingContext} browserOrBrowsingContext: The browser element or a
+ * browsing context.
+ * @returns {Promise<Boolean>}
+ */
+function getDocumentStyleSheetChangeEventsEnabled(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ return content.document.styleSheetChangeEventsEnabled;
+ });
+}
+
+/**
+ * Sort the passed array of stylesheet resources.
+ *
+ * Since the order of resources are not guaranteed, the <style> elements we use in this test
+ * have a "title" attribute that represent their expected order so we can sort them in
+ * a way that makes it easier for us to assert.
+ *
+ * @param {Array<Object>} resources: Array of stylesheet resources
+ */
+function sortResourcesByExpectedOrder(resources) {
+ resources.sort((a, b) => {
+ return Number(a.title) > Number(b.title);
+ });
+}
+
+/**
+ * Check that the resources have the expected text
+ *
+ * @param {Array<Object>} resources: Array of stylesheet resources
+ * @param {Array<Object>} expected: Array of object of the following shape:
+ * @param {Object} expected[]
+ * @param {Object} expected[].styleText: Expected text content of the stylesheet
+ */
+async function assertResource(resource, expected) {
+ const styleText = (await getStyleSheetResourceText(resource)).trim();
+ is(styleText, expected.styleText, "Style text is correct");
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_stylesheets_nested_iframes.js b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_nested_iframes.js
new file mode 100644
index 0000000000..0b13f75ab9
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_stylesheets_nested_iframes.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that stylesheets are retrieved even if an iframe does not have a content document.
+
+const TEST_URI = URL_ROOT_SSL + "stylesheets-nested-iframes.html";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Check whether ResourceCommand gets existing stylesheet");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ // Bug 285395 limits the number of nested iframes to 10, and we have one stylesheet per document.
+ await waitFor(() => availableResources.length >= 10);
+
+ is(
+ availableResources.length,
+ 10,
+ "Got the expected number of stylesheets, even with documentless iframes"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_target_destroy.js b/devtools/shared/commands/resource/tests/browser_resources_target_destroy.js
new file mode 100644
index 0000000000..fa7813d26e
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_target_destroy.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the server ResourceCommand are destroyed when the associated target actors
+// are destroyed.
+
+add_task(async function () {
+ const tab = await addTab("data:text/html,Test");
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ // Start watching for console messages. We don't care about messages here, only the
+ // registration/destroy mechanism, so we make onAvailable a no-op function.
+ await resourceCommand.watchResources(
+ [resourceCommand.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: () => {},
+ }
+ );
+
+ info(
+ "Spawn a content task in order to be able to manipulate actors and resource watchers directly"
+ );
+ const connectionPrefix = targetCommand.watcherFront.actorID.replace(
+ /watcher\d+$/,
+ ""
+ );
+ await ContentTask.spawn(
+ tab.linkedBrowser,
+ [connectionPrefix],
+ function (_connectionPrefix) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const { TargetActorRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs"
+ );
+ const {
+ getResourceWatcher,
+ TYPES,
+ } = require("resource://devtools/server/actors/resources/index.js");
+
+ // Retrieve the target actor instance and its watcher for console messages
+ const targetActor = TargetActorRegistry.getTargetActors(
+ {
+ type: "browser-element",
+ browserId: content.browsingContext.browserId,
+ },
+ _connectionPrefix
+ ).find(actor => actor.isTopLevelTarget);
+ ok(
+ targetActor,
+ "Got the top level target actor from the content process"
+ );
+ const watcher = getResourceWatcher(targetActor, TYPES.CONSOLE_MESSAGE);
+
+ // Storing the target actor in the global so we can retrieve it later, even if it
+ // was destroyed
+ content._testTargetActor = targetActor;
+
+ is(!!watcher, true, "The console message resource watcher was created");
+ }
+ );
+
+ info("Close the client, which will destroy the target");
+ targetCommand.destroy();
+ await client.close();
+
+ info(
+ "Spawn a content task in order to run some assertions on actors and resource watchers directly"
+ );
+ await ContentTask.spawn(tab.linkedBrowser, [], function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ getResourceWatcher,
+ TYPES,
+ } = require("resource://devtools/server/actors/resources/index.js");
+
+ ok(
+ content._testTargetActor && !content._testTargetActor.actorID,
+ "The target was destroyed when the client was closed"
+ );
+
+ // Retrieve the console message resource watcher
+ const watcher = getResourceWatcher(
+ content._testTargetActor,
+ TYPES.CONSOLE_MESSAGE
+ );
+
+ is(
+ !!watcher,
+ false,
+ "The console message resource watcher isn't registered anymore after the target was destroyed"
+ );
+
+ // Cleanup work variable
+ delete content._testTargetActor;
+ });
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_target_resources_race.js b/devtools/shared/commands/resource/tests/browser_resources_target_resources_race.js
new file mode 100644
index 0000000000..557d14380a
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_target_resources_race.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test initial target resources are correctly retrieved even when several calls
+ * to watchResources are made simultaneously.
+ *
+ * This checks a race condition which occurred when calling watchResources
+ * simultaneously. This made the "second" call to watchResources miss existing
+ * resources (in case those are emitted from the target instead of the watcher).
+ * See Bug 1663896.
+ */
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ const { client, resourceCommand, targetCommand } =
+ await initMultiProcessResourceCommand();
+
+ const expectedPlatformMessage = "expectedMessage";
+
+ info("Log a message *before* calling ResourceCommand.watchResources");
+ Services.console.logStringMessage(expectedPlatformMessage);
+
+ info("Call watchResources from 2 separate call sites consecutively");
+
+ // Empty onAvailable callback for CSS MESSAGES, we only want to check that
+ // the second resource we watch correctly provides existing resources.
+ const onCssMessageAvailable = resources => {};
+
+ // First call to watchResources.
+ // We do not await on `watchPromise1` here, in order to simulate simultaneous
+ // calls to watchResources (which could come from 2 separate modules in a real
+ // scenario).
+ const initialWatchPromise = resourceCommand.watchResources(
+ [resourceCommand.TYPES.CSS_MESSAGE],
+ {
+ onAvailable: onCssMessageAvailable,
+ }
+ );
+
+ // `waitForNextResource` will trigger another call to `watchResources`.
+ const { onResource: onMessageReceived } =
+ await resourceCommand.waitForNextResource(
+ resourceCommand.TYPES.PLATFORM_MESSAGE,
+ {
+ ignoreExistingResources: false,
+ predicate: r => r.message === expectedPlatformMessage,
+ }
+ );
+
+ info("Waiting for the expected message to be received");
+ await onMessageReceived;
+ ok(true, "All the expected messages were received");
+
+ info("Wait for the other watchResources promise to finish");
+ await initialWatchPromise;
+
+ // Unwatch all resources.
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CSS_MESSAGE], {
+ onAvailable: onCssMessageAvailable,
+ });
+
+ Services.console.reset();
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_target_switching.js b/devtools/shared/commands/resource/tests/browser_resources_target_switching.js
new file mode 100644
index 0000000000..4551fec778
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_target_switching.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the behavior of ResourceCommand when the top level target changes
+
+const TEST_URI =
+ "data:text/html;charset=utf-8,<script>console.log('foo');</script>";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+ const { CONSOLE_MESSAGE, SOURCE } = resourceCommand.TYPES;
+
+ info("Check the resources gotten from getAllResources at initial");
+ is(
+ resourceCommand.getAllResources(CONSOLE_MESSAGE).length,
+ 0,
+ "There is no resources before calling watchResources"
+ );
+
+ info(
+ "Start to watch the available resources in order to compare with resources gotten from getAllResources"
+ );
+ const availableResources = [];
+ const onAvailable = resources => {
+ availableResources.push(...resources);
+ };
+ await resourceCommand.watchResources([CONSOLE_MESSAGE], { onAvailable });
+
+ is(availableResources.length, 1, "Got the page message");
+ is(
+ availableResources[0].message.arguments[0],
+ "foo",
+ "Got the expected page message"
+ );
+
+ // Register another listener before unregistering the console listener
+ // otherwise the resource command stop watching for targets
+ const onSourceAvailable = () => {};
+ await resourceCommand.watchResources([SOURCE], {
+ onAvailable: onSourceAvailable,
+ });
+
+ info(
+ "Unregister the console listener and check that we no longer listen for console messages"
+ );
+ resourceCommand.unwatchResources([CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+
+ let onSwitched = targetCommand.once("switched-target");
+ info("Navigate to another process");
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await onSwitched;
+
+ is(
+ availableResources.length,
+ 1,
+ "about:robots doesn't fire any new message, so we should have a new one"
+ );
+
+ info("Navigate back to data: URI");
+ onSwitched = targetCommand.once("switched-target");
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, TEST_URI);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await onSwitched;
+
+ is(
+ availableResources.length,
+ 1,
+ "the data:URI fired a message, but we are no longer listening to it, so no new one should be notified"
+ );
+ is(
+ resourceCommand.getAllResources(CONSOLE_MESSAGE).length,
+ 0,
+ "As we are no longer listening to CONSOLE message, we should not collect any"
+ );
+
+ resourceCommand.unwatchResources([SOURCE], {
+ onAvailable: onSourceAvailable,
+ });
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_thread_states.js b/devtools/shared/commands/resource/tests/browser_resources_thread_states.js
new file mode 100644
index 0000000000..f915bb14d0
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_thread_states.js
@@ -0,0 +1,557 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around THREAD_STATE
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const BREAKPOINT_TEST_URL = URL_ROOT_SSL + "breakpoint_document.html";
+const REMOTE_IFRAME_URL =
+ "https://example.org/document-builder.sjs?html=" +
+ encodeURIComponent("<script>debugger;</script>");
+
+add_task(async function () {
+ // Check hitting the "debugger;" statement before and after calling
+ // watchResource(THREAD_TYPES). Both should break. First will
+ // be a cached resource and second will be a live one.
+ await checkBreakpointBeforeWatchResources();
+ await checkBreakpointAfterWatchResources();
+
+ // Check setting a real breakpoint on a given line
+ await checkRealBreakpoint();
+
+ // Check the "pause on exception" setting
+ await checkPauseOnException();
+
+ // Check an edge case where spamming setBreakpoints calls causes issues
+ await checkSetBeforeWatch();
+
+ // Check debugger statement for (remote) iframes
+ await checkDebuggerStatementInIframes();
+});
+
+async function checkBreakpointBeforeWatchResources() {
+ info(
+ "Check whether ResourceCommand gets existing breakpoint, being hit before calling watchResources"
+ );
+
+ const tab = await addTab(BREAKPOINT_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ // Ensure that the target front is initialized early from TargetCommand.onTargetAvailable
+ // By the time `initResourceCommand` resolves, it should already be initialized.
+ info(
+ "Verify that TargetFront's initialized is resolved after having calling attachAndInitThread"
+ );
+ await targetCommand.targetFront.initialized;
+
+ info("Run the 'debugger' statement");
+ // Note that we do not wait for the resolution of spawn as it will be paused
+ ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.window.wrappedJSObject.runDebuggerStatement();
+ });
+
+ info("Call watchResources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ 1,
+ "Got the THREAD_STATE's related to the debugger statement"
+ );
+ const threadState = availableResources.pop();
+
+ assertPausedResource(threadState, {
+ state: "paused",
+ why: {
+ type: "debuggerStatement",
+ },
+ frame: {
+ type: "call",
+ asyncCause: null,
+ state: "on-stack",
+ // this: object actor's form referring to `this` variable
+ displayName: "runDebuggerStatement",
+ // arguments: []
+ where: {
+ line: 17,
+ column: 6,
+ },
+ },
+ });
+
+ const { threadFront } = targetCommand.targetFront;
+ await threadFront.resume();
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Wait until we receive the resumed event"
+ );
+
+ const resumed = availableResources.pop();
+
+ assertResumedResource(resumed);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function checkBreakpointAfterWatchResources() {
+ info(
+ "Check whether ResourceCommand gets breakpoint hit after calling watchResources"
+ );
+
+ const tab = await addTab(BREAKPOINT_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Call watchResources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ 0,
+ "Got no THREAD_STATE when calling watchResources"
+ );
+
+ info("Run the 'debugger' statement");
+ // Note that we do not wait for the resolution of spawn as it will be paused
+ ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.window.wrappedJSObject.runDebuggerStatement();
+ });
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Got the THREAD_STATE related to the debugger statement"
+ );
+ const threadState = availableResources.pop();
+
+ assertPausedResource(threadState, {
+ state: "paused",
+ why: {
+ type: "debuggerStatement",
+ },
+ frame: {
+ type: "call",
+ asyncCause: null,
+ state: "on-stack",
+ // this: object actor's form referring to `this` variable
+ displayName: "runDebuggerStatement",
+ // arguments: []
+ where: {
+ line: 17,
+ column: 6,
+ },
+ },
+ });
+
+ // treadFront is created and attached while calling watchResources
+ const { threadFront } = targetCommand.targetFront;
+
+ await threadFront.resume();
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Wait until we receive the resumed event"
+ );
+
+ const resumed = availableResources.pop();
+
+ assertResumedResource(resumed);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function checkRealBreakpoint() {
+ info(
+ "Check whether ResourceCommand gets breakpoint set via the thread Front (instead of just debugger statements)"
+ );
+
+ const tab = await addTab(BREAKPOINT_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Call watchResources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ 0,
+ "Got no THREAD_STATE when calling watchResources"
+ );
+
+ // treadFront is created and attached while calling watchResources
+ const { threadFront } = targetCommand.targetFront;
+
+ // We have to call `sources` request, otherwise the Thread Actor
+ // doesn't start watching for sources, and ignore the setBreakpoint call
+ // as it doesn't have any source registered.
+ await threadFront.getSources();
+
+ await threadFront.setBreakpoint(
+ { sourceUrl: BREAKPOINT_TEST_URL, line: 14 },
+ {}
+ );
+
+ info("Run the test function where we set a breakpoint");
+ // Note that we do not wait for the resolution of spawn as it will be paused
+ ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.window.wrappedJSObject.testFunction();
+ });
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Got the THREAD_STATE related to the debugger statement"
+ );
+ const threadState = availableResources.pop();
+
+ assertPausedResource(threadState, {
+ state: "paused",
+ why: {
+ type: "breakpoint",
+ },
+ frame: {
+ type: "call",
+ asyncCause: null,
+ state: "on-stack",
+ // this: object actor's form referring to `this` variable
+ displayName: "testFunction",
+ // arguments: []
+ where: {
+ line: 14,
+ column: 6,
+ },
+ },
+ });
+
+ await threadFront.resume();
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Wait until we receive the resumed event"
+ );
+
+ const resumed = availableResources.pop();
+
+ assertResumedResource(resumed);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function checkPauseOnException() {
+ info(
+ "Check whether ResourceCommand gets breakpoint for exception (when explicitly requested)"
+ );
+
+ const tab = await addTab(
+ "data:text/html,<meta charset=utf8><script>a.b.c.d</script>"
+ );
+
+ const { commands, resourceCommand, targetCommand } =
+ await initResourceCommand(tab);
+
+ info("Call watchResources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ 0,
+ "Got no THREAD_STATE when calling watchResources"
+ );
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ });
+
+ info("Reload the page, in order to trigger exception on load");
+ const reloaded = reloadBrowser();
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Got the THREAD_STATE related to the debugger statement"
+ );
+ const threadState = availableResources.pop();
+
+ assertPausedResource(threadState, {
+ state: "paused",
+ why: {
+ type: "exception",
+ },
+ frame: {
+ type: "global",
+ asyncCause: null,
+ state: "on-stack",
+ // this: object actor's form referring to `this` variable
+ displayName: "(global)",
+ // arguments: []
+ where: {
+ line: 1,
+ column: 27,
+ },
+ },
+ });
+
+ const { threadFront } = targetCommand.targetFront;
+ await threadFront.resume();
+ info("Wait for page to finish reloading after resume");
+ await reloaded;
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Wait until we receive the resumed event"
+ );
+
+ const resumed = availableResources.pop();
+
+ assertResumedResource(resumed);
+
+ targetCommand.destroy();
+ await commands.destroy();
+}
+
+async function checkSetBeforeWatch() {
+ info(
+ "Verify bug 1683139 - D103068, where setting a breakpoint before watching for thread state, avoid receiving the paused state"
+ );
+
+ const tab = await addTab(BREAKPOINT_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ // Instantiate the thread front in order to be able to set a breakpoint before watching for thread state
+ info("Attach the top level thread actor");
+ await targetCommand.targetFront.attachAndInitThread(targetCommand);
+ const { threadFront } = targetCommand.targetFront;
+
+ // We have to call `sources` request, otherwise the Thread Actor
+ // doesn't start watching for sources, and ignore the setBreakpoint call
+ // as it doesn't have any source registered.
+ await threadFront.getSources();
+
+ // Set the breakpoint before trying to hit it
+ await threadFront.setBreakpoint(
+ { sourceUrl: BREAKPOINT_TEST_URL, line: 14, column: 6 },
+ {}
+ );
+
+ info("Run the test function where we set a breakpoint");
+ // Note that we do not wait for the resolution of spawn as it will be paused
+ ContentTask.spawn(tab.linkedBrowser, null, () => {
+ content.window.wrappedJSObject.testFunction();
+ });
+
+ // bug 1683139 - D103068. Re-setting the breakpoint just before watching for thread state
+ // prevented to receive the paused state change.
+ await threadFront.setBreakpoint(
+ { sourceUrl: BREAKPOINT_TEST_URL, line: 14, column: 6 },
+ {}
+ );
+
+ info("Call watchResources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Got the THREAD_STATE related to the debugger statement"
+ );
+ const threadState = availableResources.pop();
+
+ assertPausedResource(threadState, {
+ state: "paused",
+ why: {
+ type: "breakpoint",
+ },
+ frame: {
+ type: "call",
+ asyncCause: null,
+ state: "on-stack",
+ // this: object actor's form referring to `this` variable
+ displayName: "testFunction",
+ // arguments: []
+ where: {
+ line: 14,
+ column: 6,
+ },
+ },
+ });
+
+ await threadFront.resume();
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Wait until we receive the resumed event"
+ );
+
+ const resumed = availableResources.pop();
+
+ assertResumedResource(resumed);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function checkDebuggerStatementInIframes() {
+ info("Check whether ResourceCommand gets breakpoint for (remote) iframes");
+
+ const tab = await addTab(BREAKPOINT_TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ info("Call watchResources");
+ const availableResources = [];
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ 0,
+ "Got no THREAD_STATE when calling watchResources"
+ );
+
+ info("Inject the iframe with an inline 'debugger' statement");
+ // Note that we do not wait for the resolution of spawn as it will be paused
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REMOTE_IFRAME_URL],
+ async function (url) {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Got the THREAD_STATE related to the iframe's debugger statement"
+ );
+ const threadState = availableResources.pop();
+
+ assertPausedResource(threadState, {
+ state: "paused",
+ why: {
+ type: "debuggerStatement",
+ },
+ frame: {
+ type: "global",
+ asyncCause: null,
+ state: "on-stack",
+ // this: object actor's form referring to `this` variable
+ displayName: "(global)",
+ // arguments: []
+ where: {
+ line: 1,
+ column: 8,
+ },
+ },
+ });
+
+ const iframeTarget = threadState.targetFront;
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ is(
+ iframeTarget.url,
+ REMOTE_IFRAME_URL,
+ "With fission/EFT, the pause is from the iframe's target"
+ );
+ } else {
+ is(
+ iframeTarget,
+ targetCommand.targetFront,
+ "Without fission/EFT, the pause is from the top level target"
+ );
+ }
+ const { threadFront } = iframeTarget;
+
+ await threadFront.resume();
+
+ await waitFor(
+ () => availableResources.length == 1,
+ "Wait until we receive the resumed event"
+ );
+
+ const resumed = availableResources.pop();
+
+ assertResumedResource(resumed);
+
+ targetCommand.destroy();
+ await client.close();
+}
+
+async function assertPausedResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.THREAD_STATE,
+ "Resource type is correct"
+ );
+ is(resource.state, "paused", "state attribute is correct");
+ is(resource.why.type, expected.why.type, "why.type attribute is correct");
+ is(
+ resource.frame.type,
+ expected.frame.type,
+ "frame.type attribute is correct"
+ );
+ is(
+ resource.frame.asyncCause,
+ expected.frame.asyncCause,
+ "frame.asyncCause attribute is correct"
+ );
+ is(
+ resource.frame.state,
+ expected.frame.state,
+ "frame.state attribute is correct"
+ );
+ is(
+ resource.frame.displayName,
+ expected.frame.displayName,
+ "frame.displayName attribute is correct"
+ );
+ is(
+ resource.frame.where.line,
+ expected.frame.where.line,
+ "frame.where.line attribute is correct"
+ );
+ is(
+ resource.frame.where.column,
+ expected.frame.where.column,
+ "frame.where.column attribute is correct"
+ );
+}
+
+async function assertResumedResource(resource) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.THREAD_STATE,
+ "Resource type is correct"
+ );
+ is(resource.state, "resumed", "state attribute is correct");
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_unwatch_early.js b/devtools/shared/commands/resource/tests/browser_resources_unwatch_early.js
new file mode 100644
index 0000000000..e3890cf970
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_unwatch_early.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that calling unwatchResources before watchResources could resolve still
+// removes watcher entries correctly.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const TEST_URI = "data:text/html;charset=utf-8,";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+ const { CONSOLE_MESSAGE, ROOT_NODE } = resourceCommand.TYPES;
+
+ info("Use console.log in the content page");
+ await logInTab(tab, "msg-1");
+
+ info("Call watchResources with various configurations");
+
+ // Watcher 1 only watches for CONSOLE_MESSAGE.
+ // For this call site, unwatchResource will be called before onAvailable has
+ // resolved.
+ const messages1 = [];
+ const onAvailable1 = createMessageCallback(messages1);
+ const onWatcher1Ready = resourceCommand.watchResources([CONSOLE_MESSAGE], {
+ onAvailable: onAvailable1,
+ });
+ resourceCommand.unwatchResources([CONSOLE_MESSAGE], {
+ onAvailable: onAvailable1,
+ });
+
+ info(
+ "Calling unwatchResources for an already unregistered callback should be a no-op"
+ );
+ // and more importantly, it should not throw
+ resourceCommand.unwatchResources([CONSOLE_MESSAGE], {
+ onAvailable: onAvailable1,
+ });
+
+ // Watcher 2 watches for CONSOLE_MESSAGE & another resource (ROOT_NODE).
+ // Again unwatchResource will be called before onAvailable has resolved.
+ // But unwatchResource is only called for CONSOLE_MESSAGE, not for ROOT_NODE.
+ const messages2 = [];
+ const onAvailable2 = createMessageCallback(messages2);
+ const onWatcher2Ready = resourceCommand.watchResources(
+ [CONSOLE_MESSAGE, ROOT_NODE],
+ {
+ onAvailable: onAvailable2,
+ }
+ );
+ resourceCommand.unwatchResources([CONSOLE_MESSAGE], {
+ onAvailable: onAvailable2,
+ });
+
+ // Watcher 3 watches for CONSOLE_MESSAGE, but we will not call unwatchResource
+ // explicitly for it before the end of test. Used as a reference.
+ const messages3 = [];
+ const onAvailable3 = createMessageCallback(messages3);
+ const onWatcher3Ready = resourceCommand.watchResources([CONSOLE_MESSAGE], {
+ onAvailable: onAvailable3,
+ });
+
+ info("Call unwatchResources for CONSOLE_MESSAGE on watcher 1 & 2");
+
+ info("Wait for all watchers `watchResources` to resolve");
+ await Promise.all([onWatcher1Ready, onWatcher2Ready, onWatcher3Ready]);
+ ok(!hasMessage(messages1, "msg-1"), "Watcher 1 did not receive msg-1");
+ ok(!hasMessage(messages2, "msg-1"), "Watcher 2 did not receive msg-1");
+ ok(hasMessage(messages3, "msg-1"), "Watcher 3 received msg-1");
+
+ info("Log a new message");
+ await logInTab(tab, "msg-2");
+
+ info("Wait until watcher 3 received the new message");
+ await waitUntil(() => hasMessage(messages3, "msg-2"));
+
+ ok(!hasMessage(messages1, "msg-2"), "Watcher 1 did not receive msg-2");
+ ok(!hasMessage(messages2, "msg-2"), "Watcher 2 did not receive msg-2");
+
+ targetCommand.destroy();
+ await client.close();
+});
+
+function logInTab(tab, message) {
+ return ContentTask.spawn(tab.linkedBrowser, message, function (_message) {
+ content.console.log(_message);
+ });
+}
+
+function hasMessage(messageResources, text) {
+ return messageResources.find(
+ resource => resource.message.arguments[0] === text
+ );
+}
+
+// All resource command callbacks share the same pattern here: they add all
+// console message resources to a provided `messages` array.
+function createMessageCallback(messages) {
+ const { CONSOLE_MESSAGE } = ResourceCommand.TYPES;
+ return async resources => {
+ for (const resource of resources) {
+ if (resource.resourceType === CONSOLE_MESSAGE) {
+ messages.push(resource);
+ }
+ }
+ };
+}
diff --git a/devtools/shared/commands/resource/tests/browser_resources_watch_unwatch_multiple.js b/devtools/shared/commands/resource/tests/browser_resources_watch_unwatch_multiple.js
new file mode 100644
index 0000000000..cc45e7bf7f
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_watch_unwatch_multiple.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that watching/unwatching multiple times works as expected
+
+add_task(async function () {
+ const TEST_URL = "data:text/html;charset=utf-8,<!DOCTYPE html>foo";
+ const tab = await addTab(TEST_URL);
+
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ let resources = [];
+ const onAvailable = _resources => {
+ resources.push(..._resources);
+ };
+
+ info("Watch for error messages resources");
+ await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable,
+ });
+
+ ok(
+ resourceCommand.isResourceWatched(resourceCommand.TYPES.ERROR_MESSAGE),
+ "The error message resource is currently been watched."
+ );
+
+ is(
+ resources.length,
+ 0,
+ "no resources were received after the first watchResources call"
+ );
+
+ info("Trigger an error in the page");
+ await ContentTask.spawn(tab.linkedBrowser, [], function frameScript() {
+ const document = content.document;
+ const scriptEl = document.createElement("script");
+ scriptEl.textContent = `document.unknownFunction()`;
+ document.body.appendChild(scriptEl);
+ });
+
+ await waitFor(() => resources.length === 1);
+ const EXPECTED_ERROR_MESSAGE =
+ "TypeError: document.unknownFunction is not a function";
+ is(
+ resources[0].pageError.errorMessage,
+ EXPECTED_ERROR_MESSAGE,
+ "The resource was received"
+ );
+
+ info("Unwatching resources…");
+ resourceCommand.unwatchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable,
+ });
+
+ ok(
+ !resourceCommand.isResourceWatched(resourceCommand.TYPES.ERROR_MESSAGE),
+ "The error message resource is no longer been watched."
+ );
+ // clearing resources
+ resources = [];
+
+ info("…and watching again");
+ await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable,
+ });
+
+ ok(
+ resourceCommand.isResourceWatched(resourceCommand.TYPES.ERROR_MESSAGE),
+ "The error message resource is been watched again."
+ );
+ is(
+ resources.length,
+ 1,
+ "we retrieve the expected number of existing resources"
+ );
+ is(
+ resources[0].pageError.errorMessage,
+ EXPECTED_ERROR_MESSAGE,
+ "The resource is the expected one"
+ );
+
+ targetCommand.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/commands/resource/tests/browser_resources_websocket.js b/devtools/shared/commands/resource/tests/browser_resources_websocket.js
new file mode 100644
index 0000000000..601620bc59
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/browser_resources_websocket.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceCommand API around WEBSOCKET.
+
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const IS_NUMBER = "IS_NUMBER";
+const SHOULD_EXIST = "SHOULD_EXIST";
+
+const targets = {
+ TOP_LEVEL_DOCUMENT: "top-level-document",
+ IN_PROCESS_IFRAME: "in-process-frame",
+ OUT_PROCESS_IFRAME: "out-process-frame",
+};
+
+add_task(async function () {
+ info("Testing the top-level document");
+ await testWebsocketResources(targets.TOP_LEVEL_DOCUMENT);
+ info("Testing the in-process iframe");
+ await testWebsocketResources(targets.IN_PROCESS_IFRAME);
+ info("Testing the out-of-process iframe");
+ await testWebsocketResources(targets.OUT_PROCESS_IFRAME);
+});
+
+async function testWebsocketResources(target) {
+ const tab = await addTab(URL_ROOT_SSL + "websocket_frontend.html");
+ const { client, resourceCommand, targetCommand } = await initResourceCommand(
+ tab
+ );
+
+ const availableResources = [];
+ function onResourceAvailable(resources) {
+ availableResources.push(...resources);
+ }
+
+ await resourceCommand.watchResources([resourceCommand.TYPES.WEBSOCKET], {
+ onAvailable: onResourceAvailable,
+ });
+
+ info("Check available resources at initial");
+ is(
+ availableResources.length,
+ 0,
+ "Length of existing resources is correct at initial"
+ );
+
+ info("Check resource of opening websocket");
+ await executeFunctionInContext(tab, target, "openConnection");
+
+ await waitUntil(() => availableResources.length === 1);
+
+ const httpChannelId = availableResources[0].httpChannelId;
+
+ ok(httpChannelId, "httpChannelId is present in the resource");
+
+ assertResource(availableResources[0], {
+ wsMessageType: "webSocketOpened",
+ effectiveURI:
+ "wss://example.com/browser/devtools/shared/commands/resource/tests/websocket_backend",
+ extensions: "permessage-deflate",
+ protocols: "",
+ });
+
+ info("Check resource of sending/receiving the data via websocket");
+ await executeFunctionInContext(tab, target, "sendData", "test");
+
+ await waitUntil(() => availableResources.length === 3);
+
+ assertResource(availableResources[1], {
+ wsMessageType: "frameSent",
+ httpChannelId,
+ data: {
+ type: "sent",
+ payload: "test",
+ timeStamp: SHOULD_EXIST,
+ finBit: SHOULD_EXIST,
+ rsvBit1: SHOULD_EXIST,
+ rsvBit2: SHOULD_EXIST,
+ rsvBit3: SHOULD_EXIST,
+ opCode: SHOULD_EXIST,
+ mask: SHOULD_EXIST,
+ maskBit: SHOULD_EXIST,
+ },
+ });
+ assertResource(availableResources[2], {
+ wsMessageType: "frameReceived",
+ httpChannelId,
+ data: {
+ type: "received",
+ payload: "test",
+ timeStamp: SHOULD_EXIST,
+ finBit: SHOULD_EXIST,
+ rsvBit1: SHOULD_EXIST,
+ rsvBit2: SHOULD_EXIST,
+ rsvBit3: SHOULD_EXIST,
+ opCode: SHOULD_EXIST,
+ mask: SHOULD_EXIST,
+ maskBit: SHOULD_EXIST,
+ },
+ });
+
+ info("Check resource of closing websocket");
+ await executeFunctionInContext(tab, target, "closeConnection");
+
+ await waitUntil(() => availableResources.length === 6);
+ assertResource(availableResources[3], {
+ wsMessageType: "frameSent",
+ httpChannelId,
+ data: {
+ type: "sent",
+ payload: "",
+ timeStamp: SHOULD_EXIST,
+ finBit: SHOULD_EXIST,
+ rsvBit1: SHOULD_EXIST,
+ rsvBit2: SHOULD_EXIST,
+ rsvBit3: SHOULD_EXIST,
+ opCode: SHOULD_EXIST,
+ mask: SHOULD_EXIST,
+ maskBit: SHOULD_EXIST,
+ },
+ });
+ assertResource(availableResources[4], {
+ wsMessageType: "frameReceived",
+ httpChannelId,
+ data: {
+ type: "received",
+ payload: "",
+ timeStamp: SHOULD_EXIST,
+ finBit: SHOULD_EXIST,
+ rsvBit1: SHOULD_EXIST,
+ rsvBit2: SHOULD_EXIST,
+ rsvBit3: SHOULD_EXIST,
+ opCode: SHOULD_EXIST,
+ mask: SHOULD_EXIST,
+ maskBit: SHOULD_EXIST,
+ },
+ });
+ assertResource(availableResources[5], {
+ wsMessageType: "webSocketClosed",
+ httpChannelId,
+ code: IS_NUMBER,
+ reason: "",
+ wasClean: true,
+ });
+
+ info("Check existing resources");
+ const existingResources = [];
+
+ function onExsistingResourceAvailable(resources) {
+ existingResources.push(...resources);
+ }
+
+ await resourceCommand.watchResources([resourceCommand.TYPES.WEBSOCKET], {
+ onAvailable: onExsistingResourceAvailable,
+ });
+
+ is(
+ availableResources.length,
+ existingResources.length,
+ "Length of existing resources is correct"
+ );
+
+ for (let i = 0; i < availableResources.length; i++) {
+ Assert.strictEqual(
+ availableResources[i],
+ existingResources[i],
+ `The ${i}th resource is correct`
+ );
+ }
+
+ await resourceCommand.unwatchResources([resourceCommand.TYPES.WEBSOCKET], {
+ onAvailable: onResourceAvailable,
+ });
+
+ await resourceCommand.unwatchResources([resourceCommand.TYPES.WEBSOCKET], {
+ onAvailable: onExsistingResourceAvailable,
+ });
+
+ targetCommand.destroy();
+ await client.close();
+ BrowserTestUtils.removeTab(tab);
+}
+
+/**
+ * Execute global functions defined in the correct
+ * target (top-level-window or frames) contexts.
+ *
+ * @param {object} tab The current window tab
+ * @param {string} target A string identify if we want to test the top level document or iframes
+ * @param {string} funcName The name of the global function which needs to be called.
+ * @param {*} funcArgs The arguments to pass to the global function
+ */
+async function executeFunctionInContext(tab, target, funcName, ...funcArgs) {
+ let browsingContext = tab.linkedBrowser.browsingContext;
+ // If the target is an iframe get its window global
+ if (target !== targets.TOP_LEVEL_DOCUMENT) {
+ browsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [target],
+ async _target => {
+ const iframe = content.document.getElementById(_target);
+ return iframe.browsingContext;
+ }
+ );
+ }
+
+ return SpecialPowers.spawn(
+ browsingContext,
+ [funcName, funcArgs],
+ async (_funcName, _funcArgs) => {
+ await content.wrappedJSObject[_funcName](..._funcArgs);
+ }
+ );
+}
+
+function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceCommand.TYPES.WEBSOCKET,
+ "Resource type is correct"
+ );
+
+ assertObject(resource, expected);
+}
+
+function assertObject(object, expected) {
+ for (const field in expected) {
+ if (typeof expected[field] === "object") {
+ assertObject(object[field], expected[field]);
+ } else if (expected[field] === SHOULD_EXIST) {
+ Assert.notStrictEqual(
+ object[field],
+ undefined,
+ `The value of ${field} exists`
+ );
+ } else if (expected[field] === IS_NUMBER) {
+ ok(!isNaN(object[field]), `The value of ${field} is number`);
+ } else {
+ is(object[field], expected[field], `The value of ${field} is correct`);
+ }
+ }
+}
diff --git a/devtools/shared/commands/resource/tests/doc_console.html b/devtools/shared/commands/resource/tests/doc_console.html
new file mode 100644
index 0000000000..ee883cf47d
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/doc_console.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test document for console</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+ <body>
+ <p>Test document for console</p>
+
+ <iframe src="data:text/html;charset=utf-8,foo<script>console.log('data url data log')</script>"></iframe>
+ <script>
+ "use strict";
+ console.log("top-level document log");
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/doc_console_iframe.html b/devtools/shared/commands/resource/tests/doc_console_iframe.html
new file mode 100644
index 0000000000..e088dff4e5
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/doc_console_iframe.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+ <body>
+ <p>remote iframe</p>
+ <script>
+ "use strict";
+ console.log(`${document.location.origin} iframe log`);
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/early_console_document.html b/devtools/shared/commands/resource/tests/early_console_document.html
new file mode 100644
index 0000000000..e4523dbdeb
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/early_console_document.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+ console.log("early-page-log");
+ </script>
+</head>
+<body></body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/empty.html b/devtools/shared/commands/resource/tests/empty.html
new file mode 100644
index 0000000000..195b296bfe
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/empty.html
@@ -0,0 +1,11 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Empty page (No network requests)</title>
+</head>
+<body></body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/fission_document.html b/devtools/shared/commands/resource/tests/fission_document.html
new file mode 100644
index 0000000000..222f92d999
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/fission_document.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test fission iframe</p>
+
+<script>
+ "use strict";
+ const iframe = document.createElement("iframe");
+ let iframeUrl = `https://example.org/browser/devtools/shared/commands/resource/tests/fission_iframe.html`;
+ if (document.location.search) {
+ iframeUrl += `?${new URLSearchParams(document.location.search)}`;
+ }
+ iframe.src = iframeUrl;
+ document.body.append(iframe);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/fission_document_workers.html b/devtools/shared/commands/resource/tests/fission_document_workers.html
new file mode 100644
index 0000000000..bbbe3e8bf8
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/fission_document_workers.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+
+ const params = new URLSearchParams(document.location.search);
+
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker("https://example.com/browser/devtools/shared/commands/resource/tests/test_worker.js#simple-worker");
+
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("https://example.com/browser/devtools/shared/commands/resource/tests/test_worker.js#shared-worker");
+
+ if (!params.has("noServiceWorker")) {
+ // Expose a reference to the registration so that tests can unregister it.
+ window.registrationPromise = navigator.serviceWorker.register("https://example.com/browser/devtools/shared/commands/resource/tests/test_service_worker.js#service-worker");
+ }
+
+ /* exported logMessageInWorker */
+ function logMessageInWorker(message) {
+ worker.postMessage({
+ type: "log-in-worker",
+ message,
+ });
+ }
+ </script>
+</head>
+<body>
+<p>Test fission iframe</p>
+
+<script>
+ "use strict";
+ const iframe = document.createElement("iframe");
+ let iframeUrl = `https://example.org/browser/devtools/shared/commands/resource/tests/fission_iframe_workers.html`;
+ if (document.location.search) {
+ iframeUrl += `?${new URLSearchParams(document.location.search)}`;
+ }
+ iframe.src = iframeUrl;
+ document.body.append(iframe);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/fission_iframe.html b/devtools/shared/commands/resource/tests/fission_iframe.html
new file mode 100644
index 0000000000..f674321102
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/fission_iframe.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>remote iframe</p>
+</body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/fission_iframe_workers.html b/devtools/shared/commands/resource/tests/fission_iframe_workers.html
new file mode 100644
index 0000000000..deae49f833
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/fission_iframe_workers.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+ const params = new URLSearchParams(document.location.search);
+ const hashSuffix = params.get("hashSuffix") || "in-iframe";
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker("test_worker.js#simple-worker-" + hashSuffix);
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("test_worker.js#shared-worker-" + hashSuffix);
+
+ /* exported logMessageInWorker */
+ function logMessageInWorker(message) {
+ worker.postMessage({
+ type: "log-in-worker",
+ message,
+ });
+ }
+ </script>
+</head>
+<body>
+<p>remote iframe</p>
+</body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/head.js b/devtools/shared/commands/resource/tests/head.js
new file mode 100644
index 0000000000..5cee383070
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/head.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+async function _initResourceCommandFromCommands(
+ commands,
+ { listenForWorkers = false } = {}
+) {
+ const targetCommand = commands.targetCommand;
+ if (listenForWorkers) {
+ targetCommand.listenForWorkers = true;
+ }
+ await targetCommand.startListening();
+
+ //Bug 1709065: Stop exporting resourceCommand and use commands.resourceCommand
+ //And rename all these methods
+ return {
+ client: commands.client,
+ commands,
+ resourceCommand: commands.resourceCommand,
+ targetCommand,
+ };
+}
+
+/**
+ * Instantiate a ResourceCommand for the given tab.
+ *
+ * @param {Tab} tab
+ * The browser frontend's tab to connect to.
+ * @param {Object} options
+ * @param {Boolean} options.listenForWorkers
+ * @return {Object} object
+ * @return {ResourceCommand} object.resourceCommand
+ * The underlying resource command interface.
+ * @return {Object} object.commands
+ * The commands object defined by modules from devtools/shared/commands.
+ * @return {DevToolsClient} object.client
+ * The underlying client instance.
+ * @return {TargetCommand} object.targetCommand
+ * The underlying target list instance.
+ */
+async function initResourceCommand(tab, options) {
+ const commands = await CommandsFactory.forTab(tab);
+ return _initResourceCommandFromCommands(commands, options);
+}
+
+/**
+ * Instantiate a multi-process ResourceCommand, watching all type of targets.
+ *
+ * @return {Object} object
+ * @return {ResourceCommand} object.resourceCommand
+ * The underlying resource command interface.
+ * @return {Object} object.commands
+ * The commands object defined by modules from devtools/shared/commands.
+ * @return {DevToolsClient} object.client
+ * The underlying client instance.
+ * @return {DevToolsClient} object.targetCommand
+ * The underlying target list instance.
+ */
+async function initMultiProcessResourceCommand() {
+ const commands = await CommandsFactory.forMainProcess();
+ return _initResourceCommandFromCommands(commands);
+}
+
+// Copied from devtools/shared/webconsole/test/chrome/common.js
+function checkObject(object, expected) {
+ if (object && object.getGrip) {
+ object = object.getGrip();
+ }
+
+ for (const name of Object.keys(expected)) {
+ const expectedValue = expected[name];
+ const value = object[name];
+ checkValue(name, value, expectedValue);
+ }
+}
+
+function checkValue(name, value, expected) {
+ if (expected === null) {
+ is(value, null, `'${name}' is null`);
+ } else if (expected === undefined) {
+ is(value, expected, `'${name}' is undefined`);
+ } else if (
+ typeof expected == "string" ||
+ typeof expected == "number" ||
+ typeof expected == "boolean"
+ ) {
+ is(value, expected, "property '" + name + "'");
+ } else if (expected instanceof RegExp) {
+ ok(expected.test(value), name + ": " + expected + " matched " + value);
+ } else if (Array.isArray(expected)) {
+ info("checking array for property '" + name + "'");
+ ok(Array.isArray(value), `property '${name}' is an array`);
+
+ is(value.length, expected.length, "Array has expected length");
+ if (value.length !== expected.length) {
+ is(JSON.stringify(value, null, 2), JSON.stringify(expected, null, 2));
+ } else {
+ checkObject(value, expected);
+ }
+ } else if (typeof expected == "object") {
+ info("checking object for property '" + name + "'");
+ checkObject(value, expected);
+ }
+}
+
+async function triggerNetworkRequests(browser, commands) {
+ for (let i = 0; i < commands.length; i++) {
+ await SpecialPowers.spawn(browser, [commands[i]], async function (code) {
+ const script = content.document.createElement("script");
+ script.append(
+ content.document.createTextNode(
+ `async function triggerRequest() {${code}}`
+ )
+ );
+ content.document.body.append(script);
+ await content.wrappedJSObject.triggerRequest();
+ script.remove();
+ });
+ }
+}
+
+/**
+ * Get the stylesheet text for a given stylesheet resource.
+ *
+ * @param {Object} styleSheetResource
+ * @returns Promise<String>
+ */
+async function getStyleSheetResourceText(styleSheetResource) {
+ const styleSheetsFront = await styleSheetResource.targetFront.getFront(
+ "stylesheets"
+ );
+ const res = await styleSheetsFront.getText(styleSheetResource.resourceId);
+ return res.string();
+}
diff --git a/devtools/shared/commands/resource/tests/network_document.html b/devtools/shared/commands/resource/tests/network_document.html
new file mode 100644
index 0000000000..5c4744cb0c
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/network_document.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Test for network events</title>
+ </head>
+ <body>
+ <p>Test for network events</p>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/network_document_navigation.html b/devtools/shared/commands/resource/tests/network_document_navigation.html
new file mode 100644
index 0000000000..c4ec651c05
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/network_document_navigation.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Test for network events</title>
+ </head>
+ <body>
+ <p>Test for network events</p>
+ <script src="network_navigation.js" />
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/network_navigation.js b/devtools/shared/commands/resource/tests/network_navigation.js
new file mode 100644
index 0000000000..6004b69d3c
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/network_navigation.js
@@ -0,0 +1 @@
+// empty script loaded by network_document_navigation.html
diff --git a/devtools/shared/commands/resource/tests/service-worker-sources.js b/devtools/shared/commands/resource/tests/service-worker-sources.js
new file mode 100644
index 0000000000..614644ee5d
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/service-worker-sources.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+function serviceWorkerSource() {}
diff --git a/devtools/shared/commands/resource/tests/sources.html b/devtools/shared/commands/resource/tests/sources.html
new file mode 100644
index 0000000000..9e1ad67d85
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/sources.html
@@ -0,0 +1,53 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ <!-- introductionType=eventHandler -->
+ <div onclick="console.log('link')">link</div>
+
+ <!-- introductionType=inlineScript mapped to scriptElement -->
+ <script type="text/javascript">
+ "use strict";
+ /* eslint-disable */
+ function inlineSource() {}
+
+ // introductionType=eval
+ // Assign it to a global in order to avoid it being GCed
+ eval("this.global = function evalFunction() {}");
+
+ // introductionType=Function
+ // Also assign to a global to avoid being GCed
+ this.global2 = new Function("return 42;");
+
+ // introductionType=injectedScript mapped to scriptElement
+ const script = document.createElement("script");
+ script.textContent = "console.log('inline-script')";
+ document.documentElement.appendChild(script);
+
+ // introductionType=Worker, but ends up being null on SourceActor's form
+ // Assign the worker to a global variable in order to avoid
+ // having it be GCed.
+ this.worker = new Worker("worker-sources.js");
+
+ window.registrationPromise = navigator.serviceWorker.register("service-worker-sources.js");
+
+ // introductionType=domTimer
+ setTimeout(`console.log("timeout")`, 0);
+
+ // introductionType=eventHandler
+ window.addEventListener("DOMContentLoaded", () => {
+ document.querySelector("div[onclick]").click();
+ });
+ </script>
+ <!-- introductionType=srcScript mapped to scriptElement -->
+ <script src="sources.js"></script>
+ <!-- introductionType=javascriptURL -->
+ <iframe src="javascript:666"></iframe>
+ <!-- srcdoc attribute on iframes -->
+ <iframe srcdoc="<script>console.log('srcdoc')</script> <script>console.log('srcdoc 2')</script>"></iframe>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/sources.js b/devtools/shared/commands/resource/tests/sources.js
new file mode 100644
index 0000000000..7ae6c6272b
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/sources.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+function scriptSource() {}
diff --git a/devtools/shared/commands/resource/tests/sse_backend.sjs b/devtools/shared/commands/resource/tests/sse_backend.sjs
new file mode 100644
index 0000000000..777520577a
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/sse_backend.sjs
@@ -0,0 +1,8 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.processAsync();
+ response.setHeader("Content-Type", "text/event-stream");
+ response.write("data: Why so serious?\n\n");
+ response.finish();
+}
diff --git a/devtools/shared/commands/resource/tests/sse_frontend.html b/devtools/shared/commands/resource/tests/sse_frontend.html
new file mode 100644
index 0000000000..3bdddbc5bc
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/sse_frontend.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>SSE Inspection Test Page</title>
+ </head>
+ <body>
+ <h1>SSE Inspection Test Page</h1>
+ <script type="text/javascript">
+ "use strict";
+
+ /* exported openConnection */
+ function openConnection() {
+ return new Promise(resolve => {
+ const es = new EventSource("sse_backend.sjs");
+ es.onmessage = function (e) {
+ es.close();
+ resolve();
+ };
+ });
+ }
+ </script>
+ <iframe id="in-process-frame" src="https://example.com/browser/devtools/shared/commands/resource/tests/sse_frontend_iframe.html"> </iframe>
+ <iframe id="out-process-frame" src="https://example.org/browser/devtools/shared/commands/resource/tests/sse_frontend_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/sse_frontend_iframe.html b/devtools/shared/commands/resource/tests/sse_frontend_iframe.html
new file mode 100644
index 0000000000..477dca013d
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/sse_frontend_iframe.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>SSE Inspection Test Page in iframe</title>
+ </head>
+ <body>
+ <h1>SSE Inspection Test Page in Iframe</h1>
+ <script type="text/javascript">
+ "use strict";
+
+ /* exported openConnection */
+ function openConnection() {
+ return new Promise(resolve => {
+ const es = new EventSource("sse_backend.sjs");
+ es.onmessage = function (e) {
+ es.close();
+ resolve();
+ };
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/style_document.css b/devtools/shared/commands/resource/tests/style_document.css
new file mode 100644
index 0000000000..aa54533924
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/style_document.css
@@ -0,0 +1 @@
+body { margin: 1px; }
diff --git a/devtools/shared/commands/resource/tests/style_document.html b/devtools/shared/commands/resource/tests/style_document.html
new file mode 100644
index 0000000000..deaf6c4248
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/style_document.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Test style document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <style>
+ body { color: lime; }
+ </style>
+ <link href="style_document.css" rel="stylesheet">
+ <script>
+ "use strict";
+ const s = new CSSStyleSheet();
+ s.replaceSync("body { background-color: blue }");
+ document.adoptedStyleSheets.push(s);
+ </script>
+ </head>
+ <body>
+ <iframe src="https://example.org/browser/devtools/shared/commands/resource/tests/style_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/style_iframe.css b/devtools/shared/commands/resource/tests/style_iframe.css
new file mode 100644
index 0000000000..30e7ae802b
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/style_iframe.css
@@ -0,0 +1 @@
+body { padding: 1px; }
diff --git a/devtools/shared/commands/resource/tests/style_iframe.html b/devtools/shared/commands/resource/tests/style_iframe.html
new file mode 100644
index 0000000000..11ad9f785b
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/style_iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Test style iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <style>
+ body { background-color: pink; }
+ </style>
+ <link href="style_iframe.css" rel="stylesheet" type="text/css"/>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/stylesheets-nested-iframes.html b/devtools/shared/commands/resource/tests/stylesheets-nested-iframes.html
new file mode 100644
index 0000000000..eb6c371867
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/stylesheets-nested-iframes.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>StyleSheetsActor iframe test</title>
+ <style>
+ p {
+ padding: 1em;
+ }
+ </style>
+</head>
+<body>
+ <p>A test page with nested iframes</p>
+ <iframe></iframe>
+ <script type="application/javascript">
+ "use strict";
+
+ const iframe = document.querySelector("iframe");
+ let i = parseInt(location.href.split("?")[1], 10) || 1;
+
+ // The frame can't have the same src URL as any of its ancestors.
+ // This will not infinitely recurse because a frame won't get a content
+ // document once it's nested deeply enough.
+ iframe.src = location.href.split("?")[0] + "?" + (++i);
+ </script>
+</body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/test_image.png b/devtools/shared/commands/resource/tests/test_image.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/test_image.png
Binary files differ
diff --git a/devtools/shared/commands/resource/tests/test_service_worker.js b/devtools/shared/commands/resource/tests/test_service_worker.js
new file mode 100644
index 0000000000..aabc3fda0f
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/test_service_worker.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We don't need any computation in the worker,
+// but at least register a fetch listener so that
+// we force instantiating the SW when loading the page.
+self.onfetch = function (event) {
+ // do nothing.
+};
diff --git a/devtools/shared/commands/resource/tests/test_worker.js b/devtools/shared/commands/resource/tests/test_worker.js
new file mode 100644
index 0000000000..60ccc6d52b
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/test_worker.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+console.log("[WORKER] started", globalThis.location.toString(), globalThis);
+
+globalThis.onmessage = function (e) {
+ const { type, message } = e.data;
+
+ if (type === "log-in-worker") {
+ // Printing `e` so we can check that we have an object and not a stringified version
+ console.log("[WORKER]", message, e);
+ }
+};
diff --git a/devtools/shared/commands/resource/tests/websocket_backend_wsh.py b/devtools/shared/commands/resource/tests/websocket_backend_wsh.py
new file mode 100644
index 0000000000..170f15fe6c
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/websocket_backend_wsh.py
@@ -0,0 +1,20 @@
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ while not request.client_terminated:
+ resp = msgutil.receive_message(request)
+ msgutil.send_message(request, resp)
+
+
+def web_socket_passive_closing_handshake(request):
+ # If we use `pass` here, the `payload` of `frameReceived` which will be happened
+ # of communication of closing will be `\u0003è`. In order to make the `payload`
+ # to be empty string, return code and reason explicitly.
+ code = None
+ reason = None
+ return code, reason
diff --git a/devtools/shared/commands/resource/tests/websocket_frontend.html b/devtools/shared/commands/resource/tests/websocket_frontend.html
new file mode 100644
index 0000000000..7efe11f9eb
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/websocket_frontend.html
@@ -0,0 +1,45 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Websocket Inspection Test Page</title>
+ </head>
+ <body>
+ <h1>Websocket Inspection Test Page</h1>
+ <script type="text/javascript">
+ /* exported openConnection, closeConnection, sendData */
+ "use strict";
+
+ let webSocket;
+ function openConnection() {
+ return new Promise(resolve => {
+ webSocket = new WebSocket(
+ "wss://example.com/browser/devtools/shared/commands/resource/tests/websocket_backend"
+ );
+ webSocket.onopen = () => {
+ resolve();
+ };
+ });
+ }
+
+ function closeConnection() {
+ return new Promise(resolve => {
+ webSocket.onclose = () => {
+ resolve();
+ };
+ webSocket.close();
+ })
+ }
+
+ function sendData(payload) {
+ webSocket.send(payload);
+ }
+ </script>
+ <iframe id="in-process-frame"
+ src="https://example.com/browser/devtools/shared/commands/resource/tests/websocket_frontend_iframe.html"></iframe>
+ <iframe id="out-process-frame"
+ src="https://example.org/browser/devtools/shared/commands/resource/tests/websocket_frontend_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/websocket_frontend_iframe.html b/devtools/shared/commands/resource/tests/websocket_frontend_iframe.html
new file mode 100644
index 0000000000..e18576f911
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/websocket_frontend_iframe.html
@@ -0,0 +1,41 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Websocket Inspection Test Page</title>
+ </head>
+ <body>
+ <h1>Websocket Inspection Test Page</h1>
+ <script type="text/javascript">
+ /* exported openConnection, closeConnection, sendData */
+ "use strict";
+
+ let webSocket;
+ function openConnection() {
+ return new Promise(resolve => {
+ webSocket = new WebSocket(
+ "wss://example.com/browser/devtools/shared/commands/resource/tests/websocket_backend"
+ );
+ webSocket.onopen = () => {
+ resolve();
+ };
+ });
+ }
+
+ function closeConnection() {
+ return new Promise(resolve => {
+ webSocket.onclose = () => {
+ resolve();
+ };
+ webSocket.close();
+ })
+ }
+
+ function sendData(payload) {
+ webSocket.send(payload);
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/commands/resource/tests/worker-sources.js b/devtools/shared/commands/resource/tests/worker-sources.js
new file mode 100644
index 0000000000..dcf2ed8031
--- /dev/null
+++ b/devtools/shared/commands/resource/tests/worker-sources.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+function workerSource() {}
diff --git a/devtools/shared/commands/resource/transformers/console-messages.js b/devtools/shared/commands/resource/transformers/console-messages.js
new file mode 100644
index 0000000000..9c8ca51f04
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/console-messages.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// eslint-disable-next-line mozilla/reject-some-requires
+loader.lazyRequireGetter(
+ this,
+ "getAdHocFrontOrPrimitiveGrip",
+ "resource://devtools/client/fronts/object.js",
+ true
+);
+
+module.exports = function ({ resource, targetFront }) {
+ if (Array.isArray(resource.message.arguments)) {
+ // We might need to create fronts for each of the message arguments.
+ resource.message.arguments = resource.message.arguments.map(arg =>
+ getAdHocFrontOrPrimitiveGrip(arg, targetFront)
+ );
+ }
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/error-messages.js b/devtools/shared/commands/resource/transformers/error-messages.js
new file mode 100644
index 0000000000..2b71f5b7ca
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/error-messages.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";
+
+// eslint-disable-next-line mozilla/reject-some-requires
+loader.lazyRequireGetter(
+ this,
+ "getAdHocFrontOrPrimitiveGrip",
+ "resource://devtools/client/fronts/object.js",
+ true
+);
+
+module.exports = function ({ resource, targetFront }) {
+ if (resource?.pageError?.errorMessage) {
+ resource.pageError.errorMessage = getAdHocFrontOrPrimitiveGrip(
+ resource.pageError.errorMessage,
+ targetFront
+ );
+ }
+
+ if (resource?.pageError?.exception) {
+ resource.pageError.exception = getAdHocFrontOrPrimitiveGrip(
+ resource.pageError.exception,
+ targetFront
+ );
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/moz.build b/devtools/shared/commands/resource/transformers/moz.build
new file mode 100644
index 0000000000..5b0b94853a
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/moz.build
@@ -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/.
+
+DevToolsModules(
+ "console-messages.js",
+ "error-messages.js",
+ "network-events.js",
+ "storage-cache.js",
+ "storage-cookie.js",
+ "storage-extension.js",
+ "storage-indexed-db.js",
+ "storage-local-storage.js",
+ "storage-session-storage.js",
+ "thread-states.js",
+)
diff --git a/devtools/shared/commands/resource/transformers/network-events.js b/devtools/shared/commands/resource/transformers/network-events.js
new file mode 100644
index 0000000000..d7f757d706
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/network-events.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 {
+ getUrlDetails,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+module.exports = function ({ resource }) {
+ resource.urlDetails = getUrlDetails(resource.url);
+ resource.startedMs = Date.parse(resource.startedDateTime);
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/storage-cache.js b/devtools/shared/commands/resource/transformers/storage-cache.js
new file mode 100644
index 0000000000..245d892041
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/storage-cache.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 {
+ TYPES: { CACHE_STORAGE },
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ if (!(resource instanceof Front) && watcherFront) {
+ // instantiate front for local storage
+ resource = types.getType("Cache").read(resource, targetFront);
+ resource.resourceType = CACHE_STORAGE;
+ resource.resourceKey = "Cache";
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/storage-cookie.js b/devtools/shared/commands/resource/transformers/storage-cookie.js
new file mode 100644
index 0000000000..23e221672b
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/storage-cookie.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ TYPES: { COOKIE },
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ if (!(resource instanceof Front) && watcherFront) {
+ const { innerWindowId } = resource;
+
+ // it's safe to instantiate the front now, so we do it.
+ resource = types.getType("cookies").read(resource, targetFront);
+ resource.resourceType = COOKIE;
+ resource.resourceId = `${COOKIE}-${targetFront.browsingContextID}`;
+ resource.resourceKey = "cookies";
+ resource.innerWindowId = innerWindowId;
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/storage-extension.js b/devtools/shared/commands/resource/transformers/storage-extension.js
new file mode 100644
index 0000000000..3e40bdd6d0
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/storage-extension.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ TYPES: { EXTENSION_STORAGE },
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ if (!(resource instanceof Front) && watcherFront) {
+ const { innerWindowId } = resource;
+
+ // it's safe to instantiate the front now, so we do it.
+ resource = types.getType("extensionStorage").read(resource, targetFront);
+ resource.resourceType = EXTENSION_STORAGE;
+ resource.resourceId = `${EXTENSION_STORAGE}-${targetFront.browsingContextID}`;
+ resource.resourceKey = "extensionStorage";
+ resource.innerWindowId = innerWindowId;
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/storage-indexed-db.js b/devtools/shared/commands/resource/transformers/storage-indexed-db.js
new file mode 100644
index 0000000000..8021719070
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/storage-indexed-db.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ TYPES: { INDEXED_DB },
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ if (!(resource instanceof Front) && watcherFront) {
+ const { innerWindowId } = resource;
+
+ // it's safe to instantiate the front now, so we do it.
+ resource = types.getType("indexedDB").read(resource, targetFront);
+ resource.resourceType = INDEXED_DB;
+ resource.resourceId = `${INDEXED_DB}-${targetFront.browsingContextID}`;
+ resource.resourceKey = "indexedDB";
+ resource.innerWindowId = innerWindowId;
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/storage-local-storage.js b/devtools/shared/commands/resource/transformers/storage-local-storage.js
new file mode 100644
index 0000000000..13488723f3
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/storage-local-storage.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 {
+ TYPES: { LOCAL_STORAGE },
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ if (!(resource instanceof Front) && watcherFront) {
+ // instantiate front for local storage
+ resource = types.getType("localStorage").read(resource, targetFront);
+ resource.resourceType = LOCAL_STORAGE;
+ resource.resourceKey = "localStorage";
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/storage-session-storage.js b/devtools/shared/commands/resource/transformers/storage-session-storage.js
new file mode 100644
index 0000000000..ab9f1361c8
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/storage-session-storage.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 {
+ TYPES: { SESSION_STORAGE },
+} = require("resource://devtools/shared/commands/resource/resource-command.js");
+
+const { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ if (!(resource instanceof Front) && watcherFront) {
+ // instantiate front for session storage
+ resource = types.getType("sessionStorage").read(resource, targetFront);
+ resource.resourceType = SESSION_STORAGE;
+ resource.resourceKey = "sessionStorage";
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/resource/transformers/thread-states.js b/devtools/shared/commands/resource/transformers/thread-states.js
new file mode 100644
index 0000000000..1564585b36
--- /dev/null
+++ b/devtools/shared/commands/resource/transformers/thread-states.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 { Front, types } = require("resource://devtools/shared/protocol.js");
+
+module.exports = function ({ resource, watcherFront, targetFront }) {
+ // only "paused" have a frame attribute, and legacy listeners are already passing a FrameFront
+ if (resource.frame && !(resource.frame instanceof Front)) {
+ // Use ThreadFront as parent as debugger's commands.js expects FrameFront to be children
+ // of the ThreadFront.
+ resource.frame = types
+ .getType("frame")
+ .read(resource.frame, targetFront.threadFront);
+ }
+
+ // If we are using server side request (i.e. watcherFront is defined)
+ // Fake paused and resumed events as the thread front depends on them.
+ // We can't emit "EventEmitter" events, as ThreadFront uses `Front.before`
+ // to listen for paused and resumed. ("before" is part of protocol.js Front and not part of EventEmitter)
+ if (watcherFront) {
+ if (resource.state == "paused") {
+ targetFront.threadFront._beforePaused(resource);
+ } else if (resource.state == "resumed") {
+ targetFront.threadFront._beforeResumed(resource);
+ }
+ }
+
+ return resource;
+};
diff --git a/devtools/shared/commands/root-resource/moz.build b/devtools/shared/commands/root-resource/moz.build
new file mode 100644
index 0000000000..2bf7204d1f
--- /dev/null
+++ b/devtools/shared/commands/root-resource/moz.build
@@ -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/.
+
+DevToolsModules(
+ "root-resource-command.js",
+)
diff --git a/devtools/shared/commands/root-resource/root-resource-command.js b/devtools/shared/commands/root-resource/root-resource-command.js
new file mode 100644
index 0000000000..1071d1bcb1
--- /dev/null
+++ b/devtools/shared/commands/root-resource/root-resource-command.js
@@ -0,0 +1,348 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { throttle } = require("resource://devtools/shared/throttle.js");
+
+class RootResourceCommand {
+ /**
+ * This class helps retrieving existing and listening to "root" resources.
+ *
+ * This is a fork of ResourceCommand, but specific to context-less
+ * resources which can be listened to right away when connecting to the RDP server.
+ *
+ * The main difference in term of implementation is that:
+ * - we receive a root front as constructor argument (instead of `commands` object)
+ * - we only listen for RDP events on the Root actor (instead of watcher and target actors)
+ * - there is no legacy listener support
+ * - there is no resource transformers
+ * - there is a lot of logic around targets that is removed here.
+ *
+ * See ResourceCommand for comments and jsdoc.
+ *
+ * TODO Bug 1758530 - Investigate sharing code with ResourceCommand instead of forking.
+ *
+ * @param object commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ * @param object rootFront
+ * Front for the Root actor.
+ */
+ constructor({ commands, rootFront }) {
+ this.rootFront = rootFront ? rootFront : commands.client.mainRoot;
+
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this._onResourceDestroyed = this._onResourceDestroyed.bind(this);
+
+ this._watchers = [];
+
+ this._pendingWatchers = new Set();
+
+ this._cache = [];
+ this._listenedResources = new Set();
+
+ this._processingExistingResources = new Set();
+
+ this._notifyWatchers = this._notifyWatchers.bind(this);
+ this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100);
+ }
+
+ getAllResources(resourceType) {
+ return this._cache.filter(r => r.resourceType === resourceType);
+ }
+
+ getResourceById(resourceType, resourceId) {
+ return this._cache.find(
+ r => r.resourceType === resourceType && r.resourceId === resourceId
+ );
+ }
+
+ async watchResources(resources, options) {
+ const {
+ onAvailable,
+ onUpdated,
+ onDestroyed,
+ ignoreExistingResources = false,
+ } = options;
+
+ if (typeof onAvailable !== "function") {
+ throw new Error(
+ "RootResourceCommand.watchResources expects an onAvailable function as argument"
+ );
+ }
+
+ for (const type of resources) {
+ if (!this._isValidResourceType(type)) {
+ throw new Error(
+ `RootResourceCommand.watchResources invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ const pendingWatcher = {
+ resources,
+ onAvailable,
+ };
+ this._pendingWatchers.add(pendingWatcher);
+
+ if (!this._listenerRegistered) {
+ this._listenerRegistered = true;
+ this.rootFront.on("resource-available-form", this._onResourceAvailable);
+ this.rootFront.on("resource-destroyed-form", this._onResourceDestroyed);
+ }
+
+ const promises = [];
+ for (const resource of resources) {
+ promises.push(this._startListening(resource));
+ }
+ await Promise.all(promises);
+
+ this._notifyWatchers();
+
+ this._pendingWatchers.delete(pendingWatcher);
+
+ const watchedResources = pendingWatcher.resources;
+
+ if (!watchedResources.length) {
+ return;
+ }
+
+ this._watchers.push({
+ resources: watchedResources,
+ onAvailable,
+ onUpdated,
+ onDestroyed,
+ pendingEvents: [],
+ });
+
+ if (!ignoreExistingResources) {
+ await this._forwardExistingResources(watchedResources, onAvailable);
+ }
+ }
+
+ unwatchResources(resources, options) {
+ const { onAvailable } = options;
+
+ if (typeof onAvailable !== "function") {
+ throw new Error(
+ "RootResourceCommand.unwatchResources expects an onAvailable function as argument"
+ );
+ }
+
+ for (const type of resources) {
+ if (!this._isValidResourceType(type)) {
+ throw new Error(
+ `RootResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ const allWatchers = [...this._watchers, ...this._pendingWatchers];
+ for (const watcherEntry of allWatchers) {
+ if (watcherEntry.onAvailable == onAvailable) {
+ watcherEntry.resources = watcherEntry.resources.filter(resourceType => {
+ return !resources.includes(resourceType);
+ });
+ }
+ }
+ this._watchers = this._watchers.filter(entry => {
+ return !!entry.resources.length;
+ });
+
+ for (const resource of resources) {
+ const isResourceWatched = allWatchers.some(watcherEntry =>
+ watcherEntry.resources.includes(resource)
+ );
+
+ if (!isResourceWatched && this._listenedResources.has(resource)) {
+ this._stopListening(resource);
+ }
+ }
+ }
+
+ clearResources(resourceTypes) {
+ if (!Array.isArray(resourceTypes)) {
+ throw new Error("clearResources expects an array of resource types");
+ }
+ // Clear the cached resources of the type.
+ this._cache = this._cache.filter(
+ cachedResource => !resourceTypes.includes(cachedResource.resourceType)
+ );
+
+ if (resourceTypes.length) {
+ this.rootFront.clearResources(resourceTypes);
+ }
+ }
+
+ async waitForNextResource(
+ resourceType,
+ { ignoreExistingResources = false, predicate } = {}
+ ) {
+ predicate = predicate || (resource => !!resource);
+
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+ const onAvailable = async resources => {
+ const matchingResource = resources.find(resource => predicate(resource));
+ if (matchingResource) {
+ this.unwatchResources([resourceType], { onAvailable });
+ resolve(matchingResource);
+ }
+ };
+
+ await this.watchResources([resourceType], {
+ ignoreExistingResources,
+ onAvailable,
+ });
+ return { onResource: promise };
+ }
+
+ async _onResourceAvailable(resources) {
+ for (const resource of resources) {
+ const { resourceType } = resource;
+
+ resource.isAlreadyExistingResource =
+ this._processingExistingResources.has(resourceType);
+
+ this._queueResourceEvent("available", resourceType, resource);
+
+ this._cache.push(resource);
+ }
+
+ this._throttledNotifyWatchers();
+ }
+
+ async _onResourceDestroyed(resources) {
+ for (const resource of resources) {
+ const { resourceType, resourceId } = resource;
+
+ let index = -1;
+ if (resourceId) {
+ index = this._cache.findIndex(
+ cachedResource =>
+ cachedResource.resourceType == resourceType &&
+ cachedResource.resourceId == resourceId
+ );
+ } else {
+ index = this._cache.indexOf(resource);
+ }
+ if (index >= 0) {
+ this._cache.splice(index, 1);
+ } else {
+ console.warn(
+ `Resource ${resourceId || ""} of ${resourceType} was not found.`
+ );
+ }
+
+ this._queueResourceEvent("destroyed", resourceType, resource);
+ }
+ this._throttledNotifyWatchers();
+ }
+
+ _queueResourceEvent(callbackType, resourceType, update) {
+ for (const { resources, pendingEvents } of this._watchers) {
+ if (!resources.includes(resourceType)) {
+ continue;
+ }
+ if (pendingEvents.length) {
+ const lastEvent = pendingEvents[pendingEvents.length - 1];
+ if (lastEvent.callbackType == callbackType) {
+ lastEvent.updates.push(update);
+ continue;
+ }
+ }
+ pendingEvents.push({
+ callbackType,
+ updates: [update],
+ });
+ }
+ }
+
+ _notifyWatchers() {
+ for (const watcherEntry of this._watchers) {
+ const { onAvailable, onDestroyed, pendingEvents } = watcherEntry;
+ watcherEntry.pendingEvents = [];
+
+ for (const { callbackType, updates } of pendingEvents) {
+ try {
+ if (callbackType == "available") {
+ onAvailable(updates, { areExistingResources: false });
+ } else if (callbackType == "destroyed" && onDestroyed) {
+ onDestroyed(updates);
+ }
+ } catch (e) {
+ console.error(
+ "Exception while calling a RootResourceCommand",
+ callbackType,
+ "callback",
+ ":",
+ e
+ );
+ }
+ }
+ }
+ }
+
+ _isValidResourceType(type) {
+ return this.ALL_TYPES.includes(type);
+ }
+
+ async _startListening(resourceType) {
+ if (this._listenedResources.has(resourceType)) {
+ return;
+ }
+ this._listenedResources.add(resourceType);
+
+ this._processingExistingResources.add(resourceType);
+
+ // For now, if the server doesn't support the resource type
+ // act as if we were listening, but do nothing.
+ // Calling watchResources/unwatchResources will work fine,
+ // but no resource will be notified.
+ if (this.rootFront.traits.resources?.[resourceType]) {
+ await this.rootFront.watchResources([resourceType]);
+ } else {
+ console.warn(
+ `Ignored watchRequest, resourceType "${resourceType}" not found in rootFront.traits.resources`
+ );
+ }
+ this._processingExistingResources.delete(resourceType);
+ }
+
+ async _forwardExistingResources(resourceTypes, onAvailable) {
+ const existingResources = this._cache.filter(resource =>
+ resourceTypes.includes(resource.resourceType)
+ );
+ if (existingResources.length) {
+ await onAvailable(existingResources, { areExistingResources: true });
+ }
+ }
+
+ _stopListening(resourceType) {
+ if (!this._listenedResources.has(resourceType)) {
+ throw new Error(
+ `Stopped listening for resource '${resourceType}' that isn't being listened to`
+ );
+ }
+ this._listenedResources.delete(resourceType);
+
+ this._cache = this._cache.filter(
+ cachedResource => cachedResource.resourceType !== resourceType
+ );
+
+ if (
+ !this.rootFront.isDestroyed() &&
+ this.rootFront.traits.resources?.[resourceType]
+ ) {
+ this.rootFront.unwatchResources([resourceType]);
+ }
+ }
+}
+
+RootResourceCommand.TYPES = RootResourceCommand.prototype.TYPES = {
+ EXTENSIONS_BGSCRIPT_STATUS: "extensions-backgroundscript-status",
+};
+RootResourceCommand.ALL_TYPES = RootResourceCommand.prototype.ALL_TYPES =
+ Object.values(RootResourceCommand.TYPES);
+module.exports = RootResourceCommand;
diff --git a/devtools/shared/commands/script/moz.build b/devtools/shared/commands/script/moz.build
new file mode 100644
index 0000000000..70570b2599
--- /dev/null
+++ b/devtools/shared/commands/script/moz.build
@@ -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/.
+
+DevToolsModules(
+ "script-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/script/script-command.js b/devtools/shared/commands/script/script-command.js
new file mode 100644
index 0000000000..cf5a7e263e
--- /dev/null
+++ b/devtools/shared/commands/script/script-command.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ getAdHocFrontOrPrimitiveGrip,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/fronts/object.js");
+
+class ScriptCommand {
+ constructor({ commands }) {
+ this._commands = commands;
+ }
+
+ /**
+ * Execute a JavaScript expression.
+ *
+ * @param {String} expression: The code you want to evaluate.
+ * @param {Object} options: Options for evaluation:
+ * @param {Object} options.frameActor: a FrameActor ID. The actor holds a reference to
+ * a Debugger.Frame. This option allows you to evaluate the string in the frame
+ * of the given FrameActor.
+ * @param {String} options.url: the url to evaluate the script as. Defaults to "debugger eval code".
+ * @param {TargetFront} options.selectedTargetFront: When passed, the expression will be
+ * evaluated in the context of the target (as opposed to the default, top-level one).
+ * @param {String} options.selectedNodeActor: A NodeActor ID that may be used by helper
+ * functions that can reference the currently selected node in the Inspector, like $0.
+ * @param {String} options.selectedObjectActor: the actorID of a given objectActor.
+ * This is used by context menu entries to get a reference to an object, in order
+ * to perform some operation on it (copy it, store it as a global variable, …).
+ * @param {Number} options.innerWindowID: An optional window id to be used for the evaluation,
+ * instead of the regular webConsoleActor.evalWindow.
+ * This is used by functions that may want to evaluate in a different window (for
+ * example a non-remote iframe), like getting the elements of a given document.
+ * @param {object} options.mapped: An optional object indicating if the original expression
+ * entered by the users have been modified
+ * @param {boolean} options.mapped.await: true if the expression was a top-level await
+ * expression that was wrapped in an async-iife
+ * @param {boolean} options.disableBreaks: Set to true to avoid triggering any
+ * type of breakpoint when evaluating the source. Also, the evaluated source won't be
+ * visible in the debugger UI.
+ * @param {boolean} options.preferConsoleCommandsOverLocalSymbols: Set to true to force
+ * overriding local symbols defined by the page with same-name console commands.
+ *
+ * @return {Promise}: A promise that resolves with the response.
+ */
+ async execute(expression, options = {}) {
+ const {
+ selectedObjectActor,
+ selectedNodeActor,
+ frameActor,
+ selectedTargetFront,
+ } = options;
+
+ // Retrieve the right WebConsole front that relates either to (by order of priority):
+ // - the currently selected target in the context selector
+ // (selectedTargetFront argument),
+ // - the object picked in the console (when using store as global) (selectedObjectActor),
+ // - the currently selected Node in the inspector (selectedNodeActor),
+ // - the currently selected frame in the debugger (when paused) (frameActor),
+ // - the currently selected target in the iframe dropdown
+ // (selectedTargetFront from the TargetCommand)
+ let targetFront = this._commands.targetCommand.selectedTargetFront;
+
+ const selectedActor =
+ selectedObjectActor || selectedNodeActor || frameActor;
+
+ if (selectedTargetFront) {
+ targetFront = selectedTargetFront;
+ } else if (selectedActor) {
+ const selectedFront = this._commands.client.getFrontByID(selectedActor);
+ if (selectedFront) {
+ targetFront = selectedFront.targetFront;
+ }
+ }
+
+ const consoleFront = await targetFront.getFront("console");
+
+ // We call `evaluateJSAsync` RDP request, which immediately returns a simple `resultID`,
+ // for which we later receive a related `evaluationResult` RDP event, with the same resultID.
+ // The evaluation result will be contained in this RDP event.
+ let resultID;
+ const response = await new Promise(resolve => {
+ const offEvaluationResult = consoleFront.on(
+ "evaluationResult",
+ async packet => {
+ // In some cases, the evaluationResult event can be received before the call to
+ // evaluationJSAsync completes. So make sure to wait for the corresponding promise
+ // before handling the evaluationResult event.
+ await onEvaluateJSAsync;
+
+ if (packet.resultID === resultID) {
+ resolve(packet);
+ offEvaluationResult();
+ }
+ }
+ );
+
+ const onEvaluateJSAsync = consoleFront
+ .evaluateJSAsync({
+ text: expression,
+ eager: options.eager,
+ frameActor,
+ innerWindowID: options.innerWindowID,
+ mapped: options.mapped,
+ selectedNodeActor,
+ selectedObjectActor,
+ url: options.url,
+ disableBreaks: options.disableBreaks,
+ preferConsoleCommandsOverLocalSymbols:
+ options.preferConsoleCommandsOverLocalSymbols,
+ })
+ .then(packet => {
+ resultID = packet.resultID;
+ });
+ });
+
+ // `response` is the packet sent via `evaluationResult` RDP event.
+ if (response.error) {
+ throw response;
+ }
+
+ if (response.result) {
+ response.result = getAdHocFrontOrPrimitiveGrip(
+ response.result,
+ consoleFront
+ );
+ }
+
+ if (response.helperResult?.object) {
+ response.helperResult.object = getAdHocFrontOrPrimitiveGrip(
+ response.helperResult.object,
+ consoleFront
+ );
+ }
+
+ if (response.exception) {
+ response.exception = getAdHocFrontOrPrimitiveGrip(
+ response.exception,
+ consoleFront
+ );
+ }
+
+ if (response.exceptionMessage) {
+ response.exceptionMessage = getAdHocFrontOrPrimitiveGrip(
+ response.exceptionMessage,
+ consoleFront
+ );
+ }
+
+ return response;
+ }
+}
+
+module.exports = ScriptCommand;
diff --git a/devtools/shared/commands/script/tests/browser.toml b/devtools/shared/commands/script/tests/browser.toml
new file mode 100644
index 0000000000..21b59c0ea3
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "head.js",
+]
+
+["browser_script_command_execute_basic.js"]
+
+["browser_script_command_execute_document__proto__.js"]
+
+["browser_script_command_execute_last_result.js"]
+
+["browser_script_command_execute_throw.js"]
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_basic.js b/devtools/shared/commands/script/tests/browser_script_command_execute_basic.js
new file mode 100644
index 0000000000..e63f55a338
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_basic.js
@@ -0,0 +1,1050 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing basic expression evaluation
+const {
+ MAX_AUTOCOMPLETE_ATTEMPTS,
+ MAX_AUTOCOMPLETIONS,
+} = require("resource://devtools/shared/webconsole/js-property-provider.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+add_task(async () => {
+ const tab = await addTab(`data:text/html;charset=utf-8,
+ <!DOCTYPE html>
+ <html dir="ltr" class="class1">
+ <head><title>Testcase</title></head>
+ <script>
+ window.foobarObject = Object.create(
+ null,
+ Object.getOwnPropertyDescriptors({
+ foo: 1,
+ foobar: 2,
+ foobaz: 3,
+ omg: 4,
+ omgfoo: 5,
+ strfoo: "foobarz",
+ omgstr: "foobarz" + "abb".repeat(${DevToolsServer.LONG_STRING_LENGTH} * 2),
+ })
+ );
+
+ window.largeObject1 = Object.create(null);
+ for (let i = 0; i < ${MAX_AUTOCOMPLETE_ATTEMPTS} + 1; i++) {
+ window.largeObject1["a" + i] = i;
+ }
+
+ window.largeObject2 = Object.create(null);
+ for (let i = 0; i < ${MAX_AUTOCOMPLETIONS} * 2; i++) {
+ window.largeObject2["a" + i] = i;
+ }
+
+ var originalExec = RegExp.prototype.exec;
+
+ var promptIterable = { [Symbol.iterator]() { return { next: prompt } } };
+
+ function aliasedTest() {
+ const aliased = "ALIASED";
+ return [0].map(() => aliased)[0];
+ }
+
+ var testMap = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ var testSet = new Set([1, 2, 3, 4, 5]);
+ var testProxy = new Proxy({}, { getPrototypeOf: prompt });
+ var testArray = [1,2,3];
+ var testInt8Array = new Int8Array([1, 2, 3]);
+ var testArrayBuffer = testInt8Array.buffer;
+ var testDataView = new DataView(testArrayBuffer, 2);
+
+ var testCanvasContext = document.createElement("canvas").getContext("2d");
+
+ var objWithNativeGetter = {};
+ Object.defineProperty(objWithNativeGetter, "print", { get: print });
+ Object.defineProperty(objWithNativeGetter, "Element", { get: Element });
+ Object.defineProperty(objWithNativeGetter, "setAttribute", { get: Element.prototype.setAttribute });
+ Object.defineProperty(objWithNativeGetter, "setClassName", { get: Object.getOwnPropertyDescriptor(Element.prototype, "className").set });
+ Object.defineProperty(objWithNativeGetter, "requestPermission", { get: Notification.requestPermission });
+
+ async function testAsync() { return 10; }
+ async function testAsyncAwait() { await 1; return 10; }
+ async function * testAsyncGen() { return 10; }
+ async function * testAsyncGenAwait() { await 1; return 10; }
+
+ function testFunc() {}
+
+ var testLocale = new Intl.Locale("de-latn-de-u-ca-gregory-co-phonebk-hc-h23-kf-true-kn-false-nu-latn");
+ </script>
+ <body id="body1" class="class2"><h1>Body text</h1></body>
+ </html>`);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ await doSimpleEval(commands);
+ await doWindowEval(commands);
+ await doEvalWithException(commands);
+ await doEvalWithHelper(commands);
+ await doEvalString(commands);
+ await doEvalLongString(commands);
+ await doEvalWithBinding(commands);
+ await forceLexicalInit(commands);
+ await doSimpleEagerEval(commands);
+ await doEagerEvalWithSideEffect(commands);
+ await doEagerEvalWithSideEffectIterator(commands);
+ await doEagerEvalWithSideEffectMonkeyPatched(commands);
+ await doEagerEvalESGetters(commands);
+ await doEagerEvalDOMGetters(commands);
+ await doEagerEvalOtherNativeGetters(commands);
+ await doEagerEvalAsyncFunctions(commands);
+
+ await commands.destroy();
+});
+
+async function doSimpleEval(commands) {
+ info("test eval '2+2'");
+ const response = await commands.scriptCommand.execute("2+2");
+ checkObject(response, {
+ input: "2+2",
+ result: 4,
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doWindowEval(commands) {
+ info("test eval 'document'");
+ const response = await commands.scriptCommand.execute("document");
+ checkObject(response, {
+ input: "document",
+ result: {
+ type: "object",
+ class: "HTMLDocument",
+ actor: /[a-z]/,
+ },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doEvalWithException(commands) {
+ info("test eval with exception");
+ const response = await commands.scriptCommand.execute(
+ "window.doTheImpossible()"
+ );
+ checkObject(response, {
+ input: "window.doTheImpossible()",
+ result: {
+ type: "undefined",
+ },
+ exceptionMessage: /doTheImpossible/,
+ });
+
+ ok(response.exception, "js eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doEvalWithHelper(commands) {
+ info("test eval with helper");
+ const response = await commands.scriptCommand.execute("clear()");
+ checkObject(response, {
+ input: "clear()",
+ result: {
+ type: "undefined",
+ },
+ helperResult: { type: "clearOutput" },
+ });
+
+ ok(!response.exception, "no eval exception");
+}
+
+async function doEvalString(commands) {
+ const response = await commands.scriptCommand.execute(
+ "window.foobarObject.strfoo"
+ );
+
+ checkObject(response, {
+ input: "window.foobarObject.strfoo",
+ result: "foobarz",
+ });
+}
+
+async function doEvalLongString(commands) {
+ const response = await commands.scriptCommand.execute(
+ "window.foobarObject.omgstr"
+ );
+
+ const str = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.wrappedJSObject.foobarObject.omgstr;
+ }
+ );
+
+ const initial = str.substring(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
+
+ checkObject(response, {
+ input: "window.foobarObject.omgstr",
+ result: {
+ type: "longString",
+ initial,
+ length: str.length,
+ },
+ });
+}
+
+async function doEvalWithBinding(commands) {
+ const response = await commands.scriptCommand.execute("document;");
+ const documentActor = response.result.actorID;
+
+ info("running a command with _self as document using selectedObjectActor");
+ const selectedObjectSame = await commands.scriptCommand.execute(
+ "_self === document",
+ {
+ selectedObjectActor: documentActor,
+ }
+ );
+ checkObject(selectedObjectSame, {
+ result: true,
+ });
+}
+
+async function forceLexicalInit(commands) {
+ info("test that failed let/const bindings are initialized to undefined");
+
+ const testData = [
+ {
+ stmt: "let foopie = wubbalubadubdub",
+ vars: ["foopie"],
+ },
+ {
+ stmt: "let {z, w={n}=null} = {}",
+ vars: ["z", "w"],
+ },
+ {
+ stmt: "let [a, b, c] = null",
+ vars: ["a", "b", "c"],
+ },
+ {
+ stmt: "const nein1 = rofl, nein2 = copter",
+ vars: ["nein1", "nein2"],
+ },
+ {
+ stmt: "const {ha} = null",
+ vars: ["ha"],
+ },
+ {
+ stmt: "const [haw=[lame]=null] = []",
+ vars: ["haw"],
+ },
+ {
+ stmt: "const [rawr, wat=[lame]=null] = []",
+ vars: ["rawr", "haw"],
+ },
+ {
+ stmt: "let {zzz: xyz=99, zwz: wb} = nexistepas()",
+ vars: ["xyz", "wb"],
+ },
+ {
+ stmt: "let {c3pdoh=101} = null",
+ vars: ["c3pdoh"],
+ },
+ {
+ stmt: "const {...x} = x",
+ vars: ["x"],
+ },
+ {
+ stmt: "const {xx,yy,...rest} = null",
+ vars: ["xx", "yy", "rest"],
+ },
+ ];
+
+ for (const data of testData) {
+ const response = await commands.scriptCommand.execute(data.stmt);
+ checkObject(response, {
+ input: data.stmt,
+ result: { type: "undefined" },
+ });
+ ok(response.exception, "expected exception");
+ for (const varName of data.vars) {
+ const response2 = await commands.scriptCommand.execute(varName);
+ checkObject(response2, {
+ input: varName,
+ result: { type: "undefined" },
+ });
+ ok(!response2.exception, "unexpected exception");
+ }
+ }
+}
+
+async function doSimpleEagerEval(commands) {
+ const testData = [
+ {
+ code: "2+2",
+ result: 4,
+ },
+ {
+ code: "(x => x * 2)(3)",
+ result: 6,
+ },
+ {
+ code: "[1, 2, 3].map(x => x * 2).join()",
+ result: "2,4,6",
+ },
+ {
+ code: `"abc".match(/a./)[0]`,
+ result: "ab",
+ },
+ {
+ code: "aliasedTest()",
+ result: "ALIASED",
+ },
+ {
+ code: "testArray.concat([4,5]).join()",
+ result: "1,2,3,4,5",
+ },
+ {
+ code: "testArray.entries().toString()",
+ result: "[object Array Iterator]",
+ },
+ {
+ code: "testArray.keys().toString()",
+ result: "[object Array Iterator]",
+ },
+ {
+ code: "testArray.values().toString()",
+ result: "[object Array Iterator]",
+ },
+ {
+ code: "testArray.every(x => x < 100)",
+ result: true,
+ },
+ {
+ code: "testArray.some(x => x > 1)",
+ result: true,
+ },
+ {
+ code: "testArray.filter(x => x % 2 == 0).join()",
+ result: "2",
+ },
+ {
+ code: "testArray.find(x => x % 2 == 0)",
+ result: 2,
+ },
+ {
+ code: "testArray.findIndex(x => x % 2 == 0)",
+ result: 1,
+ },
+ {
+ code: "[testArray].flat().join()",
+ result: "1,2,3",
+ },
+ {
+ code: "[testArray].flatMap(x => x).join()",
+ result: "1,2,3",
+ },
+ {
+ code: "testArray.forEach(x => x); testArray.join()",
+ result: "1,2,3",
+ },
+ {
+ code: "testArray.includes(1)",
+ result: true,
+ },
+ {
+ code: "testArray.lastIndexOf(1)",
+ result: 0,
+ },
+ {
+ code: "testArray.map(x => x + 1).join()",
+ result: "2,3,4",
+ },
+ {
+ code: "testArray.reduce((acc,x) => acc + x, 0)",
+ result: 6,
+ },
+ {
+ code: "testArray.reduceRight((acc,x) => acc + x, 0)",
+ result: 6,
+ },
+ {
+ code: "testArray.slice(0,1).join()",
+ result: "1",
+ },
+ {
+ code: "testArray.toReversed().join()",
+ result: "3,2,1",
+ },
+ {
+ code: "testArray.toSorted().join()",
+ result: "1,2,3",
+ },
+ {
+ code: "testArray.toSpliced(0,1).join()",
+ result: "2,3",
+ },
+ {
+ code: "testArray.with(1, 'b').join()",
+ result: "1,b,3",
+ },
+
+ {
+ code: "testInt8Array.entries().toString()",
+ result: "[object Array Iterator]",
+ },
+ {
+ code: "testInt8Array.keys().toString()",
+ result: "[object Array Iterator]",
+ },
+ {
+ code: "testInt8Array.values().toString()",
+ result: "[object Array Iterator]",
+ },
+ {
+ code: "testInt8Array.every(x => x < 100)",
+ result: true,
+ },
+ {
+ code: "testInt8Array.some(x => x > 1)",
+ result: true,
+ },
+ {
+ code: "testInt8Array.filter(x => x % 2 == 0).join()",
+ result: "2",
+ },
+ {
+ code: "testInt8Array.find(x => x % 2 == 0)",
+ result: 2,
+ },
+ {
+ code: "testInt8Array.findIndex(x => x % 2 == 0)",
+ result: 1,
+ },
+ {
+ code: "testInt8Array.forEach(x => x); testInt8Array.join()",
+ result: "1,2,3",
+ },
+ {
+ code: "testInt8Array.includes(1)",
+ result: true,
+ },
+ {
+ code: "testInt8Array.lastIndexOf(1)",
+ result: 0,
+ },
+ {
+ code: "testInt8Array.map(x => x + 1).join()",
+ result: "2,3,4",
+ },
+ {
+ code: "testInt8Array.reduce((acc,x) => acc + x, 0)",
+ result: 6,
+ },
+ {
+ code: "testInt8Array.reduceRight((acc,x) => acc + x, 0)",
+ result: 6,
+ },
+ {
+ code: "testInt8Array.slice(0,1).join()",
+ result: "1",
+ },
+ {
+ code: "testInt8Array.toReversed().join()",
+ skip:
+ typeof Reflect.getPrototypeOf(Int8Array).prototype.toReversed !==
+ "function",
+ result: "3,2,1",
+ },
+ {
+ code: "testInt8Array.toSorted().join()",
+ skip:
+ typeof Reflect.getPrototypeOf(Int8Array).prototype.toSorted !==
+ "function",
+ result: "1,2,3",
+ },
+ {
+ code: "testInt8Array.with(1, 0).join()",
+ skip:
+ typeof Reflect.getPrototypeOf(Int8Array).prototype.with !== "function",
+ result: "1,0,3",
+ },
+ ];
+
+ for (const { code, result, skip } of testData) {
+ if (skip) {
+ info(`Skipping evaluation of ${code}`);
+ continue;
+ }
+
+ info(`Evaluating: ${code}`);
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(response, {
+ input: code,
+ result,
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalWithSideEffect(commands) {
+ const testData = [
+ // Modify environment.
+ "var a = 10; a;",
+
+ // Directly call a funtion with side effect.
+ "prompt();",
+
+ // Call a funtion with side effect inside a scripted function.
+ "(() => { prompt(); })()",
+
+ // Call a funtion with side effect from self-hosted JS function.
+ "[1, 2, 3].map(prompt)",
+
+ // Call a function with Function.prototype.call.
+ "Function.prototype.call.bind(Function.prototype.call)(prompt);",
+
+ // Call a function with Function.prototype.apply.
+ "Function.prototype.apply.bind(Function.prototype.apply)(prompt);",
+
+ // Indirectly call a function with Function.prototype.apply.
+ "Reflect.apply(prompt, null, []);",
+ "'aaaaaaaa'.replace(/(a)(a)(a)(a)(a)(a)(a)(a)/, prompt)",
+
+ // Indirect call on obj[Symbol.iterator]().next.
+ "Array.from(promptIterable)",
+ ];
+
+ for (const code of testData) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(response, {
+ input: code,
+ result: { type: "undefined" },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalWithSideEffectIterator(commands) {
+ // Indirect call on %ArrayIterator%.prototype.next,
+
+ // Create an iterable object that reuses iterator across multiple call.
+ let response = await commands.scriptCommand.execute(`
+var arr = [1, 2, 3];
+var iterator = arr[Symbol.iterator]();
+var iterable = { [Symbol.iterator]() { return iterator; } };
+"ok";
+`);
+ checkObject(response, {
+ result: "ok",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ const testData = [
+ "Array.from(iterable)",
+ "new Map(iterable)",
+ "new Set(iterable)",
+ ];
+
+ for (const code of testData) {
+ response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(response, {
+ input: code,
+ result: { type: "undefined" },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+
+ // Verify the iterator's internal state isn't modified.
+ response = await commands.scriptCommand.execute(`[...iterator].join(",")`);
+ checkObject(response, {
+ result: "1,2,3",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doEagerEvalWithSideEffectMonkeyPatched(commands) {
+ // Patch the built-in function without eager evaluation.
+ let response = await commands.scriptCommand.execute(
+ `RegExp.prototype.exec = prompt; "patched"`
+ );
+ checkObject(response, {
+ result: "patched",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ // Test eager evaluation, where the patched built-in is called internally.
+ // This should be aborted.
+ const code = `"abc".match(/a./)[0]`;
+ response = await commands.scriptCommand.execute(code, { eager: true });
+ checkObject(response, {
+ input: code,
+ result: { type: "undefined" },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ // Undo the patch without eager evaluation.
+ response = await commands.scriptCommand.execute(
+ `RegExp.prototype.exec = originalExec; "unpatched"`
+ );
+ checkObject(response, {
+ result: "unpatched",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ // Test eager evaluation again, without the patch.
+ // This should be evaluated.
+ response = await commands.scriptCommand.execute(code, { eager: true });
+ checkObject(response, {
+ input: code,
+ result: "ab",
+ });
+}
+
+async function doEagerEvalESGetters(commands) {
+ // [code, expectedResult]
+ const testData = [
+ // ArrayBuffer
+ ["testArrayBuffer.byteLength", 3],
+
+ // DataView
+ ["testDataView.buffer === testArrayBuffer", true],
+ ["testDataView.byteLength", 1],
+ ["testDataView.byteOffset", 2],
+
+ // Error
+ ["typeof new Error().stack", "string"],
+
+ // Function
+ ["typeof testFunc.arguments", "object"],
+ ["typeof testFunc.caller", "object"],
+
+ // Intl.Locale
+ ["testLocale.baseName", "de-Latn-DE"],
+ ["testLocale.calendar", "gregory"],
+ ["testLocale.caseFirst", ""],
+ ["testLocale.collation", "phonebk"],
+ ["testLocale.hourCycle", "h23"],
+ ["testLocale.numeric", false],
+ ["testLocale.numberingSystem", "latn"],
+ ["testLocale.language", "de"],
+ ["testLocale.script", "Latn"],
+ ["testLocale.region", "DE"],
+
+ // Map
+ ["testMap.size", 4],
+
+ // RegExp
+ ["/a/.dotAll", false],
+ ["/a/giy.flags", "giy"],
+ ["/a/g.global", true],
+ ["/a/g.hasIndices", false],
+ ["/a/g.ignoreCase", false],
+ ["/a/g.multiline", false],
+ ["/a/g.source", "a"],
+ ["/a/g.sticky", false],
+ ["/a/g.unicode", false],
+
+ // Set
+ ["testSet.size", 5],
+
+ // Symbol
+ ["Symbol.iterator.description", "Symbol.iterator"],
+
+ // TypedArray
+ ["testInt8Array.buffer === testArrayBuffer", true],
+ ["testInt8Array.byteLength", 3],
+ ["testInt8Array.byteOffset", 0],
+ ["testInt8Array.length", 3],
+ ["testInt8Array[Symbol.toStringTag]", "Int8Array"],
+ ];
+
+ for (const [code, expectedResult] of testData) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: expectedResult,
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+
+ // Test RegExp static properties.
+ // Run preparation code here to avoid interference with other tests,
+ // given RegExp static properties are global state.
+ const regexpPreparationCode = `
+/b(c)(d)(e)(f)(g)(h)(i)(j)(k)l/.test("abcdefghijklm")
+`;
+
+ const prepResponse = await commands.scriptCommand.execute(
+ regexpPreparationCode
+ );
+ checkObject(prepResponse, {
+ input: regexpPreparationCode,
+ result: true,
+ });
+
+ ok(!prepResponse.exception, "no eval exception");
+ ok(!prepResponse.helperResult, "no helper result");
+
+ const testDataRegExp = [
+ // RegExp static
+ ["RegExp.input", "abcdefghijklm"],
+ ["RegExp.lastMatch", "bcdefghijkl"],
+ ["RegExp.lastParen", "k"],
+ ["RegExp.leftContext", "a"],
+ ["RegExp.rightContext", "m"],
+ ["RegExp.$1", "c"],
+ ["RegExp.$2", "d"],
+ ["RegExp.$3", "e"],
+ ["RegExp.$4", "f"],
+ ["RegExp.$5", "g"],
+ ["RegExp.$6", "h"],
+ ["RegExp.$7", "i"],
+ ["RegExp.$8", "j"],
+ ["RegExp.$9", "k"],
+ ["RegExp.$_", "abcdefghijklm"], // input
+ ["RegExp['$&']", "bcdefghijkl"], // lastMatch
+ ["RegExp['$+']", "k"], // lastParen
+ ["RegExp['$`']", "a"], // leftContext
+ ["RegExp[`$'`]", "m"], // rightContext
+ ];
+
+ for (const [code, expectedResult] of testDataRegExp) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: expectedResult,
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+
+ const testDataWithSideEffect = [
+ // get Object.prototype.__proto__
+ //
+ // This can invoke Proxy getPrototypeOf handler, which can be any native
+ // function, and debugger cannot hook the call.
+ `[].__proto__`,
+ `testProxy.__proto__`,
+ ];
+
+ for (const code of testDataWithSideEffect) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: { type: "undefined" },
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalDOMGetters(commands) {
+ // Getters explicitly marked no-side-effect.
+ //
+ // [code, expectedResult]
+ const testDataExplicit = [
+ // DOMTokenList
+ ["document.documentElement.classList.length", 1],
+ ["document.documentElement.classList.value", "class1"],
+
+ // Document
+ ["document.URL.startsWith('data:')", true],
+ ["document.documentURI.startsWith('data:')", true],
+ ["document.compatMode", "CSS1Compat"],
+ ["document.characterSet", "UTF-8"],
+ ["document.charset", "UTF-8"],
+ ["document.inputEncoding", "UTF-8"],
+ ["document.contentType", "text/html"],
+ ["document.doctype.constructor.name", "DocumentType"],
+ ["document.documentElement.constructor.name", "HTMLHtmlElement"],
+ ["document.title", "Testcase"],
+ ["document.dir", "ltr"],
+ ["document.body.constructor.name", "HTMLBodyElement"],
+ ["document.head.constructor.name", "HTMLHeadElement"],
+ ["document.images.constructor.name", "HTMLCollection"],
+ ["document.embeds.constructor.name", "HTMLCollection"],
+ ["document.plugins.constructor.name", "HTMLCollection"],
+ ["document.links.constructor.name", "HTMLCollection"],
+ ["document.forms.constructor.name", "HTMLCollection"],
+ ["document.scripts.constructor.name", "HTMLCollection"],
+ ["document.defaultView === window", true],
+ ["typeof document.currentScript", "object"],
+ ["document.anchors.constructor.name", "HTMLCollection"],
+ ["document.applets.constructor.name", "HTMLCollection"],
+ ["document.all.constructor.name", "HTMLAllCollection"],
+ ["document.styleSheetSets.constructor.name", "DOMStringList"],
+ ["typeof document.featurePolicy", "undefined"],
+ ["typeof document.blockedNodeByClassifierCount", "undefined"],
+ ["typeof document.blockedNodesByClassifier", "undefined"],
+ ["typeof document.permDelegateHandler", "undefined"],
+ ["document.children.constructor.name", "HTMLCollection"],
+ ["document.firstElementChild === document.documentElement", true],
+ ["document.lastElementChild === document.documentElement", true],
+ ["document.childElementCount", 1],
+ ["document.location.href.startsWith('data:')", true],
+
+ // Element
+ ["document.body.namespaceURI", "http://www.w3.org/1999/xhtml"],
+ ["document.body.prefix === null", true],
+ ["document.body.localName", "body"],
+ ["document.body.tagName", "BODY"],
+ ["document.body.id", "body1"],
+ ["document.body.className", "class2"],
+ ["document.body.classList.constructor.name", "DOMTokenList"],
+ ["document.body.part.constructor.name", "DOMTokenList"],
+ ["document.body.attributes.constructor.name", "NamedNodeMap"],
+ ["document.body.innerHTML.includes('Body text')", true],
+ ["document.body.outerHTML.includes('Body text')", true],
+ ["document.body.previousElementSibling !== null", true],
+ ["document.body.nextElementSibling === null", true],
+ ["document.body.children.constructor.name", "HTMLCollection"],
+ ["document.body.firstElementChild !== null", true],
+ ["document.body.lastElementChild !== null", true],
+ ["document.body.childElementCount", 1],
+
+ // Node
+ ["document.body.nodeType === Node.ELEMENT_NODE", true],
+ ["document.body.nodeName", "BODY"],
+ ["document.body.baseURI.startsWith('data:')", true],
+ ["document.body.isConnected", true],
+ ["document.body.ownerDocument === document", true],
+ ["document.body.parentNode === document.documentElement", true],
+ ["document.body.parentElement === document.documentElement", true],
+ ["document.body.childNodes.constructor.name", "NodeList"],
+ ["document.body.firstChild !== null", true],
+ ["document.body.lastChild !== null", true],
+ ["document.body.previousSibling !== null", true],
+ ["document.body.nextSibling === null", true],
+ ["document.body.nodeValue === null", true],
+ ["document.body.textContent.includes('Body text')", true],
+ ["typeof document.body.flattenedTreeParentNode", "undefined"],
+ ["typeof document.body.isNativeAnonymous", "undefined"],
+ ["typeof document.body.containingShadowRoot", "undefined"],
+ ["typeof document.body.accessibleNode", "undefined"],
+
+ // Performance
+ ["performance.timeOrigin > 0", true],
+ ["performance.timing.constructor.name", "PerformanceTiming"],
+ ["performance.navigation.constructor.name", "PerformanceNavigation"],
+ ["performance.eventCounts.constructor.name", "EventCounts"],
+
+ // window
+ ["window.window === window", true],
+ ["window.self === window", true],
+ ["window.document.constructor.name", "HTMLDocument"],
+ ["window.performance.constructor.name", "Performance"],
+ ["typeof window.browsingContext", "undefined"],
+ ["typeof window.windowUtils", "undefined"],
+ ["typeof window.windowGlobalChild", "undefined"],
+ ["window.visualViewport.constructor.name", "VisualViewport"],
+ ["typeof window.caches", "undefined"],
+ ["window.location.href.startsWith('data:')", true],
+ ];
+ if (typeof Scheduler === "function") {
+ // Scheduler is behind a pref.
+ testDataExplicit.push(["window.scheduler.constructor.name", "Scheduler"]);
+ }
+
+ for (const [code, expectedResult] of testDataExplicit) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: expectedResult,
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+
+ // Getters not-explicitly marked no-side-effect.
+ // All DOM getters are considered no-side-effect in eager evaluation context.
+ const testDataImplicit = [
+ // NOTE: This is not an exhaustive list.
+ // Document
+ [`document.implementation.constructor.name`, "DOMImplementation"],
+ [`typeof document.domain`, "string"],
+ [`typeof document.referrer`, "string"],
+ [`typeof document.cookie`, "string"],
+ [`typeof document.lastModified`, "string"],
+ [`typeof document.readyState`, "string"],
+ [`typeof document.designMode`, "string"],
+ [`typeof document.onbeforescriptexecute`, "object"],
+ [`typeof document.onafterscriptexecute`, "object"],
+
+ // Element
+ [`typeof document.documentElement.scrollTop`, "number"],
+ [`typeof document.documentElement.scrollLeft`, "number"],
+ [`typeof document.documentElement.scrollWidth`, "number"],
+ [`typeof document.documentElement.scrollHeight`, "number"],
+
+ // Performance
+ [`typeof performance.onresourcetimingbufferfull`, "object"],
+
+ // window
+ [`typeof window.name`, "string"],
+ [`window.history.constructor.name`, "History"],
+ [`window.customElements.constructor.name`, "CustomElementRegistry"],
+ [`window.locationbar.constructor.name`, "BarProp"],
+ [`window.menubar.constructor.name`, "BarProp"],
+ [`typeof window.status`, "string"],
+ [`window.closed`, false],
+
+ // CanvasRenderingContext2D / CanvasCompositing
+ [`testCanvasContext.globalAlpha`, 1],
+ ];
+
+ for (const [code, expectedResult] of testDataImplicit) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: expectedResult,
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalOtherNativeGetters(commands) {
+ // DOM getter functions are allowed to be eagerly-evaluated.
+ // Test the situation where non-DOM-getter function is called by accessing
+ // getter.
+ //
+ // "being a DOM getter" is tested by checking if the native function has
+ // JSJitInfo and it's marked as getter.
+ const testData = [
+ // Has no JitInfo.
+ "objWithNativeGetter.print",
+ "objWithNativeGetter.Element",
+
+ // Not marked as getter, but method.
+ "objWithNativeGetter.getAttribute",
+
+ // Not marked as getter, but setter.
+ "objWithNativeGetter.setClassName",
+
+ // Not marked as getter, but static method.
+ "objWithNativeGetter.requestPermission",
+ ];
+
+ for (const code of testData) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: { type: "undefined" },
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalAsyncFunctions(commands) {
+ // [code, expectedResult]
+ const testData = [["typeof testAsync()", "object"]];
+
+ for (const [code, expectedResult] of testData) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: expectedResult,
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+
+ const testDataWithSideEffect = [
+ // await is effectful
+ "testAsyncAwait()",
+
+ // initial yield is effectful
+ "testAsyncGen()",
+ "testAsyncGenAwait()",
+ ];
+
+ for (const code of testDataWithSideEffect) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(
+ response,
+ {
+ input: code,
+ result: { type: "undefined" },
+ },
+ code
+ );
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js b/devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js
new file mode 100644
index 0000000000..28f56ebac3
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing evaluating document.__proto__
+
+add_task(async () => {
+ const tab = await addTab(
+ `data:text/html;charset=utf-8,Test evaluating document.__proto__`
+ );
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const evaluationResponse = await commands.scriptCommand.execute(
+ "document.__proto__"
+ );
+ checkObject(evaluationResponse, {
+ input: "document.__proto__",
+ result: {
+ type: "object",
+ actor: /[a-z]/,
+ },
+ });
+
+ ok(!evaluationResponse.exception, "no eval exception");
+ ok(!evaluationResponse.helperResult, "no helper result");
+
+ const response = await evaluationResponse.result.getPrototypeAndProperties();
+ ok(!response.error, "no response error");
+
+ const props = response.ownProperties;
+ ok(props, "response properties available");
+
+ const expectedProps = Object.getOwnPropertyNames(
+ Object.getPrototypeOf(document)
+ );
+ checkObject(Object.keys(props), expectedProps, "Same own properties.");
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js b/devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js
new file mode 100644
index 0000000000..aebdaeb168
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing that last evaluation result can be accessed with `$_`
+
+add_task(async () => {
+ const tab = await addTab(`data:text/html;charset=utf-8,`);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ info("$_ returns undefined if nothing has evaluated yet");
+ let response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", { type: "undefined" });
+
+ info("$_ returns last value and performs basic arithmetic");
+ response = await commands.scriptCommand.execute("2+2");
+ basicResultCheck(response, "2+2", 4);
+
+ response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", 4);
+
+ response = await commands.scriptCommand.execute("$_ + 2");
+ basicResultCheck(response, "$_ + 2", 6);
+
+ response = await commands.scriptCommand.execute("$_ + 4");
+ basicResultCheck(response, "$_ + 4", 10);
+
+ info("$_ has correct references to objects");
+ response = await commands.scriptCommand.execute("var foo = {bar:1}; foo;");
+ basicResultCheck(response, "var foo = {bar:1}; foo;", {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ });
+ checkObject(response.result.getGrip().preview.ownProperties, {
+ bar: {
+ value: 1,
+ },
+ });
+
+ response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ });
+ checkObject(response.result.getGrip().preview.ownProperties, {
+ bar: {
+ value: 1,
+ },
+ });
+
+ info(
+ "Update a property value and check that evaluating $_ returns the expected object instance"
+ );
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.foo.bar = "updated_value";
+ });
+
+ response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ });
+ checkObject(response.result.getGrip().preview.ownProperties, {
+ bar: {
+ value: "updated_value",
+ },
+ });
+
+ await commands.destroy();
+});
+
+function basicResultCheck(response, input, output) {
+ checkObject(response, {
+ input,
+ result: output,
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_throw.js b/devtools/shared/commands/script/tests/browser_script_command_execute_throw.js
new file mode 100644
index 0000000000..8680193ecb
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_throw.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing evaluating thowing expressions
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+add_task(async () => {
+ const tab = await addTab(`data:text/html;charset=utf-8,Test throw`);
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const falsyValues = [
+ "-0",
+ "null",
+ "undefined",
+ "Infinity",
+ "-Infinity",
+ "NaN",
+ ];
+ for (const value of falsyValues) {
+ const response = await commands.scriptCommand.execute(`throw ${value};`);
+ is(
+ response.exception.type,
+ value,
+ `Got the expected value for response.exception.type when throwing "${value}"`
+ );
+ }
+
+ const identityTestValues = [false, 0];
+ for (const value of identityTestValues) {
+ const response = await commands.scriptCommand.execute(`throw ${value};`);
+ is(
+ response.exception,
+ value,
+ `Got the expected value for response.exception when throwing "${value}"`
+ );
+ }
+
+ const symbolTestValues = [
+ ["Symbol.iterator", "Symbol(Symbol.iterator)"],
+ ["Symbol('foo')", "Symbol(foo)"],
+ ["Symbol()", "Symbol()"],
+ ];
+ for (const [expr, message] of symbolTestValues) {
+ const response = await commands.scriptCommand.execute(`throw ${expr};`);
+ is(
+ response.exceptionMessage,
+ message,
+ `Got the expected value for response.exceptionMessage when throwing "${expr}"`
+ );
+ }
+
+ const longString = Array(DevToolsServer.LONG_STRING_LENGTH + 1).join("a"),
+ shortedString = longString.substring(
+ 0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ );
+ const response = await commands.scriptCommand.execute(
+ "throw '" + longString + "';"
+ );
+ is(
+ response.exception.initial,
+ shortedString,
+ "Got the expected value for exception.initial when throwing a longString"
+ );
+ is(
+ response.exceptionMessage.initial,
+ shortedString,
+ "Got the expected value for exceptionMessage.initial when throwing a longString"
+ );
+});
diff --git a/devtools/shared/commands/script/tests/head.js b/devtools/shared/commands/script/tests/head.js
new file mode 100644
index 0000000000..50635e4502
--- /dev/null
+++ b/devtools/shared/commands/script/tests/head.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";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+function checkObject(object, expected, message) {
+ if (object && object.getGrip) {
+ object = object.getGrip();
+ }
+
+ for (const name of Object.keys(expected)) {
+ const expectedValue = expected[name];
+ const value = object[name];
+ checkValue(name, value, expectedValue, message);
+ }
+}
+
+function checkValue(name, value, expected, message) {
+ if (message) {
+ message = ` for '${message}'`;
+ }
+
+ if (expected === null) {
+ is(value, null, `'${name}' is null${message}`);
+ } else if (expected === undefined) {
+ is(value, expected, `'${name}' is undefined${message}`);
+ } else if (
+ typeof expected == "string" ||
+ typeof expected == "number" ||
+ typeof expected == "boolean"
+ ) {
+ is(value, expected, "property '" + name + "'" + message);
+ } else if (expected instanceof RegExp) {
+ ok(
+ expected.test(value),
+ name + ": " + expected + " matched " + value + message
+ );
+ } else if (Array.isArray(expected)) {
+ info("checking array for property '" + name + "'" + message);
+ checkObject(value, expected, message);
+ } else if (typeof expected == "object") {
+ info("checking object for property '" + name + "'" + message);
+ checkObject(value, expected, message);
+ }
+}
diff --git a/devtools/shared/commands/target-configuration/moz.build b/devtools/shared/commands/target-configuration/moz.build
new file mode 100644
index 0000000000..c0929aee77
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/moz.build
@@ -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/.
+
+DevToolsModules(
+ "target-configuration-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/target-configuration/target-configuration-command.js b/devtools/shared/commands/target-configuration/target-configuration-command.js
new file mode 100644
index 0000000000..28e717cea2
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/target-configuration-command.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/. */
+
+"use strict";
+
+/**
+ * The TargetConfigurationCommand should be used to populate the DevTools server
+ * with settings read from the client side but which impact the server.
+ * For instance, "disable cache" is a feature toggled via DevTools UI (client),
+ * but which should be communicated to the targets (server).
+ *
+ * See the TargetConfigurationActor for a list of supported configuration options.
+ */
+class TargetConfigurationCommand {
+ constructor({ commands, watcherFront }) {
+ this._commands = commands;
+ this._watcherFront = watcherFront;
+ }
+
+ /**
+ * Return a promise that resolves to the related target configuration actor's front.
+ *
+ * @return {Promise<TargetConfigurationFront>}
+ */
+ async getFront() {
+ const front = await this._watcherFront.getTargetConfigurationActor();
+
+ if (!this._configuration) {
+ // Retrieve initial data from the front
+ this._configuration = front.initialConfiguration;
+ }
+
+ return front;
+ }
+
+ _hasTargetWatcherSupport() {
+ return this._commands.targetCommand.hasTargetWatcherSupport();
+ }
+
+ /**
+ * Retrieve the current map of configuration options pushed to the server.
+ */
+ get configuration() {
+ return this._configuration || {};
+ }
+
+ async updateConfiguration(configuration) {
+ if (this._hasTargetWatcherSupport()) {
+ const front = await this.getFront();
+ const updatedConfiguration = await front.updateConfiguration(
+ configuration
+ );
+ // Update the client-side copy of the DevTools configuration
+ this._configuration = updatedConfiguration;
+ } else {
+ await this._commands.targetCommand.targetFront.reconfigure({
+ options: configuration,
+ });
+ }
+ }
+
+ async isJavascriptEnabled() {
+ // If we don't have target watcher support, we can't get this value, so just
+ // fall back to true. Only content tab targets can update javascriptEnabled
+ // and all should have watcher support.
+ if (!this._hasTargetWatcherSupport()) {
+ return true;
+ }
+
+ const front = await this.getFront();
+ return front.isJavascriptEnabled();
+ }
+
+ /**
+ * Reports if the given configuration key is supported by the server.
+ * If the debugged context doesn't support the watcher actor,
+ * we won't be using the target configuration actor and report all keys
+ * as not supported.
+ *
+ * @param {Object} configurationKey
+ * Name of the configuration you would like to set.
+ * @return {Promise<Boolean>} True, if this configuration can be set via this API.
+ */
+ async supports(configurationKey) {
+ if (!this._hasTargetWatcherSupport()) {
+ return false;
+ }
+ const front = await this.getFront();
+ return !!front.traits.supportedOptions[configurationKey];
+ }
+
+ /**
+ * Change orientation type and angle (that can be accessed through screen.orientation in
+ * the content page) and simulates the "orientationchange" event when the device screen
+ * was rotated.
+ * Note that this will only be effective if the Responsive Design Mode is enabled.
+ *
+ * @param {Object} options
+ * @param {String} options.type: The orientation type of the rotated device.
+ * @param {Number} options.angle: The rotated angle of the device.
+ * @param {Boolean} options.isViewportRotated: Whether or not screen orientation change
+ * is a result of rotating the viewport. If true, an "orientationchange"
+ * event will be dispatched in the content window.
+ */
+ async simulateScreenOrientationChange({ type, angle, isViewportRotated }) {
+ // We need to call the method on the parent process
+ await this.updateConfiguration({
+ rdmPaneOrientation: { type, angle },
+ });
+
+ // Don't dispatch the "orientationchange" event if orientation change is a result
+ // of switching to a new device, location change, or opening RDM.
+ if (!isViewportRotated) {
+ return;
+ }
+
+ const responsiveFront =
+ await this._commands.targetCommand.targetFront.getFront("responsive");
+ await responsiveFront.dispatchOrientationChangeEvent();
+ }
+}
+
+module.exports = TargetConfigurationCommand;
diff --git a/devtools/shared/commands/target-configuration/tests/browser.toml b/devtools/shared/commands/target-configuration/tests/browser.toml
new file mode 100644
index 0000000000..d13ac34bd6
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "target_configuration_test_doc.sjs",
+ "head.js",
+]
+
+["browser_target_configuration_command.js"]
+
+["browser_target_configuration_command_color_scheme.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_target_configuration_command_custom_user_agent.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_target_configuration_command_dppx.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_target_configuration_command_touch_events.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js
new file mode 100644
index 0000000000..47dab1baa9
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the watcher's target-configuration actor API.
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab("data:text/html;charset=utf-8,Configuration actor");
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ {},
+ "Initial configuration is empty"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ cacheDisabled: true,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ { cacheDisabled: true },
+ "Option cacheDisabled was set"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ javascriptEnabled: false,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ { cacheDisabled: true, javascriptEnabled: false },
+ "Option javascriptEnabled was set"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ cacheDisabled: false,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ { cacheDisabled: false, javascriptEnabled: false },
+ "Option cacheDisabled was updated"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ {
+ cacheDisabled: false,
+ colorSchemeSimulation: "dark",
+ javascriptEnabled: false,
+ },
+ "Option colorSchemeSimulation was set, with a string value"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ setTabOffline: true,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ {
+ cacheDisabled: false,
+ colorSchemeSimulation: "dark",
+ javascriptEnabled: false,
+ setTabOffline: true,
+ },
+ "Option setTabOffline was set on"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ setTabOffline: false,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ {
+ setTabOffline: false,
+ cacheDisabled: false,
+ colorSchemeSimulation: "dark",
+ javascriptEnabled: false,
+ },
+ "Option setTabOffline was set off"
+ );
+
+ targetCommand.destroy();
+ await commands.destroy();
+});
+
+function compareOptions(options, expected, message) {
+ is(
+ Object.keys(options).length,
+ Object.keys(expected).length,
+ message + " (wrong number of options)"
+ );
+
+ for (const key of Object.keys(expected)) {
+ is(options[key], expected[key], message + ` (wrong value for ${key})`);
+ }
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js
new file mode 100644
index 0000000000..509ecb84b2
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test color scheme simulation.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab(TEST_URI);
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ false,
+ "The dark mode simulation wasn't enabled in the content page when it loaded"
+ );
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation isn't enabled in the content page by default"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ false,
+ "The dark mode simulation wasn't enabled in the remote iframe when it loaded"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation isn't enabled in the remote iframe by default"
+ );
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ info("Update configuration to enable dark mode simulation");
+ await targetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled after updating the configuration"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the remote iframe after updating the configuration"
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the content page when it loaded after reloading"
+ );
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the content page after reloading"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the remote iframe when it loaded after reloading"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the remote iframe after reloading"
+ );
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ /* includeSubFrames */ true
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onPageLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the content page when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the content page after navigating to a new browsing context"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the remote iframe when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the remote iframe after navigating to a new browsing context"
+ );
+
+ targetCommand.destroy();
+ await commands.destroy();
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation is disabled in the content page after destroying the commands"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation is disabled in the remote iframe after destroying the commands"
+ );
+});
+
+function matchPrefersDarkColorSchemeMedia(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.matchMedia("(prefers-color-scheme: dark)").matches
+ );
+}
+
+function matchPrefersDarkColorSchemeMediaAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialMatchesPrefersDarkColorScheme
+ );
+}
+
+function topLevelDocumentMatchPrefersDarkColorSchemeMedia() {
+ return matchPrefersDarkColorSchemeMedia(gBrowser.selectedBrowser);
+}
+
+function topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup() {
+ return matchPrefersDarkColorSchemeMediaAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ // Ensure we've rendered the iframe so that the prefers-color-scheme
+ // value propagated from the embedder is up-to-date.
+ await new Promise(resolve => {
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ );
+ });
+ return content.document.querySelector("iframe").browsingContext;
+ });
+}
+
+async function iframeDocumentMatchPrefersDarkColorSchemeMedia() {
+ const iframeBC = await getIframeBrowsingContext();
+ return matchPrefersDarkColorSchemeMedia(iframeBC);
+}
+
+async function iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return matchPrefersDarkColorSchemeMediaAtStartup(iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js
new file mode 100644
index 0000000000..3f342a1ac9
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js
@@ -0,0 +1,309 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test setting custom user agent.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ info("Create commands for the tab");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const initialUserAgent = await getTopLevelUserAgent();
+
+ info("Update configuration to change user agent");
+ const CUSTOM_USER_AGENT = "<MY_BORING_CUSTOM_USER_AGENT>";
+
+ await targetConfigurationCommand.updateConfiguration({
+ customUserAgent: CUSTOM_USER_AGENT,
+ });
+
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The user agent is properly set on the top level document after updating the configuration"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources on the top level document"
+ );
+
+ is(
+ await getIframeUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The user agent is properly set on the iframe after updating the configuration"
+ );
+ is(
+ await getUserAgentForIframeRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources on the iframe"
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await getTopLevelDocumentUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the content page when it loaded after reloading"
+ );
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the content page after reloading"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources after reloading"
+ );
+ is(
+ await getIframeUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the remote iframe when it loaded after reloading"
+ );
+ is(
+ await getIframeUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the remote iframe after reloading"
+ );
+ is(
+ await getUserAgentForIframeRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources in the remote iframe after reloading"
+ );
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ /* includeSubFrames */ true
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onPageLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await getTopLevelDocumentUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the content page when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the content page after navigating to a new browsing context"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources after navigating to a new browsing context"
+ );
+ is(
+ await getIframeUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the remote iframe when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getIframeUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the remote iframe after navigating to a new browsing context"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources in the remote iframes after navigating to a new browsing context"
+ );
+
+ info(
+ "Create another commands instance and check that destroying it won't reset the user agent"
+ );
+ const otherCommands = await CommandsFactory.forTab(tab);
+ const otherTargetConfigurationCommand =
+ otherCommands.targetConfigurationCommand;
+ const otherTargetCommand = otherCommands.targetCommand;
+ await otherTargetCommand.startListening();
+ // wait for the target to be fully attached to avoid pending connection to the server
+ await otherTargetCommand.watchTargets({
+ types: [otherTargetCommand.TYPES.FRAME],
+ onAvailable: () => {},
+ });
+
+ // Let's update the configuration with this commands instance to make sure we hit the TargetConfigurationActor
+ await otherTargetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+
+ otherTargetCommand.destroy();
+ await otherCommands.destroy();
+
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is still set on the page after destroying another commands instance"
+ );
+
+ info(
+ "Check that destroying the commands we set the user agent in will reset the user agent"
+ );
+ targetCommand.destroy();
+ await commands.destroy();
+
+ // XXX: This is needed at the moment since Navigator.cpp retrieve the UserAgent from the
+ // headers (when there's no custom user agent). And here, since we reloaded the page once
+ // we set the custom user agent, the header was set accordingly and still holds the custom
+ // user agent value. This should be fixed by Bug 1705326.
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is still set on the page after destroying the first commands instance. Bug 1705326 will fix that and make it equal to `initialUserAgent`"
+ );
+
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+ is(
+ await getTopLevelUserAgent(),
+ initialUserAgent,
+ "The user agent was reset in the content page after destroying the commands"
+ );
+ is(
+ await getIframeUserAgent(),
+ initialUserAgent,
+ "The user agent was reset in the remote iframe after destroying the commands"
+ );
+
+ // We need commands to retrieve the headers of the network request, and
+ // all those we created so far were destroyed; let's create new ones.
+ const newCommands = await CommandsFactory.forTab(tab);
+ await newCommands.targetCommand.startListening();
+ is(
+ await getUserAgentForTopLevelRequest(newCommands),
+ initialUserAgent,
+ "The initial user agent is used when retrieving resources after destroying the commands"
+ );
+ is(
+ await getUserAgentForIframeRequest(newCommands),
+ initialUserAgent,
+ "The initial user agent is used when retrieving resources on the remote iframe after destroying the commands"
+ );
+});
+
+function getUserAgent(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ return content.navigator.userAgent;
+ });
+}
+
+function getUserAgentAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialUserAgent
+ );
+}
+
+function getTopLevelUserAgent() {
+ return getUserAgent(gBrowser.selectedBrowser);
+}
+
+function getTopLevelDocumentUserAgentAtStartup() {
+ return getUserAgentAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+}
+
+async function getIframeUserAgent() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getUserAgent(iframeBC);
+}
+
+async function getIframeUserAgentAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getUserAgentAtStartup(iframeBC);
+}
+
+async function getRequestUserAgent(commands, browserOrBrowsingContext) {
+ const url = `unknown?${Date.now()}`;
+
+ // Wait for the resource and its headers to be available
+ const onAvailable = () => {};
+ let onUpdated;
+
+ const onResource = new Promise(resolve => {
+ onUpdated = updates => {
+ for (const { resource } of updates) {
+ if (resource.url.includes(url) && resource.requestHeadersAvailable) {
+ resolve(resource);
+ }
+ }
+ };
+
+ commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources: true,
+ }
+ );
+ });
+
+ info(`Fetch ${url}`);
+ SpecialPowers.spawn(browserOrBrowsingContext, [url], innerUrl => {
+ content.fetch(`./${innerUrl}`);
+ });
+ info("waiting for matching resource…");
+ const networkResource = await onResource;
+
+ info("…got resource, retrieve headers");
+ const packet = {
+ to: networkResource.actor,
+ type: "getRequestHeaders",
+ };
+
+ const { headers } = await commands.client.request(packet);
+
+ commands.resourceCommand.unwatchResources(
+ [commands.resourceCommand.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources: true,
+ }
+ );
+
+ return headers.find(header => header.name == "User-Agent")?.value;
+}
+
+async function getUserAgentForTopLevelRequest(commands) {
+ return getRequestUserAgent(commands, gBrowser.selectedBrowser);
+}
+
+async function getUserAgentForIframeRequest(commands) {
+ const iframeBC = await getIframeBrowsingContext();
+ return getRequestUserAgent(commands, iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js
new file mode 100644
index 0000000000..744ac2c403
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test device pixel ratio override.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ info("Create commands for the tab");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const originalDpr = await getTopLevelDocumentDevicePixelRatio();
+
+ info("Update configuration to change device pixel ratio");
+ const CUSTOM_DPR = 5.5;
+
+ await targetConfigurationCommand.updateConfiguration({
+ overrideDPPX: CUSTOM_DPR,
+ });
+
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The ratio is properly set on the top level document after updating the configuration"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The ratio is properly set on the iframe after updating the configuration"
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await getTopLevelDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the content page when it loaded after reloading"
+ );
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the content page after reloading"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the remote iframe when it loaded after reloading"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the remote iframe after reloading"
+ );
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ /* includeSubFrames */ true
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onPageLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await getTopLevelDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the content page when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the content page after navigating to a new browsing context"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the remote iframe when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the remote iframe after navigating to a new browsing context"
+ );
+
+ info(
+ "Create another commands instance and check that destroying it won't reset the ratio"
+ );
+ const otherCommands = await CommandsFactory.forTab(tab);
+ const otherTargetConfigurationCommand =
+ otherCommands.targetConfigurationCommand;
+ const otherTargetCommand = otherCommands.targetCommand;
+ await otherTargetCommand.startListening();
+
+ // Let's update the configuration with this commands instance to make sure we hit the TargetConfigurationActor
+ await otherTargetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+
+ otherTargetCommand.destroy();
+ await otherCommands.destroy();
+
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is still set on the page after destroying another commands instance"
+ );
+
+ info(
+ "Check that destroying the commands we overrode the ratio in will reset the page ratio"
+ );
+ targetCommand.destroy();
+ await commands.destroy();
+
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ originalDpr,
+ "The ratio was reset in the content page after destroying the commands"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ originalDpr,
+ "The ratio was reset in the remote iframe after destroying the commands"
+ );
+});
+
+function getDevicePixelRatio(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.browsingContext.top.overrideDPPX || content.devicePixelRatio
+ );
+}
+
+function getDevicePixelRatioAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialDevicePixelRatio
+ );
+}
+
+function getTopLevelDocumentDevicePixelRatio() {
+ return getDevicePixelRatio(gBrowser.selectedBrowser);
+}
+
+function getTopLevelDocumentDevicePixelRatioAtStartup() {
+ return getDevicePixelRatioAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+}
+
+async function getIframeDocumentDevicePixelRatio() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getDevicePixelRatio(iframeBC);
+}
+
+async function getIframeDocumentDevicePixelRatioAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getDevicePixelRatioAtStartup(iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js
new file mode 100644
index 0000000000..683dd6d999
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test touch event simulation.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ // Disable click hold and double tap zooming as it might interfere with the test
+ await pushPref("ui.click_hold_context_menus", false);
+ await pushPref("apz.allow_double_tap_zooming", false);
+
+ const tab = await addTab(TEST_URI);
+
+ info("Create commands for the tab");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ info("Touch simulation is disabled at the beginning");
+ await checkTopLevelDocumentTouchSimulation({ enabled: false });
+ await checkIframeTouchSimulation({
+ enabled: false,
+ });
+
+ info("Enable touch simulation");
+ await targetConfigurationCommand.updateConfiguration({
+ touchEventsOverride: "enabled",
+ });
+ await checkTopLevelDocumentTouchSimulation({ enabled: true });
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await topLevelDocumentMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the content page when it loaded after reloading"
+ );
+ await checkTopLevelDocumentTouchSimulation({ enabled: true });
+
+ is(
+ await iframeMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the iframe when it loaded after reloading"
+ );
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ info(
+ "Create another commands instance and check that destroying it won't reset the touch simulation"
+ );
+ const otherCommands = await CommandsFactory.forTab(tab);
+ const otherTargetConfigurationCommand =
+ otherCommands.targetConfigurationCommand;
+ const otherTargetCommand = otherCommands.targetCommand;
+
+ await otherTargetCommand.startListening();
+ // Watch targets so we wait for server communication to settle (e.g. attach calls), as
+ // this could cause intermittent failures.
+ await otherTargetCommand.watchTargets({
+ types: [otherTargetCommand.TYPES.FRAME],
+ onAvailable: () => {},
+ });
+
+ // Let's update the configuration with this commands instance to make sure we hit the TargetConfigurationActor
+ await otherTargetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+
+ otherTargetCommand.destroy();
+ await otherCommands.destroy();
+
+ await checkTopLevelDocumentTouchSimulation({ enabled: true });
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onBrowserLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await topLevelDocumentMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the content page when it loaded after navigating to a new browsing context"
+ );
+ await checkTopLevelDocumentTouchSimulation({
+ enabled: true,
+ });
+
+ is(
+ await iframeMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the iframe when it loaded after navigating to a new browsing context"
+ );
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ info(
+ "Check that destroying the commands we enabled the simulation in will disable the simulation"
+ );
+ targetCommand.destroy();
+ await commands.destroy();
+
+ await checkTopLevelDocumentTouchSimulation({ enabled: false });
+ await checkIframeTouchSimulation({
+ enabled: false,
+ });
+});
+
+function matchesCoarsePointer(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.matchMedia("(pointer: coarse)").matches
+ );
+}
+
+function matchesCoarsePointerAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialMatchesCoarsePointer
+ );
+}
+
+async function isTouchEventEmitted(browserOrBrowsingContext) {
+ const onTimeout = wait(1000).then(() => "TIMEOUT");
+ const onTouchEvent = SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ async () => {
+ content.touchStartController = new content.AbortController();
+ const el = content.document.querySelector("button");
+
+ let gotTouchEndEvent = false;
+
+ const promise = new Promise(resolve => {
+ el.addEventListener(
+ "touchend",
+ () => {
+ gotTouchEndEvent = true;
+ resolve();
+ },
+ {
+ signal: content.touchStartController.signal,
+ once: true,
+ }
+ );
+ });
+
+ // For some reason, it might happen that the event is properly registered and transformed
+ // in the touch simulator, but not received by the event listener we set up just before.
+ // So here let's try to "tap" 3 times to give us more chance to catch the event.
+ for (let i = 0; i < 3; i++) {
+ if (gotTouchEndEvent) {
+ break;
+ }
+
+ // Simulate a "tap" with mousedown and then mouseup.
+ EventUtils.synthesizeMouseAtCenter(
+ el,
+ { type: "mousedown", isSynthesized: false },
+ content
+ );
+
+ await new Promise(res => content.setTimeout(res, 10));
+ EventUtils.synthesizeMouseAtCenter(
+ el,
+ { type: "mouseup", isSynthesized: false },
+ content
+ );
+ await new Promise(res => content.setTimeout(res, 50));
+ }
+
+ return promise;
+ }
+ );
+
+ const result = await Promise.race([onTimeout, onTouchEvent]);
+
+ // Remove the event listener
+ await SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ content.touchStartController.abort();
+ delete content.touchStartController;
+ });
+
+ return result !== "TIMEOUT";
+}
+
+async function checkTopLevelDocumentTouchSimulation({ enabled }) {
+ is(
+ await matchesCoarsePointer(gBrowser.selectedBrowser),
+ enabled,
+ `The touch simulation is ${
+ enabled ? "enabled" : "disabled"
+ } on the top level document`
+ );
+
+ is(
+ await isTouchEventEmitted(gBrowser.selectedBrowser),
+ enabled,
+ `touch events are ${enabled ? "" : "not "}emitted on the top level document`
+ );
+}
+
+function topLevelDocumentMatchesCoarsePointerAtStartup() {
+ return matchesCoarsePointerAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+}
+
+async function checkIframeTouchSimulation({ enabled }) {
+ const iframeBC = await getIframeBrowsingContext();
+ is(
+ await matchesCoarsePointer(iframeBC),
+ enabled,
+ `The touch simulation is ${enabled ? "enabled" : "disabled"} on the iframe`
+ );
+
+ is(
+ await isTouchEventEmitted(iframeBC),
+ enabled,
+ `touch events are ${enabled ? "" : "not "}emitted on the iframe`
+ );
+}
+
+async function iframeMatchesCoarsePointerAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return matchesCoarsePointerAtStartup(iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/head.js b/devtools/shared/commands/target-configuration/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/head.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs b/devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs
new file mode 100644
index 0000000000..2b7511c788
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs
@@ -0,0 +1,100 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "html", false);
+
+ // Check the params and set the cross-origin-opener policy headers if needed
+ const query = new URLSearchParams(request.queryString);
+ if (query.get("crossOriginIsolated") === "true") {
+ response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false);
+ }
+
+ // We always want the iframe to have a different host from the top-level document.
+ const iframeHost =
+ request.host === "example.com" ? "example.org" : "example.com";
+ const iframeOrigin = `${request.scheme}://${iframeHost}`;
+
+ const IFRAME_HTML = `
+ <!doctype html>
+ <html>
+ <head>
+ <meta charset=utf8>
+ <script>
+ globalThis.initialMatchesPrefersDarkColorScheme =
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
+ globalThis.initialMatchesCoarsePointer =
+ window.matchMedia("(pointer: coarse)").matches;
+ globalThis.initialDevicePixelRatio = window.devicePixelRatio;
+ globalThis.initialUserAgent = navigator.userAgent;
+ </script>
+ <style>
+ html { background: cyan;}
+
+ button {
+ font-size: 2em;
+ padding-inline: 1em;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ html {background: darkred;}
+ }
+
+ </style>
+ </head>
+ <body>
+ <h1>Iframe</h1>
+ <button>Target</button>
+ </body>
+ </html>`;
+
+ const HTML = `
+ <!doctype html>
+ <html>
+ <head>
+ <meta charset=utf8>
+ <title>test</title>
+ <script type="application/javascript">
+ "use strict";
+
+ /*
+ * Store the result of dark color-scheme match very early in the document loading process
+ * so we can assert in tests that the simulation starts early enough.
+ */
+ globalThis.initialMatchesPrefersDarkColorScheme =
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
+ globalThis.initialMatchesCoarsePointer =
+ window.matchMedia("(pointer: coarse)").matches;
+ globalThis.initialDevicePixelRatio = window.devicePixelRatio
+ globalThis.initialUserAgent = navigator.userAgent;
+
+
+ </script>
+ <style>
+ iframe {
+ display: block;
+ margin-top: 1em;
+ }
+
+ button {
+ font-size: 2em;
+ padding-inline: 1em;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ html {
+ background-color: darkblue;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Test color-scheme simulation</h1>
+ <button>Target</button>
+ <iframe src='${iframeOrigin}/document-builder.sjs?html=${encodeURI(
+ IFRAME_HTML
+ )}'></iframe>
+ </body>
+ </html>`;
+
+ response.write(HTML);
+}
diff --git a/devtools/shared/commands/target/actions/moz.build b/devtools/shared/commands/target/actions/moz.build
new file mode 100644
index 0000000000..e9429c1200
--- /dev/null
+++ b/devtools/shared/commands/target/actions/moz.build
@@ -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/.
+
+DevToolsModules(
+ "targets.js",
+)
diff --git a/devtools/shared/commands/target/actions/targets.js b/devtools/shared/commands/target/actions/targets.js
new file mode 100644
index 0000000000..577e5fedd3
--- /dev/null
+++ b/devtools/shared/commands/target/actions/targets.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";
+
+function registerTarget(targetFront) {
+ return { type: "REGISTER_TARGET", targetFront };
+}
+
+function unregisterTarget(targetFront) {
+ return { type: "UNREGISTER_TARGET", targetFront };
+}
+
+/**
+ *
+ * @param {String} targetActorID: The actorID of the target we want to select.
+ */
+function selectTarget(targetActorID) {
+ return function ({ dispatch, getState }) {
+ dispatch({ type: "SELECT_TARGET", targetActorID });
+ };
+}
+
+function refreshTargets() {
+ return { type: "REFRESH_TARGETS" };
+}
+
+module.exports = {
+ registerTarget,
+ unregisterTarget,
+ selectTarget,
+ refreshTargets,
+};
diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js
new file mode 100644
index 0000000000..e0c5b18d51
--- /dev/null
+++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.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/. */
+
+"use strict";
+
+class LegacyProcessesWatcher {
+ constructor(targetCommand, onTargetAvailable, onTargetDestroyed) {
+ this.targetCommand = targetCommand;
+ this.rootFront = targetCommand.rootFront;
+
+ this.onTargetAvailable = onTargetAvailable;
+ this.onTargetDestroyed = onTargetDestroyed;
+
+ this.descriptors = new Set();
+ this._processListChanged = this._processListChanged.bind(this);
+ }
+
+ async _processListChanged() {
+ if (this.targetCommand.isDestroyed()) {
+ return;
+ }
+
+ const processes = await this.rootFront.listProcesses();
+ // Process the new list to detect the ones being destroyed
+ // Force destroyed the descriptor as well as the target
+ for (const descriptor of this.descriptors) {
+ if (!processes.includes(descriptor)) {
+ // Manually call onTargetDestroyed listeners in order to
+ // ensure calling them *before* destroying the descriptor.
+ // Otherwise the descriptor will automatically destroy the target
+ // and may not fire the contentProcessTarget's destroy event.
+ const target = descriptor.getCachedTarget();
+ if (target) {
+ this.onTargetDestroyed(target);
+ }
+
+ descriptor.destroy();
+ this.descriptors.delete(descriptor);
+ }
+ }
+
+ const promises = processes
+ .filter(descriptor => !this.descriptors.has(descriptor))
+ .map(async descriptor => {
+ // Add the new process descriptors to the local list
+ this.descriptors.add(descriptor);
+ const target = await descriptor.getTarget();
+ if (!target) {
+ console.error(
+ "Wasn't able to retrieve the target for",
+ descriptor.actorID
+ );
+ return;
+ }
+ await this.onTargetAvailable(target);
+ });
+
+ await Promise.all(promises);
+ }
+
+ async listen() {
+ this.rootFront.on("processListChanged", this._processListChanged);
+ await this._processListChanged();
+ }
+
+ unlisten() {
+ this.rootFront.off("processListChanged", this._processListChanged);
+ }
+}
+
+module.exports = LegacyProcessesWatcher;
diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js
new file mode 100644
index 0000000000..259eaea482
--- /dev/null
+++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ WorkersListener,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/shared/workers-listener.js");
+
+const LegacyWorkersWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js");
+
+class LegacyServiceWorkersWatcher extends LegacyWorkersWatcher {
+ // Holds the current target URL object
+ #currentTargetURL;
+
+ constructor(targetCommand, onTargetAvailable, onTargetDestroyed, commands) {
+ super(targetCommand, onTargetAvailable, onTargetDestroyed);
+ this._registrations = [];
+ this._processTargets = new Set();
+ this.commands = commands;
+
+ // We need to listen for registration changes at least in order to properly
+ // filter service workers by domain when debugging a local tab.
+ //
+ // A WorkerTarget instance has a url property, but it points to the url of
+ // the script, whereas the url property of the ServiceWorkerRegistration
+ // points to the URL controlled by the service worker.
+ //
+ // Historically we have been matching the service worker registration URL
+ // to match service workers for local tab tools (app panel & debugger).
+ // Maybe here we could have some more info on the actual worker.
+ this._workersListener = new WorkersListener(this.rootFront, {
+ registrationsOnly: true,
+ });
+
+ // Note that this is called much more often than when a registration
+ // is created or destroyed. WorkersListener notifies of anything that
+ // potentially impacted workers.
+ // I use it as a shortcut in this first patch. Listening to rootFront's
+ // "serviceWorkerRegistrationListChanged" should be enough to be notified
+ // about registrations. And if we need to also update the
+ // "debuggerServiceWorkerStatus" from here, then we would have to
+ // also listen to "registration-changed" one each registration.
+ this._onRegistrationListChanged =
+ this._onRegistrationListChanged.bind(this);
+ this._onDocumentEvent = this._onDocumentEvent.bind(this);
+
+ // Flag used from the parent class to listen to process targets.
+ // Decision tree is complicated, keep all logic in the parent methods.
+ this._isServiceWorkerWatcher = true;
+ }
+
+ /**
+ * Override from LegacyWorkersWatcher.
+ *
+ * We record all valid service worker targets (ie workers that match a service
+ * worker registration), but we will only notify about the ones which match
+ * the current domain.
+ */
+ _recordWorkerTarget(workerTarget) {
+ return !!this._getRegistrationForWorkerTarget(workerTarget);
+ }
+
+ // Override from LegacyWorkersWatcher.
+ _supportWorkerTarget(workerTarget) {
+ if (!workerTarget.isServiceWorker) {
+ return false;
+ }
+
+ const registration = this._getRegistrationForWorkerTarget(workerTarget);
+ return registration && this._isRegistrationValidForTarget(registration);
+ }
+
+ // Override from LegacyWorkersWatcher.
+ async listen() {
+ // Listen to the current target front.
+ this.target = this.targetCommand.targetFront;
+
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ this.#currentTargetURL = new URL(this.targetCommand.targetFront.url);
+ }
+
+ this._workersListener.addListener(this._onRegistrationListChanged);
+
+ // Fetch the registrations before calling listen, since service workers
+ // might already be available and will need to be compared with the existing
+ // registrations.
+ await this._onRegistrationListChanged();
+
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ await this.commands.resourceCommand.watchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onDocumentEvent,
+ ignoreExistingResources: true,
+ }
+ );
+ }
+
+ await super.listen();
+ }
+
+ // Override from LegacyWorkersWatcher.
+ unlisten(...args) {
+ this._workersListener.removeListener(this._onRegistrationListChanged);
+
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ this.commands.resourceCommand.unwatchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onDocumentEvent,
+ }
+ );
+ }
+
+ super.unlisten(...args);
+ }
+
+ // Override from LegacyWorkersWatcher.
+ async _onProcessAvailable({ targetFront }) {
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ // XXX: This has been ported straight from the current debugger
+ // implementation. Since pauseMatchingServiceWorkers expects an origin
+ // to filter matching workers, it only makes sense when we are debugging
+ // a tab. However in theory, parent process debugging could pause all
+ // service workers without matching anything.
+ try {
+ // To support early breakpoint we need to setup the
+ // `pauseMatchingServiceWorkers` mechanism in each process.
+ await targetFront.pauseMatchingServiceWorkers({
+ origin: this.#currentTargetURL.origin,
+ });
+ } catch (e) {
+ if (targetFront.actorID) {
+ throw e;
+ } else {
+ console.warn(
+ "Process target destroyed while calling pauseMatchingServiceWorkers"
+ );
+ }
+ }
+ }
+
+ this._processTargets.add(targetFront);
+ return super._onProcessAvailable({ targetFront });
+ }
+
+ _onProcessDestroyed({ targetFront }) {
+ this._processTargets.delete(targetFront);
+ return super._onProcessDestroyed({ targetFront });
+ }
+
+ _onDocumentEvent(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType !==
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
+ ) {
+ continue;
+ }
+
+ if (resource.name === "will-navigate") {
+ // We rely on will-navigate as the onTargetAvailable for the top-level frame can
+ // happen after the onTargetAvailable for processes (handled in _onProcessAvailable),
+ // where we need the origin we navigate to.
+ this.#currentTargetURL = new URL(resource.newURI);
+ continue;
+ }
+
+ // Note that we rely on "dom-loading" rather than "will-navigate" because the
+ // destroyed/available callbacks should be triggered after the Debugger
+ // has cleaned up its reducers, which happens on "will-navigate".
+ // On the other end, "dom-complete", which is a better mapping of "navigate", is
+ // happening too late (because of resources being throttled), and would cause failures
+ // in test (like browser_target_command_service_workers_navigation.js), as the new worker
+ // target would already be registered at this point, and seen as something that would
+ // need to be destroyed.
+ if (resource.name === "dom-loading") {
+ const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
+
+ for (const target of allServiceWorkerTargets) {
+ // Note: we call isTargetRegistered again because calls to
+ // onTargetDestroyed might have modified the list of registered targets.
+ const isRegisteredAfter =
+ this.targetCommand.isTargetRegistered(target);
+ const isValidTarget = this._supportWorkerTarget(target);
+ if (isValidTarget && !isRegisteredAfter) {
+ // If the target is still valid for the current top target, call
+ // onTargetAvailable as well.
+ this.onTargetAvailable(target);
+ }
+ }
+ }
+ }
+ }
+
+ async _onRegistrationListChanged() {
+ if (this.targetCommand.isDestroyed()) {
+ return;
+ }
+
+ await this._updateRegistrations();
+
+ // Everything after this point is not strictly necessary for sw support
+ // in the target list, but it makes the behavior closer to the previous
+ // listAllWorkers/WorkersListener pair.
+ const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
+ for (const target of allServiceWorkerTargets) {
+ const hasRegistration = this._getRegistrationForWorkerTarget(target);
+ if (!hasRegistration) {
+ // XXX: At this point the worker target is not really destroyed, but
+ // historically, listAllWorkers* APIs stopped returning worker targets
+ // if worker registrations are no longer available.
+ if (this.targetCommand.isTargetRegistered(target)) {
+ // Only emit onTargetDestroyed if it wasn't already done by
+ // onNavigate (ie the target is still tracked by TargetCommand)
+ this.onTargetDestroyed(target);
+ }
+ // Here we only care about service workers which no longer match *any*
+ // registration. The worker will be completely destroyed soon, remove
+ // it from the legacy worker watcher internal targetsByProcess Maps.
+ this._removeTargetReferences(target);
+ }
+ }
+ }
+
+ // Delete the provided worker target from the internal targetsByProcess Maps.
+ _removeTargetReferences(target) {
+ const allProcessTargets = this._getProcessTargets().filter(t =>
+ this.targetsByProcess.get(t)
+ );
+
+ for (const processTarget of allProcessTargets) {
+ this.targetsByProcess.get(processTarget).delete(target);
+ }
+ }
+
+ async _updateRegistrations() {
+ const { registrations } =
+ await this.rootFront.listServiceWorkerRegistrations();
+
+ this._registrations = registrations;
+ }
+
+ _getRegistrationForWorkerTarget(workerTarget) {
+ return this._registrations.find(r => {
+ return (
+ r.evaluatingWorker?.id === workerTarget.id ||
+ r.activeWorker?.id === workerTarget.id ||
+ r.installingWorker?.id === workerTarget.id ||
+ r.waitingWorker?.id === workerTarget.id
+ );
+ });
+ }
+
+ _getProcessTargets() {
+ return [...this._processTargets];
+ }
+
+ // Flatten all service worker targets in all processes.
+ _getAllServiceWorkerTargets() {
+ const allProcessTargets = this._getProcessTargets().filter(target =>
+ this.targetsByProcess.get(target)
+ );
+
+ const serviceWorkerTargets = [];
+ for (const target of allProcessTargets) {
+ serviceWorkerTargets.push(...this.targetsByProcess.get(target));
+ }
+ return serviceWorkerTargets;
+ }
+
+ // Check if the registration is relevant for the current target, ie
+ // corresponds to the same domain.
+ _isRegistrationValidForTarget(registration) {
+ if (this.targetCommand.descriptorFront.isBrowserProcessDescriptor) {
+ // All registrations are valid for main process debugging.
+ return true;
+ }
+
+ if (!this.targetCommand.descriptorFront.isTabDescriptor) {
+ // No support for service worker targets outside of main process &
+ // tab debugging.
+ return false;
+ }
+
+ // For local tabs, we match ServiceWorkerRegistrations and the target
+ // if they share the same hostname for their "url" properties.
+ const targetDomain = this.#currentTargetURL.hostname;
+ try {
+ const registrationDomain = new URL(registration.url).hostname;
+ return registrationDomain === targetDomain;
+ } catch (e) {
+ // XXX: Some registrations have an empty URL.
+ return false;
+ }
+ }
+}
+
+module.exports = LegacyServiceWorkersWatcher;
diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js
new file mode 100644
index 0000000000..b248e6aef7
--- /dev/null
+++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.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";
+
+const LegacyWorkersWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js");
+
+class LegacySharedWorkersWatcher extends LegacyWorkersWatcher {
+ // Flag used from the parent class to listen to process targets.
+ // Decision tree is complicated, keep all logic in the parent methods.
+ _isSharedWorkerWatcher = true;
+
+ _supportWorkerTarget(workerTarget) {
+ return workerTarget.isSharedWorker;
+ }
+}
+
+module.exports = LegacySharedWorkersWatcher;
diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js
new file mode 100644
index 0000000000..d359d5375e
--- /dev/null
+++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.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";
+
+const LegacyProcessesWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js");
+
+class LegacyWorkersWatcher {
+ constructor(targetCommand, onTargetAvailable, onTargetDestroyed) {
+ this.targetCommand = targetCommand;
+ this.rootFront = targetCommand.rootFront;
+
+ this.onTargetAvailable = onTargetAvailable;
+ this.onTargetDestroyed = onTargetDestroyed;
+
+ this.targetsByProcess = new WeakMap();
+ this.targetsListeners = new WeakMap();
+
+ this._onProcessAvailable = this._onProcessAvailable.bind(this);
+ this._onProcessDestroyed = this._onProcessDestroyed.bind(this);
+ }
+
+ async _onProcessAvailable({ targetFront }) {
+ this.targetsByProcess.set(targetFront, new Set());
+ // Listen for worker which will be created later
+ const listener = this._workerListChanged.bind(this, targetFront);
+ this.targetsListeners.set(targetFront, listener);
+
+ // If this is the browser toolbox, we have to listen from the RootFront
+ // (see comment in _workerListChanged)
+ const front = targetFront.isParentProcess ? this.rootFront : targetFront;
+ front.on("workerListChanged", listener);
+
+ // We also need to process the already existing workers
+ await this._workerListChanged(targetFront);
+ }
+
+ async _onProcessDestroyed({ targetFront }) {
+ const existingTargets = this.targetsByProcess.get(targetFront);
+
+ // Process the new list to detect the ones being destroyed
+ // Force destroying the targets
+ for (const target of existingTargets) {
+ this.onTargetDestroyed(target);
+
+ target.destroy();
+ existingTargets.delete(target);
+ }
+ this.targetsByProcess.delete(targetFront);
+ this.targetsListeners.delete(targetFront);
+ }
+
+ _supportWorkerTarget(workerTarget) {
+ // subprocess workers are ignored because they take several seconds to
+ // attach to when opening the browser toolbox. See bug 1594597.
+ // When attaching we get the following error:
+ // JavaScript error: resource://devtools/server/startup/worker.js,
+ // line 37: NetworkError: WorkerDebuggerGlobalScope.loadSubScript: Failed to load worker script at resource://devtools/shared/worker/loader.js (nsresult = 0x805e0006)
+ return (
+ workerTarget.isDedicatedWorker &&
+ !/resource:\/\/gre\/modules\/subprocess\/subprocess_.*\.worker\.js/.test(
+ workerTarget.url
+ )
+ );
+ }
+
+ async _workerListChanged(targetFront) {
+ // If we're in the Browser Toolbox, query workers from the Root Front instead of the
+ // ParentProcessTarget as the ParentProcess Target filters out the workers to only
+ // show the one from the top level window, whereas we expect the one from all the
+ // windows, and also the window-less ones.
+ // TODO: For Content Toolbox, expose SW of the page, maybe optionally?
+ const front = targetFront.isParentProcess ? this.rootFront : targetFront;
+ if (!front || front.isDestroyed() || this.targetCommand.isDestroyed()) {
+ return;
+ }
+
+ let workers;
+ try {
+ ({ workers } = await front.listWorkers());
+ } catch (e) {
+ // Workers may be added/removed at anytime so that listWorkers request
+ // can be spawn during a toolbox destroy sequence and easily fail
+ if (front.isDestroyed()) {
+ return;
+ }
+ throw e;
+ }
+
+ // Fetch the list of already existing worker targets for this process target front.
+ const existingTargets = this.targetsByProcess.get(targetFront);
+ if (!existingTargets) {
+ // unlisten was called while processing the workerListChanged callback.
+ return;
+ }
+
+ // Process the new list to detect the ones being destroyed
+ // Force destroying the targets
+ for (const target of existingTargets) {
+ if (!workers.includes(target)) {
+ this.onTargetDestroyed(target);
+
+ target.destroy();
+ existingTargets.delete(target);
+ }
+ }
+
+ const promises = workers.map(workerTarget =>
+ this._processNewWorkerTarget(workerTarget, existingTargets)
+ );
+ await Promise.all(promises);
+ }
+
+ // This is overloaded for Service Workers, which records all SW targets,
+ // but only notify about a subset of them.
+ _recordWorkerTarget(workerTarget) {
+ return this._supportWorkerTarget(workerTarget);
+ }
+
+ async _processNewWorkerTarget(workerTarget, existingTargets) {
+ if (
+ !this._recordWorkerTarget(workerTarget) ||
+ existingTargets.has(workerTarget) ||
+ this.targetCommand.isDestroyed()
+ ) {
+ return;
+ }
+
+ // Add the new worker targets to the local list
+ existingTargets.add(workerTarget);
+
+ if (this._supportWorkerTarget(workerTarget)) {
+ await this.onTargetAvailable(workerTarget);
+ }
+ }
+
+ async listen() {
+ // Listen to the current target front.
+ this.target = this.targetCommand.targetFront;
+
+ if (this.target.isParentProcess) {
+ await this.targetCommand.watchTargets({
+ types: [this.targetCommand.TYPES.PROCESS],
+ onAvailable: this._onProcessAvailable,
+ onDestroyed: this._onProcessDestroyed,
+ });
+
+ // The ParentProcessTarget front is considered to be a FRAME instead of a PROCESS.
+ // So process it manually here.
+ await this._onProcessAvailable({ targetFront: this.target });
+ return;
+ }
+
+ if (this._isSharedWorkerWatcher) {
+ // Here we're not in the browser toolbox, and SharedWorker targets are not supported
+ // in regular toolbox (See Bug 1607778)
+ return;
+ }
+
+ if (this._isServiceWorkerWatcher) {
+ this._legacyProcessesWatcher = new LegacyProcessesWatcher(
+ this.targetCommand,
+ async targetFront => {
+ // Service workers only live in content processes.
+ if (!targetFront.isParentProcess) {
+ await this._onProcessAvailable({ targetFront });
+ }
+ },
+ targetFront => {
+ if (!targetFront.isParentProcess) {
+ this._onProcessDestroyed({ targetFront });
+ }
+ }
+ );
+ await this._legacyProcessesWatcher.listen();
+ return;
+ }
+
+ // Here, we're handling Dedicated Workers in content toolbox.
+ this.targetsByProcess.set(
+ this.target,
+ this.targetsByProcess.get(this.target) || new Set()
+ );
+ this._workerListChangedListener = this._workerListChanged.bind(
+ this,
+ this.target
+ );
+ this.target.on("workerListChanged", this._workerListChangedListener);
+ await this._workerListChanged(this.target);
+ }
+
+ _getProcessTargets() {
+ return this.targetCommand.getAllTargets([this.targetCommand.TYPES.PROCESS]);
+ }
+
+ unlisten({ isTargetSwitching } = {}) {
+ // Stop listening for new process targets.
+ if (this.target.isParentProcess) {
+ this.targetCommand.unwatchTargets({
+ types: [this.targetCommand.TYPES.PROCESS],
+ onAvailable: this._onProcessAvailable,
+ onDestroyed: this._onProcessDestroyed,
+ });
+ } else if (this._isServiceWorkerWatcher) {
+ this._legacyProcessesWatcher.unlisten();
+ }
+
+ // Cleanup the targetsByProcess/targetsListeners maps, and unsubscribe from
+ // all targetFronts. Process target fronts are either stored locally when
+ // watching service workers for the content toolbox, or can be retrieved via
+ // the TargetCommand API otherwise (see _getProcessTargets implementations).
+ if (this.target.isParentProcess || this._isServiceWorkerWatcher) {
+ for (const targetFront of this._getProcessTargets()) {
+ const listener = this.targetsListeners.get(targetFront);
+ targetFront.off("workerListChanged", listener);
+
+ // When unlisten is called from a target switch or when we observe service workers targets
+ // we don't want to remove the targets from targetsByProcess
+ if (!isTargetSwitching || !this._isServiceWorkerWatcher) {
+ this.targetsByProcess.delete(targetFront);
+ }
+ this.targetsListeners.delete(targetFront);
+ }
+ } else {
+ this.target.off("workerListChanged", this._workerListChangedListener);
+ delete this._workerListChangedListener;
+ this.targetsByProcess.delete(this.target);
+ this.targetsListeners.delete(this.target);
+ }
+ }
+}
+
+module.exports = LegacyWorkersWatcher;
diff --git a/devtools/shared/commands/target/legacy-target-watchers/moz.build b/devtools/shared/commands/target/legacy-target-watchers/moz.build
new file mode 100644
index 0000000000..60fdd7ec22
--- /dev/null
+++ b/devtools/shared/commands/target/legacy-target-watchers/moz.build
@@ -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/.
+
+DevToolsModules(
+ "legacy-processes-watcher.js",
+ "legacy-serviceworkers-watcher.js",
+ "legacy-sharedworkers-watcher.js",
+ "legacy-workers-watcher.js",
+)
diff --git a/devtools/shared/commands/target/moz.build b/devtools/shared/commands/target/moz.build
new file mode 100644
index 0000000000..811fc180f0
--- /dev/null
+++ b/devtools/shared/commands/target/moz.build
@@ -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/.
+
+DIRS += [
+ "actions",
+ "legacy-target-watchers",
+ "reducers",
+ "selectors",
+]
+
+DevToolsModules(
+ "target-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/devtools/shared/commands/target/reducers/moz.build b/devtools/shared/commands/target/reducers/moz.build
new file mode 100644
index 0000000000..e9429c1200
--- /dev/null
+++ b/devtools/shared/commands/target/reducers/moz.build
@@ -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/.
+
+DevToolsModules(
+ "targets.js",
+)
diff --git a/devtools/shared/commands/target/reducers/targets.js b/devtools/shared/commands/target/reducers/targets.js
new file mode 100644
index 0000000000..2e93ddd7f0
--- /dev/null
+++ b/devtools/shared/commands/target/reducers/targets.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 initialReducerState = {
+ // Array of targetFront
+ targets: [],
+ // The selected targetFront instance
+ selected: null,
+ // timestamp of the last time a target was updated (i.e. url/title was updated).
+ // This is used by the EvaluationContextSelector component to re-render the list of
+ // targets when the list itself did not change (no addition/removal)
+ lastTargetRefresh: Date.now(),
+};
+
+function update(state = initialReducerState, action) {
+ switch (action.type) {
+ case "SELECT_TARGET": {
+ const { targetActorID } = action;
+
+ if (state.selected?.actorID === targetActorID) {
+ return state;
+ }
+
+ const selectedTarget = state.targets.find(
+ target => target.actorID === targetActorID
+ );
+
+ // It's possible that the target reducer is missing a target
+ // e.g. workers, remote iframes, etc. (Bug 1594754)
+ if (!selectedTarget) {
+ return state;
+ }
+
+ return { ...state, selected: selectedTarget };
+ }
+
+ case "REGISTER_TARGET": {
+ return {
+ ...state,
+ targets: [...state.targets, action.targetFront],
+ };
+ }
+
+ case "REFRESH_TARGETS": {
+ // The data _in_ targetFront was updated, so we only need to mutate the state,
+ // while keeping the same values.
+ return {
+ ...state,
+ lastTargetRefresh: Date.now(),
+ };
+ }
+
+ case "UNREGISTER_TARGET": {
+ const targets = state.targets.filter(
+ target => target !== action.targetFront
+ );
+
+ let { selected } = state;
+ if (selected === action.targetFront) {
+ selected = null;
+ }
+
+ return { ...state, targets, selected };
+ }
+ }
+ return state;
+}
+module.exports = update;
diff --git a/devtools/shared/commands/target/selectors/moz.build b/devtools/shared/commands/target/selectors/moz.build
new file mode 100644
index 0000000000..e9429c1200
--- /dev/null
+++ b/devtools/shared/commands/target/selectors/moz.build
@@ -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/.
+
+DevToolsModules(
+ "targets.js",
+)
diff --git a/devtools/shared/commands/target/selectors/targets.js b/devtools/shared/commands/target/selectors/targets.js
new file mode 100644
index 0000000000..95da81bbba
--- /dev/null
+++ b/devtools/shared/commands/target/selectors/targets.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/>. */
+"use strict";
+
+function getToolboxTargets(state) {
+ return state.targets;
+}
+
+function getSelectedTarget(state) {
+ return state.selected;
+}
+
+function getLastTargetRefresh(state) {
+ return state.lastTargetRefresh;
+}
+
+exports.getToolboxTargets = getToolboxTargets;
+exports.getSelectedTarget = getSelectedTarget;
+exports.getLastTargetRefresh = getLastTargetRefresh;
diff --git a/devtools/shared/commands/target/target-command.js b/devtools/shared/commands/target/target-command.js
new file mode 100644
index 0000000000..81b791f724
--- /dev/null
+++ b/devtools/shared/commands/target/target-command.js
@@ -0,0 +1,1167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const BROWSERTOOLBOX_SCOPE_PREF = "devtools.browsertoolbox.scope";
+// Possible values of the previous pref:
+const BROWSERTOOLBOX_SCOPE_EVERYTHING = "everything";
+const BROWSERTOOLBOX_SCOPE_PARENTPROCESS = "parent-process";
+
+// eslint-disable-next-line mozilla/reject-some-requires
+const createStore = require("resource://devtools/client/shared/redux/create-store.js");
+const reducer = require("resource://devtools/shared/commands/target/reducers/targets.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["refreshTargets", "registerTarget", "unregisterTarget"],
+ "resource://devtools/shared/commands/target/actions/targets.js",
+ true
+);
+
+class TargetCommand extends EventEmitter {
+ #selectedTargetFront;
+ /**
+ * This class helps managing, iterating over and listening for Targets.
+ *
+ * It exposes:
+ * - the top level target, typically the main process target for the browser toolbox
+ * or the browsing context target for a regular web toolbox
+ * - target of remoted iframe, in case Fission is enabled and some <iframe>
+ * are running in a distinct process
+ * - target switching. If the top level target changes for a new one,
+ * all the targets are going to be declared as destroyed and the new ones
+ * will be notified to the user of this API.
+ *
+ * @fires target-tread-wrong-order-on-resume : An event that is emitted when resuming
+ * the thread throws with the "wrongOrder" error.
+ *
+ * @param {DescriptorFront} descriptorFront
+ * The context to inspector identified by this descriptor.
+ * @param {WatcherFront} watcherFront
+ * If available, a reference to the related Watcher Front.
+ * @param {Object} commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ */
+ constructor({ descriptorFront, watcherFront, commands }) {
+ super();
+
+ this.commands = commands;
+ this.descriptorFront = descriptorFront;
+ this.watcherFront = watcherFront;
+ this.rootFront = descriptorFront.client.mainRoot;
+
+ this.store = createStore(reducer);
+ // Name of the store used when calling createProvider.
+ this.storeId = "target-store";
+
+ this._updateBrowserToolboxScope =
+ this._updateBrowserToolboxScope.bind(this);
+
+ Services.prefs.addObserver(
+ BROWSERTOOLBOX_SCOPE_PREF,
+ this._updateBrowserToolboxScope
+ );
+ // Until Watcher actor notify about new top level target when navigating to another process
+ // we have to manually switch to a new target from the client side
+ this.onLocalTabRemotenessChange =
+ this.onLocalTabRemotenessChange.bind(this);
+ if (this.descriptorFront.isTabDescriptor) {
+ this.descriptorFront.on(
+ "remoteness-change",
+ this.onLocalTabRemotenessChange
+ );
+ }
+
+ if (this.isServerTargetSwitchingEnabled()) {
+ // XXX: Will only be used for local tab server side target switching if
+ // the first target is generated from the server.
+ this._onFirstTarget = new Promise(r => (this._resolveOnFirstTarget = r));
+ }
+
+ // Reports if we have at least one listener for the given target type
+ this._listenersStarted = new Set();
+
+ // List of all the target fronts
+ this._targets = new Set();
+ // {Map<Function, Set<targetFront>>} A Map keyed by `onAvailable` function passed to
+ // `watchTargets`, whose initial value is a Set of the existing target fronts at the
+ // time watchTargets is called.
+ this._pendingWatchTargetInitialization = new Map();
+
+ // Listeners for target creation, destruction and selection
+ this._createListeners = new EventEmitter();
+ this._destroyListeners = new EventEmitter();
+ this._selectListeners = new EventEmitter();
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._onTargetSelected = this._onTargetSelected.bind(this);
+ // Bug 1675763: Watcher actor is not available in all situations yet.
+ if (this.watcherFront) {
+ this.watcherFront.on("target-available", this._onTargetAvailable);
+ this.watcherFront.on("target-destroyed", this._onTargetDestroyed);
+ }
+
+ this.legacyImplementation = {};
+
+ // Public flag to allow listening for workers even if the fission pref is off
+ // This allows listening for workers in the content toolbox outside of fission contexts
+ // For now, this is only toggled by tests.
+ this.listenForWorkers =
+ this.rootFront.traits.workerConsoleApiMessagesDispatchedToMainThread ===
+ false;
+ this.listenForServiceWorkers = false;
+
+ // Tells us if we received the first top level target.
+ // If target switching is done on:
+ // * client side, this is done from startListening => _createFirstTarget
+ // and pull from the Descriptor front.
+ // * server side, this is also done from startListening,
+ // but we wait for the watcher actor to notify us about it
+ // via target-available-form avent.
+ this._gotFirstTopLevelTarget = false;
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ }
+
+ get selectedTargetFront() {
+ return this.#selectedTargetFront || this.targetFront;
+ }
+
+ /**
+ * Called fired when BROWSERTOOLBOX_SCOPE_PREF pref changes.
+ * This will enable/disable the full multiprocess debugging.
+ * When enabled we will watch for content process targets and debug all the processes.
+ * When disabled we will only watch for FRAME and WORKER and restrict ourself to parent process resources.
+ */
+ _updateBrowserToolboxScope() {
+ const browserToolboxScope = Services.prefs.getCharPref(
+ BROWSERTOOLBOX_SCOPE_PREF
+ );
+ if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
+ // Force listening to new additional target types
+ this.startListening();
+ } else if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_PARENTPROCESS) {
+ const disabledTargetTypes = [
+ TargetCommand.TYPES.FRAME,
+ TargetCommand.TYPES.PROCESS,
+ ];
+ // Force unwatching for additional targets types
+ // (we keep listening to workers)
+ // The related targets will be destroyed by the server
+ // and reported as destroyed to the frontend.
+ for (const type of disabledTargetTypes) {
+ this.stopListeningForType(type, {
+ isTargetSwitching: false,
+ isModeSwitching: true,
+ });
+ }
+ }
+ }
+
+ // Called whenever a new Target front is available.
+ // Either because a target was already available as we started calling startListening
+ // or if it has just been created
+ async _onTargetAvailable(targetFront) {
+ // We put the `commands` on the targetFront so it can be retrieved from any front easily.
+ // Without this, protocol.js fronts won't have any easy access to it.
+ // Ideally, Fronts would all be migrated to commands and we would no longer need this hack.
+ targetFront.commands = this.commands;
+
+ // If the new target is a top level target, we are target switching.
+ // Target-switching is only triggered for "local-tab" browsing-context
+ // targets which should always have the topLevelTarget flag initialized
+ // on the server.
+ const isTargetSwitching = targetFront.isTopLevel;
+ const isFirstTarget =
+ targetFront.isTopLevel && !this._gotFirstTopLevelTarget;
+
+ if (this._targets.has(targetFront)) {
+ // The top level target front can be reported via listProcesses in the
+ // case of the BrowserToolbox. For any other target, log an error if it is
+ // already registered.
+ if (targetFront != this.targetFront) {
+ console.error(
+ "Target is already registered in the TargetCommand",
+ targetFront.actorID
+ );
+ }
+ return;
+ }
+
+ if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // Handle top level target switching
+ // Note that, for now, `_onTargetAvailable` isn't called for the *initial* top level target.
+ // i.e. the one that is passed to TargetCommand constructor.
+ if (targetFront.isTopLevel) {
+ // First report that all existing targets are destroyed
+ if (!isFirstTarget) {
+ this._destroyExistingTargetsOnTargetSwitching();
+ }
+
+ // Update the reference to the memoized top level target
+ this.targetFront = targetFront;
+ this.descriptorFront.setTarget(targetFront);
+ this.#selectedTargetFront = null;
+
+ if (isFirstTarget && this.isServerTargetSwitchingEnabled()) {
+ this._gotFirstTopLevelTarget = true;
+ this._resolveOnFirstTarget();
+ }
+ }
+
+ // Map the descriptor typeName to a target type.
+ const targetType = this.getTargetType(targetFront);
+ targetFront.setTargetType(targetType);
+
+ this._targets.add(targetFront);
+ try {
+ await targetFront.attachAndInitThread(this);
+ } catch (e) {
+ console.error("Error when attaching target:", e);
+ this._targets.delete(targetFront);
+ return;
+ }
+
+ for (const targetFrontsSet of this._pendingWatchTargetInitialization.values()) {
+ targetFrontsSet.delete(targetFront);
+ }
+
+ if (this.isDestroyed() || targetFront.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ this.store.dispatch(registerTarget(targetFront));
+
+ // Then, once the target is attached, notify the target front creation listeners
+ await this._createListeners.emitAsync(targetType, {
+ targetFront,
+ isTargetSwitching,
+ });
+
+ // Re-register the listeners as the top level target changed
+ // and some targets are fetched from it
+ if (targetFront.isTopLevel && !isFirstTarget) {
+ await this.startListening({ isTargetSwitching: true });
+ }
+
+ // These two events are used by tests using the production codepath (i.e. disabling flags.testing)
+ // To be consumed by tests triggering frame navigations, spawning workers...
+ this.emit("processed-available-target", targetFront);
+
+ if (isTargetSwitching) {
+ this.emit("switched-target", targetFront);
+ }
+ }
+
+ _destroyExistingTargetsOnTargetSwitching() {
+ const destroyedTargets = [];
+ for (const target of this._targets) {
+ // We only consider the top level target to be switched
+ const isDestroyedTargetSwitching = target == this.targetFront;
+ const isServiceWorker = target.targetType === this.TYPES.SERVICE_WORKER;
+ const isPopup = target.targetForm.isPopup;
+
+ // Never destroy the popup targets when the top level target is destroyed
+ // as the popup follow a different lifecycle.
+ // Also avoid destroying service worker targets for similar reason.
+ if (!isPopup && !isServiceWorker) {
+ this._onTargetDestroyed(target, {
+ isTargetSwitching: isDestroyedTargetSwitching,
+ // Do not destroy service worker front as we may want to keep using it.
+ shouldDestroyTargetFront: !isServiceWorker,
+ });
+ destroyedTargets.push(target);
+ }
+ }
+
+ // Stop listening to legacy listeners as we now have to listen
+ // on the new target.
+ this.stopListening({ isTargetSwitching: true });
+
+ // Remove destroyed target from the cached target list. We don't simply clear the
+ // Map as SW targets might not have been destroyed.
+ for (const target of destroyedTargets) {
+ this._targets.delete(target);
+ }
+ }
+
+ /**
+ * Function fired everytime a target is destroyed.
+ *
+ * This is called either:
+ * - via target-destroyed event fired by the WatcherFront,
+ * event which is a simple translation of the target-destroyed-form emitted by the WatcherActor.
+ * Watcher Actor emits this is various condition when the debugged target is meant to be destroyed:
+ * - the related target context is destroyed (tab closed, worker shut down, content process destroyed, ...),
+ * - when the DevToolsServerConnection used on the server side to communicate to the client is closed.
+
+ * - by TargetCommand._onTargetAvailable, when a top level target switching happens and all previously
+ * registered target fronts should be destroyed.
+
+ * - by the legacy Targets listeners, calling this method directly.
+ * This usecase is meant to be removed someday when all target targets are supported by the Watcher.
+ * (bug 1687459)
+ *
+ * @param {TargetFront} targetFront
+ * The target that just got destroyed.
+ * @param {Object} options
+ * @param {Boolean} [options.isTargetSwitching]
+ * To be set to true when this is about the top level target which is being replaced
+ * by a new one.
+ * The passed target should be still the one store in TargetCommand.targetFront
+ * and will be replaced via a call to onTargetAvailable with a new target front.
+ * @param {Boolean} [options.isModeSwitching]
+ * To be set to true when the target was destroyed was called as the result of a
+ * change to the devtools.browsertoolbox.scope pref.
+ * @param {Boolean} [options.shouldDestroyTargetFront]
+ * By default, the passed target front will be destroyed. But in some cases like
+ * legacy listeners for service workers we want to keep the front alive.
+ */
+ _onTargetDestroyed(
+ targetFront,
+ {
+ isModeSwitching = false,
+ isTargetSwitching = false,
+ shouldDestroyTargetFront = true,
+ } = {}
+ ) {
+ // The watcher actor may notify us about the destruction of the top level target.
+ // But second argument to this method, isTargetSwitching is only passed from the frontend.
+ // So automatically toggle the isTargetSwitching flag for server side destructions
+ // only if that's about the existing top level target.
+ if (targetFront == this.targetFront) {
+ isTargetSwitching = true;
+ }
+ this._destroyListeners.emit(targetFront.targetType, {
+ targetFront,
+ isTargetSwitching,
+ isModeSwitching,
+ });
+ this._targets.delete(targetFront);
+
+ this.store.dispatch(unregisterTarget(targetFront));
+
+ // If the destroyed target was the selected one, we need to do some cleanup
+ if (this.#selectedTargetFront == targetFront) {
+ // If we're doing a targetSwitch, simply nullify #selectedTargetFront
+ if (isTargetSwitching) {
+ this.#selectedTargetFront = null;
+ } else {
+ // Otherwise we want to select the top level target
+ this.selectTarget(this.targetFront);
+ }
+ }
+
+ if (shouldDestroyTargetFront) {
+ // When calling targetFront.destroy(), we will first call TargetFrontMixin.destroy,
+ // which will try to call `detach` RDP method.
+ // Unfortunately, this request will never complete in some cases like bfcache navigations.
+ // Because of that, the target front will never be completely destroy as it will prevent
+ // calling super.destroy and Front.destroy.
+ // Workaround that by manually calling Front class destroy method:
+ targetFront.baseFrontClassDestroy();
+
+ targetFront.destroy();
+
+ // Delete the attribute we set from _onTargetAvailable so that we avoid leaking commands
+ // if any target front is leaked.
+ delete targetFront.commands;
+ }
+ }
+
+ /**
+ *
+ * @param {TargetFront} targetFront
+ */
+ async _onTargetSelected(targetFront) {
+ if (this.#selectedTargetFront == targetFront) {
+ // Target is already selected, we can bail out.
+ return;
+ }
+
+ this.#selectedTargetFront = targetFront;
+ const targetType = this.getTargetType(targetFront);
+ await this._selectListeners.emitAsync(targetType, {
+ targetFront,
+ });
+ }
+
+ _setListening(type, value) {
+ if (value) {
+ this._listenersStarted.add(type);
+ } else {
+ this._listenersStarted.delete(type);
+ }
+ }
+
+ _isListening(type) {
+ return this._listenersStarted.has(type);
+ }
+
+ /**
+ * Check if the watcher is currently supported.
+ *
+ * When no typeOrTrait is provided, we will only check that the watcher is
+ * available.
+ *
+ * When a typeOrTrait is provided, we will check for an explicit trait on the
+ * watcherFront that indicates either that:
+ * - a target type is supported
+ * - or that a custom trait is true
+ *
+ * @param {String} [targetTypeOrTrait]
+ * Optional target type or trait.
+ * @return {Boolean} true if the watcher is available and supports the
+ * optional targetTypeOrTrait
+ */
+ hasTargetWatcherSupport(targetTypeOrTrait) {
+ if (targetTypeOrTrait) {
+ // Target types are also exposed as traits, where resource types are
+ // exposed under traits.resources (cf hasResourceWatcherSupport
+ // implementation).
+ return !!this.watcherFront?.traits[targetTypeOrTrait];
+ }
+
+ return !!this.watcherFront;
+ }
+
+ /**
+ * Start listening for targets from the server
+ *
+ * Interact with the actors in order to start listening for new types of targets.
+ * This will fire the _onTargetAvailable function for all already-existing targets,
+ * as well as the next one to be created. It will also call _onTargetDestroyed
+ * everytime a target is reported as destroyed by the actors.
+ * By the time this function resolves, all the already-existing targets will be
+ * reported to _onTargetAvailable.
+ *
+ * @param Object options
+ * @param Boolean options.isTargetSwitching
+ * Set to true when this is called while a target switching happens. In such case,
+ * we won't register listener set on the Watcher Actor, but still register listeners
+ * set via Legacy Listeners.
+ */
+ async startListening({ isTargetSwitching = false } = {}) {
+ // The first time we call this method, we pull the current top level target from the descriptor
+ if (
+ !this.isServerTargetSwitchingEnabled() &&
+ !this._gotFirstTopLevelTarget
+ ) {
+ await this._createFirstTarget();
+ }
+
+ // If no pref are set to true, nor is listenForWorkers set to true,
+ // we won't listen for any additional target. Only the top level target
+ // will be managed. We may still do target-switching.
+ const types = this._computeTargetTypes();
+
+ for (const type of types) {
+ if (this._isListening(type)) {
+ continue;
+ }
+ this._setListening(type, true);
+
+ // Only a few top level targets support the watcher actor at the moment (see WatcherActor
+ // traits in the _form method). Bug 1675763 tracks watcher actor support for all targets.
+ if (this.hasTargetWatcherSupport(type)) {
+ // When we switch to a new top level target, we don't have to stop and restart
+ // Watcher listener as it is independant from the top level target.
+ // This isn't the case for some Legacy Listeners, which fetch targets from the top level target
+ if (!isTargetSwitching) {
+ await this.watcherFront.watchTargets(type);
+ }
+ } else if (LegacyTargetWatchers[type]) {
+ // Instantiate the legacy listener only once for each TargetCommand, and reuse it if we stop and restart listening
+ if (!this.legacyImplementation[type]) {
+ this.legacyImplementation[type] = new LegacyTargetWatchers[type](
+ this,
+ this._onTargetAvailable,
+ this._onTargetDestroyed,
+ this.commands
+ );
+ }
+ await this.legacyImplementation[type].listen();
+ } else {
+ throw new Error(`Unsupported target type '${type}'`);
+ }
+ }
+
+ if (!this._watchingDocumentEvent && !this.isDestroyed()) {
+ // We want to watch DOCUMENT_EVENT in order to update the url and title of target fronts,
+ // as the initial value that is set in them might be erroneous (if the target was
+ // created so early that the document url is still pointing to about:blank and the
+ // html hasn't be parsed yet, so we can't know the <title> content).
+
+ this._watchingDocumentEvent = true;
+ await this.commands.resourceCommand.watchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onResourceAvailable,
+ }
+ );
+ }
+
+ if (this.isServerTargetSwitchingEnabled()) {
+ await this._onFirstTarget;
+ }
+ }
+
+ async _createFirstTarget() {
+ // Note that this is a public attribute, used outside of this class
+ // and helps knowing what is the current top level target we debug.
+ this.targetFront = await this.descriptorFront.getTarget();
+ this.targetFront.setTargetType(this.getTargetType(this.targetFront));
+ this.targetFront.setIsTopLevel(true);
+ this._gotFirstTopLevelTarget = true;
+
+ // See _onTargetAvailable. As this target isn't going through that method
+ // we have to replicate doing that here.
+ this.targetFront.commands = this.commands;
+
+ // Add the top-level target to the list of targets.
+ this._targets.add(this.targetFront);
+ this.store.dispatch(registerTarget(this.targetFront));
+ }
+
+ _computeTargetTypes() {
+ let types = [];
+
+ // We also check for watcher support as some xpcshell tests uses legacy APIs and don't support frames.
+ if (
+ this.descriptorFront.isTabDescriptor &&
+ this.hasTargetWatcherSupport(TargetCommand.TYPES.FRAME)
+ ) {
+ types = [TargetCommand.TYPES.FRAME];
+ } else if (this.descriptorFront.isBrowserProcessDescriptor) {
+ const browserToolboxScope = Services.prefs.getCharPref(
+ BROWSERTOOLBOX_SCOPE_PREF
+ );
+ if (browserToolboxScope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
+ types = TargetCommand.ALL_TYPES;
+ }
+ }
+ if (this.listenForWorkers && !types.includes(TargetCommand.TYPES.WORKER)) {
+ types.push(TargetCommand.TYPES.WORKER);
+ }
+ if (
+ this.listenForWorkers &&
+ !types.includes(TargetCommand.TYPES.SHARED_WORKER)
+ ) {
+ types.push(TargetCommand.TYPES.SHARED_WORKER);
+ }
+ if (
+ this.listenForServiceWorkers &&
+ !types.includes(TargetCommand.TYPES.SERVICE_WORKER)
+ ) {
+ types.push(TargetCommand.TYPES.SERVICE_WORKER);
+ }
+
+ return types;
+ }
+
+ /**
+ * Stop listening for targets from the server
+ *
+ * @param Object options
+ * @param Boolean options.isTargetSwitching
+ * Set to true when this is called while a target switching happens. In such case,
+ * we won't unregister listener set on the Watcher Actor, but still unregister
+ * listeners set via Legacy Listeners.
+ */
+ stopListening({ isTargetSwitching = false } = {}) {
+ // As DOCUMENT_EVENT isn't using legacy listener,
+ // there is no need to stop and restart it in case of target switching.
+ if (this._watchingDocumentEvent && !isTargetSwitching) {
+ this.commands.resourceCommand.unwatchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onResourceAvailable,
+ }
+ );
+ this._watchingDocumentEvent = false;
+ }
+
+ for (const type of TargetCommand.ALL_TYPES) {
+ this.stopListeningForType(type, { isTargetSwitching });
+ }
+ }
+
+ /**
+ * Stop listening for targets of a given type from the server
+ *
+ * @param String type
+ * target type we want to stop listening for
+ * @param Object options
+ * @param Boolean options.isTargetSwitching
+ * Set to true when this is called while a target switching happens. In such case,
+ * we won't unregister listener set on the Watcher Actor, but still unregister
+ * listeners set via Legacy Listeners.
+ * @param Boolean options.isModeSwitching
+ * Set to true when this is called as the result of a change to the
+ * devtools.browsertoolbox.scope pref.
+ */
+ stopListeningForType(type, { isTargetSwitching, isModeSwitching }) {
+ if (!this._isListening(type)) {
+ return;
+ }
+ this._setListening(type, false);
+
+ // Only a few top level targets support the watcher actor at the moment (see WatcherActor
+ // traits in the _form method). Bug 1675763 tracks watcher actor support for all targets.
+ if (this.hasTargetWatcherSupport(type)) {
+ // When we switch to a new top level target, we don't have to stop and restart
+ // Watcher listener as it is independant from the top level target.
+ // This isn't the case for some Legacy Listeners, which fetch targets from the top level target
+ // Also, TargetCommand.destroy may be called after the client is closed.
+ // So avoid calling the RDP method in that situation.
+ if (!isTargetSwitching && !this.watcherFront.isDestroyed()) {
+ this.watcherFront.unwatchTargets(type, { isModeSwitching });
+ }
+ } else if (this.legacyImplementation[type]) {
+ this.legacyImplementation[type].unlisten({
+ isTargetSwitching,
+ isModeSwitching,
+ });
+ } else {
+ throw new Error(`Unsupported target type '${type}'`);
+ }
+ }
+
+ getTargetType(target) {
+ const { typeName } = target;
+ if (typeName == "windowGlobalTarget") {
+ return TargetCommand.TYPES.FRAME;
+ }
+
+ if (
+ typeName == "contentProcessTarget" ||
+ typeName == "parentProcessTarget"
+ ) {
+ return TargetCommand.TYPES.PROCESS;
+ }
+
+ if (typeName == "workerDescriptor" || typeName == "workerTarget") {
+ if (target.isSharedWorker) {
+ return TargetCommand.TYPES.SHARED_WORKER;
+ }
+
+ if (target.isServiceWorker) {
+ return TargetCommand.TYPES.SERVICE_WORKER;
+ }
+
+ return TargetCommand.TYPES.WORKER;
+ }
+
+ throw new Error("Unsupported target typeName: " + typeName);
+ }
+
+ _matchTargetType(type, target) {
+ return type === target.targetType;
+ }
+
+ _onResourceAvailable(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType ===
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
+ ) {
+ const { targetFront } = resource;
+ if (resource.title !== undefined && targetFront?.setTitle) {
+ targetFront.setTitle(resource.title);
+ }
+ if (resource.url !== undefined && targetFront?.setUrl) {
+ targetFront.setUrl(resource.url);
+ }
+ if (
+ !resource.isFrameSwitching &&
+ // `url` is set on the targetFront when we receive dom-loading, and `title` when
+ // `dom-interactive` is received. Here we're only updating the window title in
+ // the "newer" event.
+ resource.name === "dom-interactive"
+ ) {
+ // We just updated the targetFront title and url, force a refresh
+ // so that the EvaluationContext selector update them.
+ this.store.dispatch(refreshTargets());
+ }
+ }
+ }
+ }
+
+ /**
+ * Listen for the creation and/or destruction of target fronts matching one of the provided types.
+ *
+ * @param {Object} options
+ * @param {Array<String>} options.types
+ * The type of target to listen for. Constant of TargetCommand.TYPES.
+ * @param {Function} options.onAvailable
+ * Mandatory callback fired when a target has been just created or was already available.
+ * The function is called with a single object argument containing the following properties:
+ * - {TargetFront} targetFront: The target Front
+ * - {Boolean} isTargetSwitching: Is this target relates to a navigation and
+ * this replaced a previously available target, this flag will be true
+ * @param {Function} options.onDestroyed
+ * Optional callback fired in case of target front destruction.
+ * The function is called with the same arguments than onAvailable.
+ * @param {Function} options.onSelected
+ * Optional callback fired when a given target is selected from the iframe picker
+ * The function is called with a single object argument containing the following properties:
+ * - {TargetFront} targetFront: The target Front
+ */
+ async watchTargets(options = {}) {
+ const availableOptions = [
+ "types",
+ "onAvailable",
+ "onDestroyed",
+ "onSelected",
+ ];
+ const unsupportedKeys = Object.keys(options).filter(
+ key => !availableOptions.includes(key)
+ );
+ if (unsupportedKeys.length) {
+ throw new Error(
+ `TargetCommand.watchTargets does not expect the following options: ${unsupportedKeys.join(
+ ", "
+ )}`
+ );
+ }
+
+ const { types, onAvailable, onDestroyed, onSelected } = options;
+ if (typeof onAvailable != "function") {
+ throw new Error(
+ "TargetCommand.watchTargets expects a function for the onAvailable option"
+ );
+ }
+
+ for (const type of types) {
+ if (!this._isValidTargetType(type)) {
+ throw new Error(
+ `TargetCommand.watchTargets invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ // Notify about already existing target of these types
+ const targetFronts = [...this._targets].filter(targetFront =>
+ types.includes(targetFront.targetType)
+ );
+ this._pendingWatchTargetInitialization.set(
+ onAvailable,
+ new Set(targetFronts)
+ );
+ const promises = targetFronts.map(async targetFront => {
+ // Attach the targets that aren't attached yet (e.g. the initial top-level target),
+ // and wait for the other ones to be fully attached.
+ try {
+ await targetFront.attachAndInitThread(this);
+ } catch (e) {
+ console.error("Error when attaching target:", e);
+ return;
+ }
+
+ // It can happen that onAvailable was already called with this targetFront at
+ // this time (via _onTargetAvailable). If that's the case, we don't want to call
+ // onAvailable a second time.
+ if (
+ this._pendingWatchTargetInitialization &&
+ this._pendingWatchTargetInitialization.has(onAvailable) &&
+ !this._pendingWatchTargetInitialization
+ .get(onAvailable)
+ .has(targetFront)
+ ) {
+ return;
+ }
+
+ try {
+ // Ensure waiting for eventual async create listeners
+ // which may setup things regarding the existing targets
+ // and listen callsite may care about the full initialization
+ await onAvailable({
+ targetFront,
+ isTargetSwitching: false,
+ });
+ } catch (e) {
+ // Prevent throwing when onAvailable handler throws on one target
+ // so that it can try to register the other targets
+ console.error(
+ "Exception when calling onAvailable handler",
+ e.message,
+ e
+ );
+ }
+ });
+
+ for (const type of types) {
+ this._createListeners.on(type, onAvailable);
+ if (onDestroyed) {
+ this._destroyListeners.on(type, onDestroyed);
+ }
+ if (onSelected) {
+ this._selectListeners.on(type, onSelected);
+ }
+ }
+
+ await Promise.all(promises);
+ this._pendingWatchTargetInitialization.delete(onAvailable);
+ }
+
+ /**
+ * Stop listening for the creation and/or destruction of a given type of target fronts.
+ * See `watchTargets()` for documentation of the arguments.
+ */
+ unwatchTargets(options = {}) {
+ const availableOptions = [
+ "types",
+ "onAvailable",
+ "onDestroyed",
+ "onSelected",
+ ];
+ const unsupportedKeys = Object.keys(options).filter(
+ key => !availableOptions.includes(key)
+ );
+ if (unsupportedKeys.length) {
+ throw new Error(
+ `TargetCommand.unwatchTargets does not expect the following options: ${unsupportedKeys.join(
+ ", "
+ )}`
+ );
+ }
+
+ const { types, onAvailable, onDestroyed, onSelected } = options;
+ if (typeof onAvailable != "function") {
+ throw new Error(
+ "TargetCommand.unwatchTargets expects a function for the onAvailable option"
+ );
+ }
+
+ for (const type of types) {
+ if (!this._isValidTargetType(type)) {
+ throw new Error(
+ `TargetCommand.unwatchTargets invoked with an unknown type: "${type}"`
+ );
+ }
+
+ this._createListeners.off(type, onAvailable);
+ if (onDestroyed) {
+ this._destroyListeners.off(type, onDestroyed);
+ }
+ if (onSelected) {
+ this._selectListeners.off(type, onSelected);
+ }
+ }
+ this._pendingWatchTargetInitialization.delete(onAvailable);
+ }
+
+ /**
+ * Retrieve all the current target fronts of a given type.
+ *
+ * @param {Array<String>} types
+ * The types of target to retrieve. Array of TargetCommand.TYPES
+ * @return {Array<TargetFront>} Array of target fronts matching any of the
+ * provided types.
+ */
+ getAllTargets(types) {
+ if (!types?.length) {
+ throw new Error("getAllTargets expects a non-empty array of types");
+ }
+
+ const targets = [...this._targets].filter(target =>
+ types.some(type => this._matchTargetType(type, target))
+ );
+
+ return targets;
+ }
+
+ /**
+ * Retrieve all the target fronts in the selected target tree (including the selected
+ * target itself).
+ *
+ * @param {Array<String>} types
+ * The types of target to retrieve. Array of TargetCommand.TYPES
+ * @return {Promise<Array<TargetFront>>} Promise that resolves to an array of target fronts.
+ */
+ async getAllTargetsInSelectedTargetTree(types) {
+ const allTargets = this.getAllTargets(types);
+ if (this.isTopLevelTargetSelected()) {
+ return allTargets;
+ }
+
+ const targets = [this.selectedTargetFront];
+ for (const target of allTargets) {
+ const isInSelectedTree = await target.isTargetAnAncestor(
+ this.selectedTargetFront
+ );
+
+ if (isInSelectedTree) {
+ targets.push(target);
+ }
+ }
+ return targets;
+ }
+
+ /**
+ * For all the target fronts of given types, retrieve all the target-scoped fronts of the given types.
+ *
+ * @param {Array<String>} targetTypes
+ * The types of target to iterate over. Constant of TargetCommand.TYPES.
+ * @param {String} frontType
+ * The type of target-scoped front to retrieve. It can be "inspector", "console", "thread",...
+ * @param {Object} options
+ * @param {Boolean} options.onlyInSelectedTargetTree
+ * Set to true to only get the fronts for targets who are in the "targets tree"
+ * of the selected target.
+ */
+ async getAllFronts(
+ targetTypes,
+ frontType,
+ { onlyInSelectedTargetTree = false } = {}
+ ) {
+ if (!Array.isArray(targetTypes) || !targetTypes?.length) {
+ throw new Error("getAllFronts expects a non-empty array of target types");
+ }
+ const promises = [];
+ const targets = !onlyInSelectedTargetTree
+ ? this.getAllTargets(targetTypes)
+ : await this.getAllTargetsInSelectedTargetTree(targetTypes);
+ for (const target of targets) {
+ // For still-attaching worker targets, the thread or console front may not yet be available,
+ // whereas TargetMixin.getFront will throw if the actorID isn't available in targetForm.
+ // Also ignore destroyed targets. For some reason the previous methods fetching targets
+ // can sometime return destroyed targets.
+ if (
+ (frontType == "thread" && !target.targetForm.threadActor) ||
+ (frontType == "console" && !target.targetForm.consoleActor) ||
+ target.isDestroyed()
+ ) {
+ continue;
+ }
+
+ promises.push(target.getFront(frontType));
+ }
+ return Promise.all(promises);
+ }
+
+ /**
+ * This function is triggered by an event sent by the TabDescriptor when
+ * the tab navigates to a distinct process.
+ *
+ * @param TargetFront targetFront
+ * The WindowGlobalTargetFront instance that navigated to another process
+ */
+ async onLocalTabRemotenessChange(targetFront) {
+ if (this.isServerTargetSwitchingEnabled()) {
+ // For server-side target switching, everything will be handled by the
+ // _onTargetAvailable callback.
+ return;
+ }
+
+ // TabDescriptor may emit the event with a null targetFront, interpret that as if the previous target
+ // has already been destroyed
+ if (targetFront) {
+ // Wait for the target to be destroyed so that LocalTabCommandsFactory clears its memoized target for this tab
+ await targetFront.once("target-destroyed");
+ }
+
+ // Fetch the new target from the descriptor.
+ const newTarget = await this.descriptorFront.getTarget();
+
+ // If a navigation happens while we try to get the target for the page that triggered
+ // the remoteness change, `getTarget` will return null. In such case, we'll get the
+ // "next" target through onTargetAvailable so it's safe to bail here.
+ if (!newTarget) {
+ console.warn(
+ `Couldn't get the target for descriptor ${this.descriptorFront.actorID}`
+ );
+ return;
+ }
+
+ this.switchToTarget(newTarget);
+ }
+
+ /**
+ * Reload the current top level target.
+ * This only works for targets inheriting from WindowGlobalTarget.
+ *
+ * @param {Boolean} bypassCache
+ * If true, the reload will be forced to bypass any cache.
+ */
+ async reloadTopLevelTarget(bypassCache = false) {
+ if (!this.descriptorFront.traits.supportsReloadDescriptor) {
+ throw new Error("The top level target doesn't support being reloaded");
+ }
+
+ // Wait for the next DOCUMENT_EVENT's dom-complete event
+ // Wait for waitForNextResource completion before reloading, otherwise we might miss the dom-complete event.
+ // This can happen if `ResourceCommand.watchResources` made by `waitForNextResource` is still pending
+ // while the reload already started and finished loading the document early.
+ const { onResource: onReloaded } =
+ await this.commands.resourceCommand.waitForNextResource(
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
+ {
+ ignoreExistingResources: true,
+ predicate(resource) {
+ return resource.name == "dom-complete";
+ },
+ }
+ );
+
+ await this.descriptorFront.reloadDescriptor({ bypassCache });
+
+ await onReloaded;
+ }
+
+ /**
+ * Called when the top level target is replaced by a new one.
+ * Typically when we navigate to another domain which requires to be loaded in a distinct process.
+ *
+ * @param {TargetFront} newTarget
+ * The new top level target to debug.
+ */
+ async switchToTarget(newTarget) {
+ // Notify about this new target to creation listeners
+ // _onTargetAvailable will also destroy all previous target before notifying about this new one.
+ await this._onTargetAvailable(newTarget);
+ }
+
+ /**
+ * Called when the user selects a frame in the iframe picker.
+ *
+ * @param {WindowGlobalTargetFront} targetFront
+ * The target front we want the toolbox to focus on.
+ */
+ selectTarget(targetFront) {
+ return this._onTargetSelected(targetFront);
+ }
+
+ /**
+ * Returns true if the top-level frame is the selected one
+ *
+ * @returns {Boolean}
+ */
+ isTopLevelTargetSelected() {
+ return this.selectedTargetFront === this.targetFront;
+ }
+
+ /**
+ * Returns true if a non top-level frame is the selected one in the iframe picker.
+ *
+ * @returns {Boolean}
+ */
+ isNonTopLevelTargetSelected() {
+ return this.selectedTargetFront !== this.targetFront;
+ }
+
+ isTargetRegistered(targetFront) {
+ return this._targets.has(targetFront);
+ }
+
+ getParentTarget(targetFront) {
+ // Note that there are edgecases:
+ // * Until bug 1741927 is fixed and we remove non-EFT codepath entirely,
+ // we may receive a `parentInnerWindowId` that doesn't relate to any target.
+ // This happens when the parent document of the targetFront is a document loaded in the
+ // same process as its parent document. In such scenario, and only when EFT is disabled,
+ // we won't instantiate a target for the parent document of the targetFront.
+ // * `parentInnerWindowId` could be null in some case like for tabs in the MBT
+ // we should report the top level target as parent. That's what `getParentWindowGlobalTarget` does.
+ // Once we can stop using getParentWindowGlobalTarget for the other edgecase we will be able to
+ // replace it with such fallback: `return this.targetFront;`.
+ // browser_target_command_frames.js will help you get things right.
+ const { parentInnerWindowId } = targetFront.targetForm;
+ if (parentInnerWindowId) {
+ const targets = this.getAllTargets([TargetCommand.TYPES.FRAME]);
+ const parent = targets.find(
+ target => target.innerWindowId == parentInnerWindowId
+ );
+ // Until EFT is the only codepath supported (bug 1741927), we will fallback to `getParentWindowGlobalTarget`
+ // as we may not have a target if the parent is an iframe running in the same process as its parent.
+ if (parent) {
+ return parent;
+ }
+ }
+
+ // Note that all callsites which care about FRAME additional target
+ // should all have a toolbox using the watcher actor.
+ // It should be: MBT, regular tab toolbox and web extension.
+ // The others which still don't support watcher don't spawn FRAME targets:
+ // browser content toolbox and service workers.
+
+ return this.watcherFront.getParentWindowGlobalTarget(
+ targetFront.browsingContextID
+ );
+ }
+
+ isDestroyed() {
+ return this._isDestroyed;
+ }
+
+ isServerTargetSwitchingEnabled() {
+ if (this.descriptorFront.isServerTargetSwitchingEnabled) {
+ return this.descriptorFront.isServerTargetSwitchingEnabled();
+ }
+ return false;
+ }
+
+ _isValidTargetType(type) {
+ return this.ALL_TYPES.includes(type);
+ }
+
+ destroy() {
+ this.stopListening();
+ this._createListeners.off();
+ this._destroyListeners.off();
+ this._selectListeners.off();
+
+ this.#selectedTargetFront = null;
+ this._isDestroyed = true;
+
+ Services.prefs.removeObserver(
+ BROWSERTOOLBOX_SCOPE_PREF,
+ this._updateBrowserToolboxScope
+ );
+ }
+}
+
+/**
+ * All types of target:
+ */
+TargetCommand.TYPES = TargetCommand.prototype.TYPES = {
+ PROCESS: "process",
+ FRAME: "frame",
+ WORKER: "worker",
+ SHARED_WORKER: "shared_worker",
+ SERVICE_WORKER: "service_worker",
+};
+TargetCommand.ALL_TYPES = TargetCommand.prototype.ALL_TYPES = Object.values(
+ TargetCommand.TYPES
+);
+
+const LegacyTargetWatchers = {};
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.PROCESS,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js"
+);
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.WORKER,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js"
+);
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.SHARED_WORKER,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js"
+);
+loader.lazyRequireGetter(
+ LegacyTargetWatchers,
+ TargetCommand.TYPES.SERVICE_WORKER,
+ "resource://devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js"
+);
+
+module.exports = TargetCommand;
diff --git a/devtools/shared/commands/target/tests/browser.toml b/devtools/shared/commands/target/tests/browser.toml
new file mode 100644
index 0000000000..3a0c59de6f
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser.toml
@@ -0,0 +1,67 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+ "head.js",
+ "simple_document.html",
+ "incremental-js-value-script.sjs",
+ "fission_document.html",
+ "fission_iframe.html",
+ "test_service_worker.js",
+ "test_sw_page.html",
+ "test_sw_page_worker.js",
+ "test_worker.js",
+]
+
+["browser_target_command_bfcache.js"]
+
+["browser_target_command_browser_workers.js"]
+
+["browser_target_command_detach.js"]
+
+["browser_target_command_frames.js"]
+
+["browser_target_command_frames_popups.js"]
+
+["browser_target_command_frames_reload_server_side_targets.js"]
+skip-if = [
+ "os == 'linux' && !fission" # Bug 1855067
+]
+["browser_target_command_getAllTargets.js"]
+
+["browser_target_command_invalid_api_usage.js"]
+
+["browser_target_command_processes.js"]
+
+["browser_target_command_reload.js"]
+
+["browser_target_command_scope_flag.js"]
+
+["browser_target_command_service_workers.js"]
+skip-if = [
+ "http3", # Bug 1781324
+ "http2",
+]
+
+["browser_target_command_service_workers_navigation.js"]
+skip-if = [
+ "os == 'linux'", # Bug 1767781
+ "win11_2009", # Bug 1767781
+]
+
+["browser_target_command_switchToTarget.js"]
+
+["browser_target_command_tab_workers.js"]
+
+["browser_target_command_tab_workers_bfcache_navigation.js"]
+skip-if = ["debug"] # Bug 1721859
+
+["browser_target_command_various_descriptors.js"]
+skip-if = ["os == 'linux' && bits == 64 && !debug"] #Bug 1701056
+
+["browser_target_command_watchTargets.js"]
+
+["browser_watcher_actor_getter_caching.js"]
diff --git a/devtools/shared/commands/target/tests/browser_target_command_bfcache.js b/devtools/shared/commands/target/tests/browser_target_command_bfcache.js
new file mode 100644
index 0000000000..598e9c550b
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_bfcache.js
@@ -0,0 +1,505 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API when bfcache navigations happen
+
+const TEST_COM_URL = URL_ROOT_SSL + "simple_document.html";
+
+add_task(async function () {
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ info("## Test with bfcache in parent DISABLED");
+ await pushPref("fission.bfcacheInParent", false);
+ await testTopLevelNavigations(false);
+ await testIframeNavigations(false);
+ await testTopLevelNavigationsOnDocumentWithIframe(false);
+
+ // bfcacheInParent only works if sessionHistoryInParent is enable
+ // so only test it if both settings are enabled.
+ // (it looks like sessionHistoryInParent is enabled by default when fission is enabled)
+ if (Services.appinfo.sessionHistoryInParent) {
+ info("## Test with bfcache in parent ENABLED");
+ await pushPref("fission.bfcacheInParent", true);
+ await testTopLevelNavigations(true);
+ await testIframeNavigations(true);
+ await testTopLevelNavigationsOnDocumentWithIframe(true);
+ }
+});
+
+async function testTopLevelNavigations(bfcacheInParent) {
+ info(" # Test TOP LEVEL navigations");
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_COM_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(targetFront.isTopLevel, "all targets of this test are top level");
+ targets.push(targetFront);
+ };
+ const destroyedTargets = [];
+ const onDestroyed = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(targetFront.isTopLevel, "all targets of this test are top level");
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ is(targets.length, 1, "retrieved only the top level target");
+ is(targets[0], targetCommand.targetFront, "the target is the top level one");
+ is(
+ destroyedTargets.length,
+ 0,
+ "We get no destruction when calling watchTargets"
+ );
+ ok(
+ targets[0].targetForm.followWindowGlobalLifeCycle,
+ "the first server side target follows the WindowGlobal lifecycle, when server target switching is enabled"
+ );
+
+ // Navigate to the same page with query params
+ info("Load the second page");
+ const secondPageUrl = TEST_COM_URL + "?second-load";
+ const previousBrowsingContextID = gBrowser.selectedBrowser.browsingContext.id;
+ ok(
+ previousBrowsingContextID,
+ "Fetch the tab's browsing context id before navigation"
+ );
+ const onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ secondPageUrl
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ secondPageUrl
+ );
+ await onLoaded;
+
+ // Assert BrowsingContext changes as it impact the behavior of targets
+ if (bfcacheInParent) {
+ isnot(
+ previousBrowsingContextID,
+ gBrowser.selectedBrowser.browsingContext.id,
+ "When bfcacheInParent is enabled, same-origin navigations spawn new BrowsingContext"
+ );
+ } else {
+ is(
+ previousBrowsingContextID,
+ gBrowser.selectedBrowser.browsingContext.id,
+ "When bfcacheInParent is disabled, same-origin navigations re-use the same BrowsingContext"
+ );
+ }
+
+ // Same-origin navigations also spawn a new top level target
+ await waitFor(
+ () => targets.length == 2,
+ "wait for the next top level target"
+ );
+ is(
+ targets[1],
+ targetCommand.targetFront,
+ "the second target is the top level one"
+ );
+ // As targetFront.url isn't reliable and might be about:blank,
+ // try to assert that we got the right target via other means.
+ // outerWindowID should change when navigating to another process,
+ // while it would stay equal for in-process navigations.
+ is(
+ targets[1].outerWindowID,
+ gBrowser.selectedBrowser.outerWindowID,
+ "the second target is for the second page"
+ );
+ ok(
+ targets[1].targetForm.followWindowGlobalLifeCycle,
+ "the new server side target follows the WindowGlobal lifecycle"
+ );
+ ok(targets[0].isDestroyed(), "the first target is destroyed");
+ is(destroyedTargets.length, 1, "We get one target being destroyed...");
+ is(destroyedTargets[0], targets[0], "...and that's the first one");
+
+ // Go back to the first page, this should be a bfcache navigation, and,
+ // we should get a new target
+ info("Go back to the first page");
+ gBrowser.selectedBrowser.goBack();
+
+ await waitFor(
+ () => targets.length == 3,
+ "wait for the next top level target"
+ );
+ is(
+ targets[2],
+ targetCommand.targetFront,
+ "the third target is the top level one"
+ );
+ // Here as this is revived from cache, the url should always be correct
+ is(targets[2].url, TEST_COM_URL, "the third target is for the first url");
+ ok(
+ targets[2].targetForm.followWindowGlobalLifeCycle,
+ "the third target for bfcache navigations is following the WindowGlobal lifecycle"
+ );
+ ok(targets[1].isDestroyed(), "the second target is destroyed");
+ is(
+ destroyedTargets.length,
+ 2,
+ "We get one additional target being destroyed..."
+ );
+ is(destroyedTargets[1], targets[1], "...and that's the second one");
+
+ // Wait for full attach in order to having breaking any pending requests
+ // when navigating to another page and switching to new process and target.
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ // Go forward and resurect the second page, this should also be a bfcache navigation, and,
+ // get a new target.
+ info("Go forward to the second page");
+
+ // When a new target will be created, we need to wait until it's fully processed
+ // to avoid pending promises.
+ const onNewTargetProcessed = bfcacheInParent
+ ? new Promise(resolve => {
+ targetCommand.on(
+ "processed-available-target",
+ function onProcessedAvailableTarget(targetFront) {
+ if (targetFront === targets[3]) {
+ resolve();
+ targetCommand.off(
+ "processed-available-target",
+ onProcessedAvailableTarget
+ );
+ }
+ }
+ );
+ })
+ : null;
+
+ gBrowser.selectedBrowser.goForward();
+
+ await waitFor(
+ () => targets.length == 4,
+ "wait for the next top level target"
+ );
+ is(
+ targets[3],
+ targetCommand.targetFront,
+ "the 4th target is the top level one"
+ );
+ // Same here, as the document is revived from the cache, the url should always be correct
+ is(targets[3].url, secondPageUrl, "the 4th target is for the second url");
+ ok(
+ targets[3].targetForm.followWindowGlobalLifeCycle,
+ "the 4th target for bfcache navigations is following the WindowGlobal lifecycle"
+ );
+ ok(targets[2].isDestroyed(), "the third target is destroyed");
+ is(
+ destroyedTargets.length,
+ 3,
+ "We get one additional target being destroyed..."
+ );
+ is(destroyedTargets[2], targets[2], "...and that's the third one");
+
+ // Wait for full attach in order to having breaking any pending requests
+ // when navigating to another page and switching to new process and target.
+ await waitForAllTargetsToBeAttached(targetCommand);
+ await onNewTargetProcessed;
+
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testTopLevelNavigationsOnDocumentWithIframe(bfcacheInParent) {
+ info(" # Test TOP LEVEL navigations on document with iframe");
+ // Create a TargetCommand for a given test tab
+ const tab =
+ await addTab(`https://example.com/document-builder.sjs?id=top&html=
+ <h1>Top level</h1>
+ <iframe src="${encodeURIComponent(
+ "https://example.com/document-builder.sjs?id=iframe&html=<h2>In iframe</h2>"
+ )}">
+ </iframe>`);
+ const getLocationIdParam = url =>
+ new URLSearchParams(new URL(url).search).get("id");
+
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ targets.push(targetFront);
+ };
+ const destroyedTargets = [];
+ const onDestroyed = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ if (isEveryFrameTargetEnabled()) {
+ is(
+ targets.length,
+ 2,
+ "retrieved targets for top level and iframe documents"
+ );
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "the target is the top level one"
+ );
+ is(
+ getLocationIdParam(targets[1].url),
+ "iframe",
+ "the second target is the iframe one"
+ );
+ } else {
+ is(targets.length, 1, "retrieved only the top level target");
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "the target is the top level one"
+ );
+ }
+
+ is(
+ destroyedTargets.length,
+ 0,
+ "We get no destruction when calling watchTargets"
+ );
+
+ info("Navigate to a new page");
+ let targetCountBeforeNavigation = targets.length;
+ const secondPageUrl = `https://example.com/document-builder.sjs?html=second`;
+ const onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ secondPageUrl
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ secondPageUrl
+ );
+ await onLoaded;
+
+ // Same-origin navigations also spawn a new top level target
+ await waitFor(
+ () => targets.length == targetCountBeforeNavigation + 1,
+ "wait for the next top level target"
+ );
+ is(
+ targets.at(-1),
+ targetCommand.targetFront,
+ "the new target is the top level one"
+ );
+
+ ok(targets[0].isDestroyed(), "the first target is destroyed");
+ if (isEveryFrameTargetEnabled()) {
+ ok(targets[1].isDestroyed(), "the second target is destroyed");
+ is(destroyedTargets.length, 2, "The two targets were destroyed");
+ } else {
+ is(destroyedTargets.length, 1, "Only one target was destroyed");
+ }
+
+ // Go back to the first page, this should be a bfcache navigation, and,
+ // we should get a new target (or 2 if EFT is enabled)
+ targetCountBeforeNavigation = targets.length;
+ info("Go back to the first page");
+ gBrowser.selectedBrowser.goBack();
+
+ await waitFor(
+ () =>
+ targets.length ===
+ targetCountBeforeNavigation + (isEveryFrameTargetEnabled() ? 2 : 1),
+ "wait for the next top level target"
+ );
+
+ if (isEveryFrameTargetEnabled()) {
+ await waitFor(() => targets.at(-2).url && targets.at(-1).url);
+ is(
+ getLocationIdParam(targets.at(-2).url),
+ "top",
+ "the first new target is for the top document…"
+ );
+ is(
+ getLocationIdParam(targets.at(-1).url),
+ "iframe",
+ "…and the second one is for the iframe"
+ );
+ } else {
+ is(
+ getLocationIdParam(targets.at(-1).url),
+ "top",
+ "the new target is for the first url"
+ );
+ }
+
+ ok(
+ targets[targetCountBeforeNavigation - 1].isDestroyed(),
+ "the target for the second page is destroyed"
+ );
+ is(
+ destroyedTargets.length,
+ targetCountBeforeNavigation,
+ "We get one additional target being destroyed…"
+ );
+ is(
+ destroyedTargets.at(-1),
+ targets[targetCountBeforeNavigation - 1],
+ "…and that's the second page one"
+ );
+
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testIframeNavigations() {
+ info(" # Test IFRAME navigations");
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(
+ `http://example.org/document-builder.sjs?html=<iframe src="${TEST_COM_URL}"></iframe>`
+ );
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ targets.push(targetFront);
+ };
+ await targetCommand.watchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ // When fission/EFT is off, there isn't much to test for iframes as they are debugged
+ // when the unique top level target
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ targets.length,
+ 1,
+ "Without fission/EFT, there is only the top level target"
+ );
+ await commands.destroy();
+ return;
+ }
+ is(targets.length, 2, "retrieved the top level and the iframe targets");
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "the first target is the top level one"
+ );
+ is(targets[1].url, TEST_COM_URL, "the second target is the iframe one");
+
+ // Navigate to the same page with query params
+ info("Load the second page");
+ const secondPageUrl = TEST_COM_URL + "?second-load";
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [secondPageUrl],
+ function (url) {
+ const iframe = content.document.querySelector("iframe");
+ iframe.src = url;
+ }
+ );
+
+ await waitFor(() => targets.length == 3, "wait for the next target");
+ is(targets[2].url, secondPageUrl, "the second target is for the second url");
+ ok(targets[1].isDestroyed(), "the first target is destroyed");
+
+ // Go back to the first page, this should be a bfcache navigation, and,
+ // we should get a new target
+ info("Go back to the first page");
+ const iframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ const iframe = content.document.querySelector("iframe");
+ return iframe.browsingContext;
+ }
+ );
+ await SpecialPowers.spawn(iframeBrowsingContext, [], function () {
+ content.history.back();
+ });
+
+ await waitFor(() => targets.length == 4, "wait for the next target");
+ is(targets[3].url, TEST_COM_URL, "the third target is for the first url");
+ ok(targets[2].isDestroyed(), "the second target is destroyed");
+
+ // Go forward and resurect the second page, this should also be a bfcache navigation, and,
+ // get a new target.
+ info("Go forward to the second page");
+ await SpecialPowers.spawn(iframeBrowsingContext, [], function () {
+ content.history.forward();
+ });
+
+ await waitFor(() => targets.length == 5, "wait for the next target");
+ is(targets[4].url, secondPageUrl, "the 4th target is for the second url");
+ ok(targets[3].isDestroyed(), "the third target is destroyed");
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_browser_workers.js b/devtools/shared/commands/target/tests/browser_target_command_browser_workers.js
new file mode 100644
index 0000000000..181cfa2614
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_browser_workers.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around workers
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const WORKER_FILE = "test_worker.js";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + WORKER_FILE;
+const SERVICE_WORKER_URL = URL_ROOT_SSL + "test_service_worker.js";
+
+add_task(async function () {
+ // Enabled fission's pref as the TargetCommand is almost disabled without it
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ info("Test TargetCommand against workers via the parent process target");
+
+ // Instantiate a worker in the parent process
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker(CHROME_WORKER_URL + "#simple-worker");
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker(CHROME_WORKER_URL + "#shared-worker");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ // Very naive sanity check against getAllTargets([workerType])
+ info("Check that getAllTargets returned the expected targets");
+ const workers = await targetCommand.getAllTargets([TYPES.WORKER]);
+ const hasWorker = workers.find(workerTarget => {
+ return workerTarget.url == CHROME_WORKER_URL + "#simple-worker";
+ });
+ ok(hasWorker, "retrieve the target for the worker");
+
+ const sharedWorkers = await targetCommand.getAllTargets([
+ TYPES.SHARED_WORKER,
+ ]);
+ const hasSharedWorker = sharedWorkers.find(workerTarget => {
+ return workerTarget.url == CHROME_WORKER_URL + "#shared-worker";
+ });
+ ok(hasSharedWorker, "retrieve the target for the shared worker");
+
+ const serviceWorkers = await targetCommand.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ const hasServiceWorker = serviceWorkers.find(workerTarget => {
+ return workerTarget.url == SERVICE_WORKER_URL;
+ });
+ ok(hasServiceWorker, "retrieve the target for the service worker");
+
+ info(
+ "Check that calling getAllTargets again return the same target instances"
+ );
+ const workers2 = await targetCommand.getAllTargets([TYPES.WORKER]);
+ const sharedWorkers2 = await targetCommand.getAllTargets([
+ TYPES.SHARED_WORKER,
+ ]);
+ const serviceWorkers2 = await targetCommand.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ is(workers2.length, workers.length, "retrieved the same number of workers");
+ is(
+ sharedWorkers2.length,
+ sharedWorkers.length,
+ "retrieved the same number of shared workers"
+ );
+ is(
+ serviceWorkers2.length,
+ serviceWorkers.length,
+ "retrieved the same number of service workers"
+ );
+
+ workers.sort(sortFronts);
+ workers2.sort(sortFronts);
+ sharedWorkers.sort(sortFronts);
+ sharedWorkers2.sort(sortFronts);
+ serviceWorkers.sort(sortFronts);
+ serviceWorkers2.sort(sortFronts);
+
+ for (let i = 0; i < workers.length; i++) {
+ is(workers[i], workers2[i], `worker ${i} targets are the same`);
+ }
+ for (let i = 0; i < sharedWorkers2.length; i++) {
+ is(
+ sharedWorkers[i],
+ sharedWorkers2[i],
+ `shared worker ${i} targets are the same`
+ );
+ }
+ for (let i = 0; i < serviceWorkers2.length; i++) {
+ is(
+ serviceWorkers[i],
+ serviceWorkers2[i],
+ `service worker ${i} targets are the same`
+ );
+ }
+
+ info(
+ "Check that watchTargets will call the create callback for all existing workers"
+ );
+ const targets = [];
+ const topLevelTarget = await commands.targetCommand.targetFront;
+ const onAvailable = async ({ targetFront }) => {
+ ok(
+ targetFront.targetType === TYPES.WORKER ||
+ targetFront.targetType === TYPES.SHARED_WORKER ||
+ targetFront.targetType === TYPES.SERVICE_WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER, TYPES.SERVICE_WORKER],
+ onAvailable,
+ });
+ is(
+ targets.length,
+ workers.length + sharedWorkers.length + serviceWorkers.length,
+ "retrieved the same number of workers via watchTargets"
+ );
+
+ targets.sort(sortFronts);
+ const allWorkers = workers
+ .concat(sharedWorkers, serviceWorkers)
+ .sort(sortFronts);
+
+ for (let i = 0; i < allWorkers.length; i++) {
+ is(
+ allWorkers[i],
+ targets[i],
+ `worker ${i} targets are the same via watchTargets`
+ );
+ }
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER, TYPES.SERVICE_WORKER],
+ onAvailable,
+ });
+
+ // Create a new worker and see if the worker target is reported
+ const onWorkerCreated = new Promise(resolve => {
+ const onAvailable2 = async ({ targetFront }) => {
+ if (targets.includes(targetFront)) {
+ return;
+ }
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: onAvailable2,
+ });
+ resolve(targetFront);
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: onAvailable2,
+ });
+ });
+ // eslint-disable-next-line no-unused-vars
+ const worker2 = new Worker(CHROME_WORKER_URL + "#second");
+ info("Wait for the second worker to be created");
+ const workerTarget = await onWorkerCreated;
+
+ is(
+ workerTarget.url,
+ CHROME_WORKER_URL + "#second",
+ "This worker target is about the new worker"
+ );
+ is(
+ workerTarget.name,
+ "test_worker.js#second",
+ "The worker target has the expected name"
+ );
+
+ const workers3 = await targetCommand.getAllTargets([TYPES.WORKER]);
+ const hasWorker2 = workers3.find(
+ ({ url }) => url == `${CHROME_WORKER_URL}#second`
+ );
+ ok(hasWorker2, "retrieve the target for tab via getAllTargets");
+
+ info(
+ "Check that terminating the worker does trigger the onDestroyed callback"
+ );
+ const onWorkerDestroyed = new Promise(resolve => {
+ const emptyFn = () => {};
+ const onDestroyed = ({ targetFront }) => {
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: emptyFn,
+ onDestroyed,
+ });
+ resolve(targetFront);
+ };
+
+ targetCommand.watchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: emptyFn,
+ onDestroyed,
+ });
+ });
+ worker2.terminate();
+ const workerTargetFront = await onWorkerDestroyed;
+ ok(true, "onDestroyed was called when the worker was terminated");
+
+ workerTargetFront.isTopLevel;
+ ok(
+ true,
+ "isTopLevel can be called on the target front after onDestroyed was called"
+ );
+
+ workerTargetFront.name;
+ ok(
+ true,
+ "name can be accessed on the target front after onDestroyed was called"
+ );
+
+ targetCommand.destroy();
+
+ info("Unregister service workers so they don't appear in other tests.");
+ await unregisterAllServiceWorkers(commands.client);
+
+ await commands.destroy();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+function sortFronts(f1, f2) {
+ return f1.actorID < f2.actorID;
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_detach.js b/devtools/shared/commands/target/tests/browser_target_command_detach.js
new file mode 100644
index 0000000000..a0056cd7a5
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_detach.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand's when detaching the top target
+//
+// Do this with the "remote tab" codepath, which will avoid
+// destroying the DevToolsClient when the target is destroyed.
+// Otherwise, with "local tab", the client is closed and everything is destroy
+// on both client and server side.
+
+const TEST_URL = "data:text/html,test-page";
+
+add_task(async function () {
+ info(" ### Test detaching the top target");
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+
+ info("Create a first commands, which will destroy its top target");
+ const commands = await CommandsFactory.forRemoteTab(
+ tab.linkedBrowser.browserId
+ );
+ const targetCommand = commands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await targetCommand.startListening();
+
+ info("Call any target front method, to ensure it works fine");
+ await targetCommand.targetFront.focus();
+
+ // Destroying the target front should end up calling "WindowGlobalTargetActor.detach"
+ // which should destroy the target on the server side
+ await targetCommand.targetFront.destroy();
+
+ info(
+ "Now create a second commands after destroy, to see if we can spawn a new, functional target"
+ );
+ const secondCommands = await CommandsFactory.forRemoteTab(
+ tab.linkedBrowser.browserId,
+ {
+ client: commands.client,
+ }
+ );
+ const secondTargetCommand = secondCommands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await secondTargetCommand.startListening();
+
+ info("Call any target front method, to ensure it works fine");
+ await secondTargetCommand.targetFront.focus();
+
+ BrowserTestUtils.removeTab(tab);
+
+ info("Close the two commands");
+ await commands.destroy();
+ await secondCommands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_frames.js b/devtools/shared/commands/target/tests/browser_target_command_frames.js
new file mode 100644
index 0000000000..6aa0655b64
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_frames.js
@@ -0,0 +1,649 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around frames
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const IFRAME_URL = URL_ROOT_ORG_SSL + "fission_iframe.html";
+const SECOND_PAGE_URL = "https://example.org/document-builder.sjs?html=org";
+
+const PID_REGEXP = /^\d+$/;
+
+add_task(async function () {
+ // Disable bfcache for Fission for now.
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ // Enabled fission prefs
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ // Test fetching the frames from the main process descriptor
+ await testBrowserFrames();
+
+ // Test fetching the frames from a tab descriptor
+ await testTabFrames();
+
+ // Test what happens with documents running in the parent process
+ await testOpeningOnParentProcessDocument();
+ await testNavigationToParentProcessDocument();
+
+ // Test what happens with about:blank documents
+ await testOpeningOnAboutBlankDocument();
+ await testNavigationToAboutBlankDocument();
+
+ await testNestedIframes();
+});
+
+async function testOpeningOnParentProcessDocument() {
+ info("Test opening against a parent process document");
+ const tab = await addTab("about:robots");
+ is(
+ tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid,
+ -1,
+ "The tab is loaded in the parent process"
+ );
+
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, "about:robots", "target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "the target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testNavigationToParentProcessDocument() {
+ info("Test navigating to parent process document");
+ const firstLocation = "data:text/html,foo";
+ const secondLocation = "about:robots";
+
+ const tab = await addTab(firstLocation);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ // When the first top level target is created from the server,
+ // `startListening` emits a spurious switched-target event
+ // which isn't necessarily emited before it resolves.
+ // So ensure waiting for it, otherwise we may resolve too eagerly
+ // in our expected listener.
+ const onSwitchedTarget1 = targetCommand.once("switched-target");
+ await targetCommand.startListening();
+ info("wait for first top level target");
+ await onSwitchedTarget1;
+
+ const firstTarget = targetCommand.targetFront;
+ is(firstTarget.url, firstLocation, "first target url is correct");
+
+ info("Navigate to a parent process page");
+ const onSwitchedTarget = targetCommand.once("switched-target");
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, secondLocation);
+ await onLoaded;
+ is(
+ browser.browsingContext.currentWindowGlobal.osPid,
+ -1,
+ "The tab is loaded in the parent process"
+ );
+
+ await onSwitchedTarget;
+ isnot(targetCommand.targetFront, firstTarget, "got a new target");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, secondLocation, "second target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "second target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testOpeningOnAboutBlankDocument() {
+ info("Test opening against about:blank document");
+ const tab = await addTab("about:blank");
+
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, "about:blank", "target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "the target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testNavigationToAboutBlankDocument() {
+ info("Test navigating to about:blank");
+ const firstLocation = "data:text/html,foo";
+ const secondLocation = "about:blank";
+
+ const tab = await addTab(firstLocation);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ // When the first top level target is created from the server,
+ // `startListening` emits a spurious switched-target event
+ // which isn't necessarily emited before it resolves.
+ // So ensure waiting for it, otherwise we may resolve too eagerly
+ // in our expected listener.
+ const onSwitchedTarget1 = targetCommand.once("switched-target");
+ await targetCommand.startListening();
+ info("wait for first top level target");
+ await onSwitchedTarget1;
+
+ const firstTarget = targetCommand.targetFront;
+ is(firstTarget.url, firstLocation, "first target url is correct");
+
+ info("Navigate to about:blank page");
+ const onSwitchedTarget = targetCommand.once("switched-target");
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, secondLocation);
+ await onLoaded;
+
+ await onSwitchedTarget;
+ isnot(targetCommand.targetFront, firstTarget, "got a new target");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, secondLocation, "second target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "second target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testBrowserFrames() {
+ info("Test TargetCommand against frames via the parent process target");
+
+ const aboutBlankTab = await addTab("about:blank");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ // Very naive sanity check against getAllTargets([frame])
+ const frames = await targetCommand.getAllTargets([TYPES.FRAME]);
+ const hasBrowserDocument = frames.find(
+ frameTarget => frameTarget.url == window.location.href
+ );
+ ok(hasBrowserDocument, "retrieve the target for the browser document");
+
+ const hasAboutBlankDocument = frames.find(
+ frameTarget =>
+ frameTarget.browsingContextID ==
+ aboutBlankTab.linkedBrowser.browsingContext.id
+ );
+ ok(hasAboutBlankDocument, "retrieve the target for the about:blank tab");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames2 = await targetCommand.getAllTargets([TYPES.FRAME]);
+ is(frames2.length, frames.length, "retrieved the same number of frames");
+
+ function sortFronts(f1, f2) {
+ return f1.actorID < f2.actorID;
+ }
+ frames.sort(sortFronts);
+ frames2.sort(sortFronts);
+ for (let i = 0; i < frames.length; i++) {
+ is(frames[i], frames2[i], `frame ${i} targets are the same`);
+ }
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const topLevelTarget = targetCommand.targetFront;
+
+ const noParentTarget = await topLevelTarget.getParentTarget();
+ is(noParentTarget, null, "The top level target has no parent target");
+
+ const onAvailable = ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ ok(
+ PID_REGEXP.test(targetFront.processID),
+ `Target has processID of expected shape (${targetFront.processID})`
+ );
+ targets.push(targetFront);
+ };
+ await targetCommand.watchTargets({ types: [TYPES.FRAME], onAvailable });
+ is(
+ targets.length,
+ frames.length,
+ "retrieved the same number of frames via watchTargets"
+ );
+
+ frames.sort(sortFronts);
+ targets.sort(sortFronts);
+ for (let i = 0; i < frames.length; i++) {
+ is(
+ frames[i],
+ targets[i],
+ `frame ${i} targets are the same via watchTargets`
+ );
+ }
+
+ async function addTabAndAssertNewTarget(url) {
+ const previousTargetCount = targets.length;
+ const tab = await addTab(url);
+ await waitFor(
+ () => targets.length == previousTargetCount + 1,
+ "Wait for all expected targets after tab opening"
+ );
+ is(
+ targets.length,
+ previousTargetCount + 1,
+ "Opening a tab reported a new frame"
+ );
+ const newTabTarget = targets.at(-1);
+ is(newTabTarget.url, url, "This frame target is about the new tab");
+ // Internaly, the tab, which uses a <browser type='content'> element is considered detached from their owner document
+ // and so the target is having a null parentInnerWindowId. But the framework will attach all non-top-level targets
+ // as children of the top level.
+ const tabParentTarget = await newTabTarget.getParentTarget();
+ is(
+ tabParentTarget,
+ targetCommand.targetFront,
+ "tab's WindowGlobal/BrowsingContext is detached and has no parent, but we report them as children of the top level target"
+ );
+
+ const frames3 = await targetCommand.getAllTargets([TYPES.FRAME]);
+ const hasTabDocument = frames3.find(target => target.url == url);
+ ok(hasTabDocument, "retrieve the target for tab via getAllTargets");
+
+ return tab;
+ }
+
+ info("Open a tab loaded in content process");
+ await addTabAndAssertNewTarget("data:text/html,content-process-page");
+
+ info("Open a tab loaded in the parent process");
+ const parentProcessTab = await addTabAndAssertNewTarget("about:robots");
+ is(
+ parentProcessTab.linkedBrowser.browsingContext.currentWindowGlobal.osPid,
+ -1,
+ "The tab is loaded in the parent process"
+ );
+
+ info("Open a new content window via window.open");
+ info("First open a tab on .org domain");
+ const tabUrl = "https://example.org/document-builder.sjs?html=org";
+ await addTabAndAssertNewTarget(tabUrl);
+ const previousTargetCount = targets.length;
+
+ info("Then open a popup on .com domain");
+ const popupUrl = "https://example.com/document-builder.sjs?html=com";
+ const onPopupOpened = BrowserTestUtils.waitForNewTab(gBrowser, popupUrl);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [popupUrl], async url => {
+ content.window.open(url, "_blank");
+ });
+ await onPopupOpened;
+
+ await waitFor(
+ () => targets.length == previousTargetCount + 1,
+ "Wait for all expected targets after window.open()"
+ );
+ is(
+ targets.length,
+ previousTargetCount + 1,
+ "Opening a new content window reported a new frame"
+ );
+ is(
+ targets.at(-1).url,
+ popupUrl,
+ "This frame target is about the new content window"
+ );
+
+ // About:blank are a bit special because we ignore a transcient about:blank
+ // document when navigating to another process. But we should not ignore
+ // tabs, loading a real, final about:blank document.
+ info("Open a tab with about:blank");
+ await addTabAndAssertNewTarget("about:blank");
+
+ // Until we start spawning target for all WindowGlobals,
+ // including the one running in the same process as their parent,
+ // we won't create dedicated target for new top level windows.
+ // Instead, these document will be debugged via the ParentProcessTargetActor.
+ info("Open a top level chrome window");
+ const expectedTargets = targets.length;
+ const chromeWindow = Services.ww.openWindow(
+ null,
+ "about:robots",
+ "_blank",
+ "chrome",
+ null
+ );
+ await wait(250);
+ is(
+ targets.length,
+ expectedTargets,
+ "New top level window shouldn't spawn new target"
+ );
+ chromeWindow.close();
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ targetCommand.destroy();
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ await commands.destroy();
+}
+
+async function testTabFrames(mainRoot) {
+ info("Test TargetCommand against frames via a tab target");
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(FISSION_TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([TYPES.FRAME]);
+ // When fission is enabled, we also get the remote example.org iframe.
+ const expectedFramesCount =
+ isFissionEnabled() || isEveryFrameTargetEnabled() ? 2 : 1;
+ is(
+ frames.length,
+ expectedFramesCount,
+ "retrieved the expected number of targets"
+ );
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const destroyedTargets = [];
+ const topLevelTarget = targetCommand.targetFront;
+ const onAvailable = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ PID_REGEXP.test(targetFront.processID),
+ `Target has processID of expected shape (${targetFront.processID})`
+ );
+ targets.push({ targetFront, isTargetSwitching });
+ };
+ const onDestroyed = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ destroyedTargets.push({ targetFront, isTargetSwitching });
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ is(
+ targets.length,
+ frames.length,
+ "retrieved the same number of frames via watchTargets"
+ );
+ is(destroyedTargets.length, 0, "Should be no destroyed target initialy");
+
+ for (const frame of frames) {
+ ok(
+ targets.find(({ targetFront }) => targetFront === frame),
+ "frame " + frame.actorID + " target is the same via watchTargets"
+ );
+ }
+ is(
+ targets[0].targetFront.url,
+ FISSION_TEST_URL,
+ "First target should be the top document one"
+ );
+ is(
+ targets[0].targetFront.isTopLevel,
+ true,
+ "First target is a top level one"
+ );
+ is(
+ !targets[0].isTargetSwitching,
+ true,
+ "First target is not considered as a target switching"
+ );
+ const noParentTarget = await targets[0].targetFront.getParentTarget();
+ is(noParentTarget, null, "The top level target has no parent target");
+
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ is(
+ targets[1].targetFront.url,
+ IFRAME_URL,
+ "Second target should be the iframe one"
+ );
+ is(
+ !targets[1].targetFront.isTopLevel,
+ true,
+ "Iframe target isn't top level"
+ );
+ is(
+ !targets[1].isTargetSwitching,
+ true,
+ "Iframe target isn't a target swich"
+ );
+ const parentTarget = await targets[1].targetFront.getParentTarget();
+ is(
+ parentTarget,
+ targets[0].targetFront,
+ "The parent target for the iframe is the top level target"
+ );
+ }
+
+ // Before navigating to another process, ensure cleaning up everything from the first page
+ await waitForAllTargetsToBeAttached(targetCommand);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+
+ info("Navigate to another domain and process (if fission is enabled)");
+ // When a new target will be created, we need to wait until it's fully processed
+ // to avoid pending promises.
+ const onNewTargetProcessed = targetCommand.once("processed-available-target");
+
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, SECOND_PAGE_URL);
+ await onLoaded;
+
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ const afterNavigationFramesCount = 3;
+ await waitFor(
+ () => targets.length == afterNavigationFramesCount,
+ "Wait for all expected targets after navigation"
+ );
+ is(
+ targets.length,
+ afterNavigationFramesCount,
+ "retrieved all targets after navigation"
+ );
+ // As targetFront.url isn't reliable and might be about:blank,
+ // try to assert that we got the right target via other means.
+ // outerWindowID should change when navigating to another process,
+ // while it would stay equal for in-process navigations.
+ is(
+ targets[2].targetFront.outerWindowID,
+ browser.outerWindowID,
+ "The new target should be the newly loaded document"
+ );
+ is(
+ targets[2].isTargetSwitching,
+ true,
+ "and should be flagged as a target switching"
+ );
+
+ is(
+ destroyedTargets.length,
+ 2,
+ "The two existing targets should be destroyed"
+ );
+ is(
+ destroyedTargets[0].targetFront,
+ targets[1].targetFront,
+ "The first destroyed should be the iframe one"
+ );
+ is(
+ destroyedTargets[0].isTargetSwitching,
+ false,
+ "the target destruction is not flagged as target switching for iframes"
+ );
+ is(
+ destroyedTargets[1].targetFront,
+ targets[0].targetFront,
+ "The second destroyed should be the previous top level one (because it is delayed to be fired *after* will-navigate)"
+ );
+ is(
+ destroyedTargets[1].isTargetSwitching,
+ true,
+ "the target destruction is flagged as target switching"
+ );
+ } else {
+ await waitFor(
+ () => targets.length == 2,
+ "Wait for all expected targets after navigation"
+ );
+ is(
+ destroyedTargets.length,
+ 1,
+ "with JSWindowActor based target, the top level target is destroyed"
+ );
+ is(
+ targetCommand.targetFront,
+ targets[1].targetFront,
+ "we got a new target"
+ );
+ ok(
+ !targetCommand.targetFront.isDestroyed(),
+ "that target is not destroyed"
+ );
+ ok(
+ targets[0].targetFront.isDestroyed(),
+ "but the previous one is destroyed"
+ );
+ }
+
+ await onNewTargetProcessed;
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ targetCommand.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testNestedIframes() {
+ info("Test TargetCommand against nested frames");
+
+ const nestedIframeUrl = `https://example.com/document-builder.sjs?html=${encodeURIComponent(
+ "<title>second</title><h3>second level iframe</h3>"
+ )}&delay=500`;
+
+ const testUrl = `data:text/html;charset=utf-8,
+ <h1>Top-level</h1>
+ <iframe id=first-level
+ src='data:text/html;charset=utf-8,${encodeURIComponent(
+ `<title>first</title><h2>first level iframe</h2><iframe id=second-level src="${nestedIframeUrl}"></iframe>`
+ )}'
+ ></iframe>`;
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(testUrl);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([TYPES.FRAME]);
+
+ is(frames[0], targetCommand.targetFront, "First target is the top level one");
+ const topParent = await frames[0].getParentTarget();
+ is(topParent, null, "Top level target has no parent");
+
+ if (isEveryFrameTargetEnabled()) {
+ const firstIframeTarget = frames.find(target => target.title == "first");
+ ok(
+ firstIframeTarget,
+ "With EFT, got the target for the first level iframe"
+ );
+ const firstParent = await firstIframeTarget.getParentTarget();
+ is(
+ firstParent,
+ targetCommand.targetFront,
+ "With EFT, first level has top level target as parent"
+ );
+
+ const secondIframeTarget = frames.find(target => target.title == "second");
+ ok(secondIframeTarget, "Got the target for the second level iframe");
+ const secondParent = await secondIframeTarget.getParentTarget();
+ is(
+ secondParent,
+ firstIframeTarget,
+ "With EFT, second level has the first level target as parent"
+ );
+ } else if (isFissionEnabled()) {
+ const secondIframeTarget = frames.find(target => target.title == "second");
+ ok(secondIframeTarget, "Got the target for the second level iframe");
+ const secondParent = await secondIframeTarget.getParentTarget();
+ is(
+ secondParent,
+ targetCommand.targetFront,
+ "With fission, second level has top level target as parent"
+ );
+ }
+
+ await commands.destroy();
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_frames_popups.js b/devtools/shared/commands/target/tests/browser_target_command_frames_popups.js
new file mode 100644
index 0000000000..68f7244671
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_frames_popups.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we create targets for popups
+
+const TEST_URL = "https://example.org/document-builder.sjs?html=main page";
+const POPUP_URL = "https://example.com/document-builder.sjs?html=popup";
+const POPUP_SECOND_URL =
+ "https://example.com/document-builder.sjs?html=popup-navigated";
+
+add_task(async function () {
+ await pushPref("devtools.popups.debug", true);
+ // We expect to create a target for a same-process iframe
+ // in the test against window.open to load a document in an iframe.
+ await pushPref("devtools.every-frame-target.enabled", true);
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = ({ targetFront }) => {
+ targets.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ destroyedTargets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ is(targets.length, 1, "At first, we only get one target");
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "And this target is the top level one"
+ );
+
+ info("Open a popup");
+ const firstPopupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [POPUP_URL],
+ url => {
+ const win = content.open(url);
+ return win.browsingContext;
+ }
+ );
+
+ await waitFor(() => targets.length === 2);
+ ok(true, "We are notified about the first popup's target");
+
+ is(
+ targets[1].browsingContextID,
+ firstPopupBrowsingContext.id,
+ "the new target is for the popup"
+ );
+ is(targets[1].url, POPUP_URL, "the new target has the right url");
+
+ info("Navigate the popup to a second location");
+ await SpecialPowers.spawn(
+ firstPopupBrowsingContext,
+ [POPUP_SECOND_URL],
+ url => {
+ content.location.href = url;
+ }
+ );
+
+ await waitFor(() => targets.length === 3);
+ ok(true, "We are notified about the new location popup's target");
+
+ await waitFor(() => destroyedTargets.length === 1);
+ ok(true, "The first popup's target is destroyed");
+ is(
+ destroyedTargets[0],
+ targets[1],
+ "The destroyed target is the popup's one"
+ );
+
+ is(
+ targets[2].browsingContextID,
+ firstPopupBrowsingContext.id,
+ "the new location target is for the popup"
+ );
+ is(
+ targets[2].url,
+ POPUP_SECOND_URL,
+ "the new location target has the right url"
+ );
+
+ info("Close the popup");
+ await SpecialPowers.spawn(firstPopupBrowsingContext, [], () => {
+ content.close();
+ });
+
+ await waitFor(() => destroyedTargets.length === 2);
+ ok(true, "The popup's target is destroyed");
+ is(
+ destroyedTargets[1],
+ targets[2],
+ "The destroyed target is the popup's one"
+ );
+
+ info("Open a about:blank popup");
+ const aboutBlankPopupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ const win = content.open("about:blank");
+ return win.browsingContext;
+ }
+ );
+
+ await waitFor(() => targets.length === 4);
+ ok(true, "We are notified about the about:blank popup's target");
+
+ is(
+ targets[3].browsingContextID,
+ aboutBlankPopupBrowsingContext.id,
+ "the new target is for the popup"
+ );
+ is(targets[3].url, "about:blank", "the new target has the right url");
+
+ info("Select the original tab and reload it");
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.reloadTab(tab);
+
+ await waitFor(() => targets.length === 5);
+ is(targets[4], targetCommand.targetFront, "We get a new top level target");
+ ok(!targets[3].isDestroyed(), "The about:blank popup target is still alive");
+
+ info("Call about:blank popup method to ensure it really is functional");
+ await targets[3].logInPage("foo");
+
+ info(
+ "Ensure that iframe using window.open to load their document aren't considered as popups"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.setAttribute("name", "test-iframe");
+ content.document.documentElement.appendChild(iframe);
+ content.open("data:text/html,iframe", "test-iframe");
+ });
+ await waitFor(() => targets.length === 6);
+ is(
+ targets[5].targetForm.isPopup,
+ false,
+ "The iframe target isn't considered as a popup"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ targetCommand.destroy();
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js b/devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js
new file mode 100644
index 0000000000..d05ff5a962
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the framework handles reloading a document with multiple remote frames (See Bug 1724909).
+
+const REMOTE_ORIGIN = "https://example.com/";
+const REMOTE_IFRAME_URL_1 =
+ REMOTE_ORIGIN + "/document-builder.sjs?html=first_remote_iframe";
+const REMOTE_IFRAME_URL_2 =
+ REMOTE_ORIGIN + "/document-builder.sjs?html=second_remote_iframe";
+const TEST_URL =
+ "https://example.org/document-builder.sjs?html=org" +
+ `<iframe src=${REMOTE_IFRAME_URL_1}></iframe>` +
+ `<iframe src=${REMOTE_IFRAME_URL_2}></iframe>`;
+
+add_task(async function () {
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = ({ targetFront }) => {
+ targets.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ destroyedTargets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ await waitFor(() => targets.length === 3);
+ ok(
+ true,
+ "We are notified about the top-level document and the 2 remote iframes"
+ );
+
+ info("Reload the page");
+ // When a new target will be created, we need to wait until it's fully processed
+ // to avoid pending promises.
+ const onNewTargetProcessed = targetCommand.once("processed-available-target");
+ gBrowser.reloadTab(tab);
+ await onNewTargetProcessed;
+
+ await waitFor(() => targets.length === 6 && destroyedTargets.length === 3);
+
+ // Get the previous targets in a dedicated array and remove them from `targets`
+ const previousTargets = targets.splice(0, 3);
+ ok(
+ previousTargets.every(targetFront => targetFront.isDestroyed()),
+ "The previous targets are all destroyed"
+ );
+ ok(
+ targets.every(targetFront => !targetFront.isDestroyed()),
+ "The new targets are not destroyed"
+ );
+
+ info("Reload one of the iframe");
+ SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const iframeEl = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframeEl.browsingContext, [], () => {
+ content.document.location.reload();
+ });
+ });
+ await waitFor(
+ () =>
+ targets.length + previousTargets.length === 7 &&
+ destroyedTargets.length === 4
+ );
+ const iframeTarget = targets.find(t => t === destroyedTargets.at(-1));
+ ok(iframeTarget, "Got the iframe target that got destroyed");
+ for (const target of targets) {
+ if (target == iframeTarget) {
+ ok(
+ target.isDestroyed(),
+ "The iframe target we navigated from is destroyed"
+ );
+ } else {
+ ok(
+ !target.isDestroyed(),
+ `Target ${target.actorID}|${target.url} isn't destroyed`
+ );
+ }
+ }
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ targetCommand.destroy();
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js b/devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js
new file mode 100644
index 0000000000..a7d5e51b3c
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API getAllTargets.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + "test_worker.js";
+
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ info("Setup the test page with workers of all types");
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ // Instantiate a worker in the parent process
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker(CHROME_WORKER_URL + "#simple-worker");
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker(CHROME_WORKER_URL + "#shared-worker");
+
+ info("Create a target list for the main process target");
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ info("Check getAllTargets will throw when providing invalid arguments");
+ Assert.throws(
+ () => targetCommand.getAllTargets(),
+ e => e.message === "getAllTargets expects a non-empty array of types"
+ );
+
+ Assert.throws(
+ () => targetCommand.getAllTargets([]),
+ e => e.message === "getAllTargets expects a non-empty array of types"
+ );
+
+ info("Check getAllTargets returns consistent results with several types");
+ const workerTargets = targetCommand.getAllTargets([TYPES.WORKER]);
+ const serviceWorkerTargets = targetCommand.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ const sharedWorkerTargets = targetCommand.getAllTargets([
+ TYPES.SHARED_WORKER,
+ ]);
+ const processTargets = targetCommand.getAllTargets([TYPES.PROCESS]);
+ const frameTargets = targetCommand.getAllTargets([TYPES.FRAME]);
+
+ const allWorkerTargetsReference = [
+ ...workerTargets,
+ ...serviceWorkerTargets,
+ ...sharedWorkerTargets,
+ ];
+ const allWorkerTargets = targetCommand.getAllTargets([
+ TYPES.WORKER,
+ TYPES.SERVICE_WORKER,
+ TYPES.SHARED_WORKER,
+ ]);
+
+ is(
+ allWorkerTargets.length,
+ allWorkerTargetsReference.length,
+ "getAllTargets([worker, service, shared]) returned the expected number of targets"
+ );
+
+ ok(
+ allWorkerTargets.every(t => allWorkerTargetsReference.includes(t)),
+ "getAllTargets([worker, service, shared]) returned the expected targets"
+ );
+
+ const allTargetsReference = [
+ ...allWorkerTargets,
+ ...processTargets,
+ ...frameTargets,
+ ];
+ const allTargets = targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(
+ allTargets.length,
+ allTargetsReference.length,
+ "getAllTargets(ALL_TYPES) returned the expected number of targets"
+ );
+
+ ok(
+ allTargets.every(t => allTargetsReference.includes(t)),
+ "getAllTargets(ALL_TYPES) returned the expected targets"
+ );
+
+ for (const target of allTargets) {
+ is(
+ target.commands,
+ commands,
+ "Each target front has a `commands` attribute - " + target
+ );
+ }
+
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ ok(
+ !targetCommand.isDestroyed(),
+ "TargetCommand isn't destroyed before calling commands.destroy()"
+ );
+ await commands.destroy();
+ ok(
+ targetCommand.isDestroyed(),
+ "TargetCommand is destroyed after calling commands.destroy()"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js b/devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js
new file mode 100644
index 0000000000..dbdaae7f05
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test watch/unwatchTargets throw when provided with invalid types.
+
+const TEST_URL = "data:text/html;charset=utf-8,invalid api usage test";
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab(TEST_URL);
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ const onAvailable = function () {};
+
+ await Assert.rejects(
+ targetCommand.watchTargets({ types: [null], onAvailable }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for null type"
+ );
+
+ await Assert.rejects(
+ targetCommand.watchTargets({ types: [undefined], onAvailable }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for undefined type"
+ );
+
+ await Assert.rejects(
+ targetCommand.watchTargets({ types: ["NOT_A_TARGET"], onAvailable }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for unknown type"
+ );
+
+ await Assert.rejects(
+ targetCommand.watchTargets({
+ types: [targetCommand.TYPES.FRAME, "NOT_A_TARGET"],
+ onAvailable,
+ }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for unknown type mixed with a correct type"
+ );
+
+ Assert.throws(
+ () => targetCommand.unwatchTargets({ types: [null], onAvailable }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for null type"
+ );
+
+ Assert.throws(
+ () => targetCommand.unwatchTargets({ types: [undefined], onAvailable }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for undefined type"
+ );
+
+ Assert.throws(
+ () =>
+ targetCommand.unwatchTargets({ types: ["NOT_A_TARGET"], onAvailable }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for unknown type"
+ );
+
+ Assert.throws(
+ () =>
+ targetCommand.unwatchTargets({
+ types: [targetCommand.TYPES.CONSOLE_MESSAGE, "NOT_A_TARGET"],
+ onAvailable,
+ }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for unknown type mixed with a correct type"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_processes.js b/devtools/shared/commands/target/tests/browser_target_command_processes.js
new file mode 100644
index 0000000000..d4f57ae036
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_processes.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around processes
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Enabled fission's pref as the TargetCommand is almost disabled without it
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ await testProcesses(targetCommand, targetCommand.targetFront);
+
+ targetCommand.destroy();
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
+ );
+
+ await commands.destroy();
+});
+
+add_task(async function () {
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const created = [];
+ const destroyed = [];
+ const onAvailable = ({ targetFront }) => {
+ created.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ destroyed.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [targetCommand.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+ Assert.greater(created.length, 1, "We get many content process targets");
+
+ targetCommand.stopListening();
+
+ await waitFor(
+ () => created.length == destroyed.length,
+ "Wait for the destruction of all content process targets when calling stopListening"
+ );
+ is(
+ created.length,
+ destroyed.length,
+ "Got notification of destruction for all previously reported targets"
+ );
+
+ targetCommand.destroy();
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
+ );
+
+ await commands.destroy();
+});
+
+async function testProcesses(targetCommand, target) {
+ info("Test TargetCommand against processes");
+ const { TYPES } = targetCommand;
+
+ // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
+ const originalProcessesCount = Services.ppmm.childCount - 1;
+ const processes = await targetCommand.getAllTargets([TYPES.PROCESS]);
+ is(
+ processes.length,
+ originalProcessesCount,
+ "Get a target for all content processes"
+ );
+
+ const processes2 = await targetCommand.getAllTargets([TYPES.PROCESS]);
+ is(
+ processes2.length,
+ originalProcessesCount,
+ "retrieved the same number of processes"
+ );
+ function sortFronts(f1, f2) {
+ return f1.actorID < f2.actorID;
+ }
+ processes.sort(sortFronts);
+ processes2.sort(sortFronts);
+ for (let i = 0; i < processes.length; i++) {
+ is(processes[i], processes2[i], `process ${i} targets are the same`);
+ }
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = new Set();
+
+ const pidRegExp = /^\d+$/;
+
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ ok(
+ pidRegExp.test(targetFront.processID),
+ `Target has processID of expected shape (${targetFront.processID})`
+ );
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ if (!targets.has(targetFront)) {
+ ok(
+ false,
+ "A target is declared destroyed via onDestroy without being notified via onAvailable"
+ );
+ }
+ is(
+ targetFront.targetType,
+ TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ !targetFront.isTopLevel,
+ "We are never notified about the top level target destruction"
+ );
+ targets.delete(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+ is(
+ targets.size,
+ originalProcessesCount,
+ "retrieved the same number of processes via watchTargets"
+ );
+ for (let i = 0; i < processes.length; i++) {
+ ok(
+ targets.has(processes[i]),
+ `process ${i} targets are the same via watchTargets`
+ );
+ }
+
+ const previousTargets = new Set(targets);
+ // Assert that onAvailable is called for processes created *after* the call to watchTargets
+ const onProcessCreated = new Promise(resolve => {
+ const onAvailable2 = ({ targetFront }) => {
+ if (previousTargets.has(targetFront)) {
+ return;
+ }
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable2,
+ });
+ resolve(targetFront);
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable2,
+ });
+ });
+ const tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+ const createdTarget = await onProcessCreated;
+ // For some reason, creating a new tab purges processes created from previous tests
+ // so it is not reasonable to assert the size of `targets` as it may be lower than expected.
+ ok(targets.has(createdTarget), "The new tab process is in the list");
+
+ const processCountAfterTabOpen = targets.size;
+
+ // Assert that onDestroy is called for destroyed processes
+ const onProcessDestroyed = new Promise(resolve => {
+ const onAvailable3 = () => {};
+ const onDestroyed3 = ({ targetFront }) => {
+ resolve(targetFront);
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: onDestroyed3,
+ });
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: onDestroyed3,
+ });
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ const destroyedTarget = await onProcessDestroyed;
+ is(
+ targets.size,
+ processCountAfterTabOpen - 1,
+ "The closed tab's process has been reported as destroyed"
+ );
+ ok(
+ !targets.has(destroyedTarget),
+ "The destroyed target is no longer in the list"
+ );
+ is(
+ destroyedTarget,
+ createdTarget,
+ "The destroyed target is the one that has been reported as created"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+
+ // Ensure that getAllTargets still works after the call to unwatchTargets
+ const processes3 = await targetCommand.getAllTargets([TYPES.PROCESS]);
+ is(
+ processes3.length,
+ processCountAfterTabOpen - 1,
+ "getAllTargets reports a new target"
+ );
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_reload.js b/devtools/shared/commands/target/tests/browser_target_command_reload.js
new file mode 100644
index 0000000000..9d8cacd23d
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_reload.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand's reload method
+//
+// Note that we reload against main process,
+// but this is hard/impossible to test as it reloads the test script itself
+// and so stops its execution.
+
+// Load a page with a JS script that change its value everytime we load it
+// (that's to see if the reload loads from cache or not)
+const TEST_URL = URL_ROOT + "incremental-js-value-script.sjs";
+
+add_task(async function () {
+ info(" ### Test reloading a Tab");
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await targetCommand.startListening();
+
+ const firstJSValue = await getContentVariable();
+ is(firstJSValue, "1", "Got an initial value for the JS variable");
+
+ const onReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await targetCommand.reloadTopLevelTarget();
+ info("Wait for the tab to be reloaded");
+ await onReloaded;
+
+ const secondJSValue = await getContentVariable();
+ is(
+ secondJSValue,
+ "1",
+ "The first reload didn't bypass the cache, so the JS Script is the same and we got the same value"
+ );
+
+ const onSecondReloaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ await targetCommand.reloadTopLevelTarget(true);
+ info("Wait for the tab to be reloaded");
+ await onSecondReloaded;
+
+ // The value is 3 and not 2, because we got a HTTP request, but it returned 304 and the browser fetched his cached content
+ const thirdJSValue = await getContentVariable();
+ is(
+ thirdJSValue,
+ "3",
+ "The second reload did bypass the cache, so the JS Script is different and we got a new value"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+});
+
+add_task(async function () {
+ info(" ### Test reloading an Add-on");
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ background() {
+ const { browser } = this;
+ browser.test.log("background script executed");
+ },
+ });
+
+ await extension.startup();
+
+ const commands = await CommandsFactory.forAddon(extension.id);
+ const targetCommand = commands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await targetCommand.startListening();
+
+ const { onResource: onReloaded } =
+ await commands.resourceCommand.waitForNextResource(
+ commands.resourceCommand.TYPES.DOCUMENT_EVENT,
+ {
+ ignoreExistingResources: true,
+ predicate(resource) {
+ return resource.name == "dom-loading";
+ },
+ }
+ );
+
+ const backgroundPageURL = targetCommand.targetFront.url;
+ ok(backgroundPageURL, "Got the background page URL");
+ await targetCommand.reloadTopLevelTarget();
+
+ info("Wait for next dom-loading DOCUMENT_EVENT");
+ const event = await onReloaded;
+
+ // If we get about:blank here, it most likely means we receive notification
+ // for the previous background page being unload and navigating to about:blank
+ is(
+ event.url,
+ backgroundPageURL,
+ "We received the DOCUMENT_EVENT's for the expected document: the new background page."
+ );
+
+ await commands.destroy();
+
+ await extension.unload();
+});
+function getContentVariable() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.jsValue;
+ });
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_scope_flag.js b/devtools/shared/commands/target/tests/browser_target_command_scope_flag.js
new file mode 100644
index 0000000000..65d9e9a622
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_scope_flag.js
@@ -0,0 +1,190 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API with changes made to devtools.browsertoolbox.scope
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Do not run this test when both fission and EFT is disabled as it changes
+ // the number of targets
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ return;
+ }
+
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ // First test with multiprocess debugging enabled
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+ const { TYPES } = targetCommand;
+
+ const targets = new Set();
+ const destroyedTargetIsModeSwitchingMap = new Map();
+ const onAvailable = async ({ targetFront }) => {
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront, isModeSwitching }) => {
+ destroyedTargetIsModeSwitchingMap.set(targetFront, isModeSwitching);
+ targets.delete(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.PROCESS, TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ Assert.greater(targets.size, 1, "We get many targets");
+
+ info("Open a tab in a new content process");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ const newTabProcessID =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .osPid;
+ const newTabInnerWindowId =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .innerWindowId;
+
+ info("Wait for the tab content process target");
+ const processTarget = await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.PROCESS &&
+ target.processID == newTabProcessID
+ )
+ );
+
+ info("Wait for the tab window global target");
+ const windowGlobalTarget = await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == newTabInnerWindowId
+ )
+ );
+
+ let multiprocessTargetCount = targets.size;
+
+ info("Disable multiprocess debugging");
+ await pushPref("devtools.browsertoolbox.scope", "parent-process");
+
+ info("Wait for all targets but top level and workers to be destroyed");
+ await waitFor(() =>
+ [...targets].every(
+ target =>
+ target == targetCommand.targetFront || target.targetType == TYPES.WORKER
+ )
+ );
+
+ ok(processTarget.isDestroyed(), "The process target is destroyed");
+ ok(
+ destroyedTargetIsModeSwitchingMap.get(processTarget),
+ "isModeSwitching was passed to onTargetDestroyed and is true for the process target"
+ );
+ ok(windowGlobalTarget.isDestroyed(), "The window global target is destroyed");
+ ok(
+ destroyedTargetIsModeSwitchingMap.get(windowGlobalTarget),
+ "isModeSwitching was passed to onTargetDestroyed and is true for the window global target"
+ );
+
+ info("Open a second tab in a new content process");
+ const parentProcessTargetCount = targets.size;
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ await wait(1000);
+ is(
+ parentProcessTargetCount,
+ targets.size,
+ "The new tab process should be ignored and no target be created"
+ );
+
+ info("Re-enable multiprocess debugging");
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // The second tab relates to one content process target and one window global target
+ multiprocessTargetCount += 2;
+
+ await waitFor(
+ () => targets.size == multiprocessTargetCount,
+ "Wait for all targets we used to have before disable multiprocess debugging"
+ );
+
+ info("Wait for the tab content process target to be available again");
+ ok(
+ [...targets].some(
+ target =>
+ target.targetType == TYPES.PROCESS &&
+ target.processID == newTabProcessID
+ ),
+ "We have the tab content process target"
+ );
+
+ info("Wait for the tab window global target to be available again");
+ ok(
+ [...targets].some(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == newTabInnerWindowId
+ ),
+ "We have the tab window global target"
+ );
+
+ info("Open a third tab in a new content process");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ const thirdTabProcessID =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .osPid;
+ const thirdTabInnerWindowId =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .innerWindowId;
+
+ info("Wait for the third tab content process target");
+ await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.PROCESS &&
+ target.processID == thirdTabProcessID
+ )
+ );
+
+ info("Wait for the third tab window global target");
+ await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == thirdTabInnerWindowId
+ )
+ );
+
+ targetCommand.destroy();
+
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
+ );
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_service_workers.js b/devtools/shared/commands/target/tests/browser_target_command_service_workers.js
new file mode 100644
index 0000000000..d71401fd8c
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_service_workers.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API for service workers in content tabs.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ info("Setup the test page with workers of all types");
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ // Enable Service Worker listening.
+ targetCommand.listenForServiceWorkers = true;
+ await targetCommand.startListening();
+
+ const serviceWorkerTargets = targetCommand.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ is(
+ serviceWorkerTargets.length,
+ 1,
+ "TargetCommmand has 1 service worker target"
+ );
+
+ info("Check that the onAvailable is done when watchTargets resolves");
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ // Wait for one second here to check that watch targets waits for
+ // the onAvailable callbacks correctly.
+ await wait(1000);
+ targets.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) =>
+ targets.splice(targets.indexOf(targetFront), 1);
+
+ await targetCommand.watchTargets({
+ types: [TYPES.SERVICE_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ is(targets.length, 1, "onAvailable has resolved");
+ is(
+ targets[0],
+ serviceWorkerTargets[0],
+ "onAvailable was called with the expected service worker target"
+ );
+
+ info("Unregister the worker and wait until onDestroyed is called.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+ await waitUntil(() => targets.length === 0);
+
+ // Stop listening to avoid worker related requests
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js b/devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js
new file mode 100644
index 0000000000..7bf6c856c2
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js
@@ -0,0 +1,358 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API for service workers when navigating in content tabs.
+// When the top level target navigates, we manually call onTargetAvailable for
+// service workers which now match the page domain. We assert that the callbacks
+// will be called the expected number of times here.
+
+const COM_PAGE_URL = URL_ROOT_SSL + "test_sw_page.html";
+const COM_WORKER_URL = URL_ROOT_SSL + "test_sw_page_worker.js";
+const ORG_PAGE_URL = URL_ROOT_ORG_SSL + "test_sw_page.html";
+const ORG_WORKER_URL = URL_ROOT_ORG_SSL + "test_sw_page_worker.js";
+
+/**
+ * This test will navigate between two pages, both controlled by different
+ * service workers.
+ *
+ * The steps will be:
+ * - navigate to .com page
+ * - create target list
+ * -> onAvailable should be called for the .com worker
+ * - navigate to .org page
+ * -> onAvailable should be called for the .org worker
+ * - reload .org page
+ * -> nothing should happen
+ * - unregister .org worker
+ * -> onDestroyed should be called for the .org worker
+ * - navigate back to .com page
+ * -> nothing should happen
+ * - unregister .com worker
+ * -> onDestroyed should be called for the .com worker
+ */
+add_task(async function test_NavigationBetweenTwoDomains_NoDestroy() {
+ await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ const { hooks, commands, targetCommand } = await watchServiceWorkerTargets(
+ tab
+ );
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 0,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Go to .org page, wait for onAvailable to be called");
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ ORG_PAGE_URL
+ );
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 0,
+ targets: [COM_WORKER_URL, ORG_WORKER_URL],
+ });
+
+ info("Reload .org page, onAvailable and onDestroyed should not be called");
+ await BrowserTestUtils.reloadTab(gBrowser.selectedTab);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 0,
+ targets: [COM_WORKER_URL, ORG_WORKER_URL],
+ });
+
+ info("Unregister .org service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(ORG_WORKER_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Go back to .com page");
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ COM_PAGE_URL
+ );
+ await onBrowserLoaded;
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Unregister .com service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(COM_WORKER_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 2,
+ targets: [],
+ });
+
+ // Stop listening to avoid worker related requests
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+ await removeTab(tab);
+});
+
+/**
+ * In this test we load a service worker in a page prior to starting the
+ * TargetCommand. We start the target list on another page, and then we go back to
+ * the first page. We want to check that we are correctly notified about the
+ * worker that was spawned before TargetCommand.
+ *
+ * Steps:
+ * - navigate to .com page
+ * - navigate to .org page
+ * - create target list
+ * -> onAvailable is called for the .org worker
+ * - unregister .org worker
+ * -> onDestroyed is called for the .org worker
+ * - navigate back to .com page
+ * -> onAvailable is called for the .com worker
+ * - unregister .com worker
+ * -> onDestroyed is called for the .com worker
+ */
+add_task(async function test_NavigationToPageWithExistingWorker() {
+ await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ info("Wait until the service worker registration is registered");
+ await waitForRegistrationReady(tab, COM_PAGE_URL, COM_WORKER_URL);
+
+ info("Navigate to another page");
+ let onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ ORG_PAGE_URL
+ );
+
+ // Avoid TV failures, where target list still starts thinking that the
+ // current domain is .com .
+ info("Wait until we have fully navigated to the .org page");
+ // wait for the browser to be loaded otherwise the task spawned in waitForRegistrationReady
+ // might be destroyed (when it still belongs to the previous content process)
+ await onBrowserLoaded;
+ await waitForRegistrationReady(tab, ORG_PAGE_URL, ORG_WORKER_URL);
+
+ const { hooks, commands, targetCommand } = await watchServiceWorkerTargets(
+ tab
+ );
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 0,
+ targets: [ORG_WORKER_URL],
+ });
+
+ info("Unregister .org service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(ORG_WORKER_URL);
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 1,
+ targets: [],
+ });
+
+ info("Go back .com page, wait for onAvailable to be called");
+ onBrowserLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ COM_PAGE_URL
+ );
+ await onBrowserLoaded;
+
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Unregister .com service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(COM_WORKER_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 2,
+ targets: [],
+ });
+
+ // Stop listening to avoid worker related requests
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+ await removeTab(tab);
+});
+
+add_task(async function test_NavigationToPageWithExistingStoppedWorker() {
+ await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ info("Wait until the service worker registration is registered");
+ await waitForRegistrationReady(tab, COM_PAGE_URL, COM_WORKER_URL);
+
+ await stopServiceWorker(COM_WORKER_URL);
+
+ const { hooks, commands, targetCommand } = await watchServiceWorkerTargets(
+ tab
+ );
+
+ // Let some time to watch target to eventually regress and revive the worker
+ await wait(1000);
+
+ // As the Service Worker doesn't have any active worker... it doesn't report any target.
+ info(
+ "Verify that no SW is reported after it has been stopped and we start watching for service workers"
+ );
+ await checkHooks(hooks, {
+ available: 0,
+ destroyed: 0,
+ targets: [],
+ });
+
+ info("Reload the worker module via the postMessage call");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const registration = await content.wrappedJSObject.registrationPromise;
+ // Force loading the worker again, even it has been stopped
+ registration.active.postMessage("");
+ });
+
+ info("Verify that the SW is notified");
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 0,
+ targets: [COM_WORKER_URL],
+ });
+
+ await unregisterServiceWorker(COM_WORKER_URL);
+
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 1,
+ targets: [],
+ });
+
+ // Stop listening to avoid worker related requests
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+ await removeTab(tab);
+});
+
+async function setupServiceWorkerNavigationTest() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+}
+
+async function watchServiceWorkerTargets(tab) {
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // Enable Service Worker listening.
+ targetCommand.listenForServiceWorkers = true;
+ await targetCommand.startListening();
+
+ // Setup onAvailable & onDestroyed callbacks so that we can check how many
+ // times they are called and with which targetFront.
+ const hooks = {
+ availableCount: 0,
+ destroyedCount: 0,
+ targets: [],
+ };
+
+ const onAvailable = async ({ targetFront }) => {
+ info(` + Service worker target available for ${targetFront.url}\n`);
+ hooks.availableCount++;
+ hooks.targets.push(targetFront);
+ };
+
+ const onDestroyed = ({ targetFront }) => {
+ info(` - Service worker target destroy for ${targetFront.url}\n`);
+ hooks.destroyedCount++;
+ hooks.targets.splice(hooks.targets.indexOf(targetFront), 1);
+ };
+
+ await targetCommand.watchTargets({
+ types: [targetCommand.TYPES.SERVICE_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ return { hooks, commands, targetCommand };
+}
+
+/**
+ * Wait until the expected URL is loaded and win.registration has resolved.
+ */
+async function waitForRegistrationReady(tab, expectedPageUrl, workerUrl) {
+ await asyncWaitUntil(() =>
+ SpecialPowers.spawn(tab.linkedBrowser, [expectedPageUrl], function (_url) {
+ try {
+ const win = content.wrappedJSObject;
+ const isExpectedUrl = win.location.href === _url;
+ const hasRegistration = !!win.registrationPromise;
+ return isExpectedUrl && hasRegistration;
+ } catch (e) {
+ return false;
+ }
+ })
+ );
+ // On debug builds, the registration may not be yet ready in the parent process
+ // so we also need to ensure it is ready.
+ const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+ await waitFor(() => {
+ // Unfortunately we can't use swm.getRegistrationByPrincipal, as it requires a "scope", which doesn't seem to be the worker URL.
+ const registrations = swm.getAllRegistrations();
+ for (let i = 0; i < registrations.length; i++) {
+ const info = registrations.queryElementAt(
+ i,
+ Ci.nsIServiceWorkerRegistrationInfo
+ );
+ // Lookup for an exact URL match.
+ if (info.scriptSpec === workerUrl) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
+
+/**
+ * Assert helper for the `hooks` object, updated by the onAvailable and
+ * onDestroyed callbacks. Assert that the callbacks have been called the
+ * expected number of times, with the expected targets.
+ */
+async function checkHooks(hooks, { available, destroyed, targets }) {
+ await waitUntil(
+ () => hooks.availableCount == available && hooks.destroyedCount == destroyed
+ );
+ is(hooks.availableCount, available, "onAvailable was called as expected");
+ is(hooks.destroyedCount, destroyed, "onDestroyed was called as expected");
+
+ is(hooks.targets.length, targets.length, "Expected number of targets");
+ targets.forEach((url, i) => {
+ is(hooks.targets[i].url, url, `SW target ${i} has the expected url`);
+ });
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js b/devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js
new file mode 100644
index 0000000000..04646117a9
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API switchToTarget function
+
+add_task(async function testSwitchToTarget() {
+ info("Test TargetCommand.switchToTarget method");
+
+ // Create a first target to switch from, a new tab with an iframe
+ const firstTab = await addTab(
+ `data:text/html,<iframe src="data:text/html,foo"></iframe>`
+ );
+ const commands = await CommandsFactory.forTab(firstTab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ // Create a second target to switch to, a new tab with an iframe
+ const secondTab = await addTab(
+ `data:text/html,<iframe src="data:text/html,bar"></iframe>`
+ );
+ // We have to spawn a new distinct `commands` object for this new tab,
+ // but we will otherwise consider the first one as the main one.
+ // From this second one, we will only retrieve a new target.
+ const secondCommands = await CommandsFactory.forTab(secondTab, {
+ client: commands.client,
+ });
+ await secondCommands.targetCommand.startListening();
+ const secondTarget = secondCommands.targetCommand.targetFront;
+
+ const frameTargets = [];
+ const firstTarget = targetCommand.targetFront;
+ let currentTarget = targetCommand.targetFront;
+ const onFrameAvailable = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == currentTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ if (targetFront.isTopLevel) {
+ // When calling watchTargets, this will be false, but it will be true when calling switchToTarget
+ is(
+ isTargetSwitching,
+ currentTarget == secondTarget,
+ "target switching boolean is correct"
+ );
+ } else {
+ ok(!isTargetSwitching, "for now, only top level target can be switched");
+ }
+ frameTargets.push(targetFront);
+ };
+ const destroyedTargets = [];
+ const onFrameDestroyed = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "target-destroyed: We are only notified about frame targets"
+ );
+ ok(
+ targetFront == firstTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "target-destroyed: isTopLevel property is correct"
+ );
+ if (targetFront.isTopLevel) {
+ is(
+ isTargetSwitching,
+ true,
+ "target-destroyed: target switching boolean is correct"
+ );
+ } else {
+ ok(
+ !isTargetSwitching,
+ "target-destroyed: for now, only top level target can be switched"
+ );
+ }
+ destroyedTargets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable: onFrameAvailable,
+ onDestroyed: onFrameDestroyed,
+ });
+
+ // Save the original list of targets
+ const createdTargets = [...frameTargets];
+ // Clear the recorded target list of all existing targets
+ frameTargets.length = 0;
+
+ currentTarget = secondTarget;
+ await targetCommand.switchToTarget(secondTarget);
+
+ is(
+ targetCommand.targetFront,
+ currentTarget,
+ "After the switch, the top level target has been updated"
+ );
+ // Because JS Window Actor API isn't used yet, FrameDescriptor.getTarget returns null
+ // And there is no target being created for the iframe, yet.
+ // As soon as bug 1565200 is resolved, this should return two frames, including the iframe.
+ is(
+ frameTargets.length,
+ 1,
+ "We get the report of the top level iframe when switching to the new target"
+ );
+ is(frameTargets[0], currentTarget);
+ //is(frameTargets[1].url, "data:text/html,foo");
+
+ // Ensure that all the targets reported before the call to switchToTarget
+ // are reported as destroyed while calling switchToTarget.
+ is(
+ destroyedTargets.length,
+ createdTargets.length,
+ "All targets original reported are destroyed"
+ );
+ for (const newTarget of createdTargets) {
+ ok(
+ destroyedTargets.includes(newTarget),
+ "Each originally target is reported as destroyed"
+ );
+ }
+
+ targetCommand.destroy();
+
+ await commands.destroy();
+ await secondCommands.destroy();
+
+ BrowserTestUtils.removeTab(firstTab);
+ BrowserTestUtils.removeTab(secondTab);
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_tab_workers.js b/devtools/shared/commands/target/tests/browser_target_command_tab_workers.js
new file mode 100644
index 0000000000..92f5629d4c
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_tab_workers.js
@@ -0,0 +1,322 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around workers
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const IFRAME_FILE = "fission_iframe.html";
+const REMOTE_IFRAME_URL = URL_ROOT_ORG_SSL + IFRAME_FILE;
+const IFRAME_URL = URL_ROOT_SSL + IFRAME_FILE;
+const WORKER_FILE = "test_worker.js";
+const WORKER_URL = URL_ROOT_SSL + WORKER_FILE;
+const REMOTE_IFRAME_WORKER_URL = URL_ROOT_ORG_SSL + WORKER_FILE;
+
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // The WorkerDebuggerManager#getWorkerDebuggerEnumerator method we're using to retrieve
+ // workers loops through _all_ the workers in the process, which means it goes over workers
+ // from other tabs as well. Here we add a few tabs that are not going to be used in the
+ // test, just to check that their workers won't be retrieved by getAllTargets/watchTargets.
+ await addTab(`${FISSION_TEST_URL}?id=first-untargetted-tab&noServiceWorker`);
+ await addTab(`${FISSION_TEST_URL}?id=second-untargetted-tab&noServiceWorker`);
+
+ info("Test TargetCommand against workers via a tab target");
+ const tab = await addTab(`${FISSION_TEST_URL}?&noServiceWorker`);
+
+ // Create a TargetCommand for the tab
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // Workaround to allow listening for workers in the content toolbox
+ // without the fission preferences
+ targetCommand.listenForWorkers = true;
+
+ await commands.targetCommand.startListening();
+
+ const { TYPES } = targetCommand;
+
+ info("Check that getAllTargets only returns dedicated workers");
+ const workers = await targetCommand.getAllTargets([
+ TYPES.WORKER,
+ TYPES.SHARED_WORKER,
+ ]);
+
+ // XXX: This should be modified in Bug 1607778, where we plan to add support for shared workers.
+ is(workers.length, 2, "Retrieved two worker…");
+ const mainPageWorker = workers.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorker = workers.find(worker => {
+ return worker.url == `${REMOTE_IFRAME_WORKER_URL}#simple-worker-in-iframe`;
+ });
+ ok(mainPageWorker, "…the dedicated worker on the main page");
+ ok(iframeWorker, "…and the dedicated worker on the iframe");
+
+ info(
+ "Assert that watchTargets will call the create callback for existing dedicated workers"
+ );
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = async ({ targetFront }) => {
+ info(`onAvailable called for ${targetFront.url}`);
+ is(
+ targetFront.targetType,
+ TYPES.WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(!targetFront.isTopLevel, "The workers are never top level");
+ targets.push(targetFront);
+ info(`Handled ${targets.length} targets\n`);
+ };
+ const onDestroyed = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(!targetFront.isTopLevel, "The workers are never top level");
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetCommand.watchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ // XXX: This should be modified in Bug 1607778, where we plan to add support for shared workers.
+ info("Check that watched targets return the same fronts as getAllTargets");
+ is(targets.length, 2, "watcheTargets retrieved 2 worker…");
+ const mainPageWorkerTarget = targets.find(t => t === mainPageWorker);
+ const iframeWorkerTarget = targets.find(t => t === iframeWorker);
+
+ ok(
+ mainPageWorkerTarget,
+ "…the dedicated worker in main page, which is the same front we received from getAllTargets"
+ );
+ ok(
+ iframeWorkerTarget,
+ "…the dedicated worker in iframe, which is the same front we received from getAllTargets"
+ );
+
+ info("Spawn workers in main page and iframe");
+ await SpecialPowers.spawn(tab.linkedBrowser, [WORKER_FILE], workerUrl => {
+ // Put the worker on the global so we can access it later
+ content.spawnedWorker = new content.Worker(`${workerUrl}#spawned-worker`);
+ const iframe = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframe, [workerUrl], innerWorkerUrl => {
+ // Put the worker on the global so we can access it later
+ content.spawnedWorker = new content.Worker(
+ `${innerWorkerUrl}#spawned-worker-in-iframe`
+ );
+ });
+ });
+
+ await waitFor(
+ () => targets.length === 4,
+ "Wait for the target list to notify us about the spawned worker"
+ );
+ const mainPageSpawnedWorkerTarget = targets.find(
+ innerTarget => innerTarget.url == `${WORKER_URL}#spawned-worker`
+ );
+ ok(mainPageSpawnedWorkerTarget, "Retrieved spawned worker");
+ const iframeSpawnedWorkerTarget = targets.find(
+ innerTarget =>
+ innerTarget.url == `${REMOTE_IFRAME_WORKER_URL}#spawned-worker-in-iframe`
+ );
+ ok(iframeSpawnedWorkerTarget, "Retrieved spawned worker in iframe");
+
+ await wait(100);
+
+ info(
+ "Check that the target list calls onDestroy when a worker is terminated"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.spawnedWorker.terminate();
+ content.spawnedWorker = null;
+
+ SpecialPowers.spawn(content.document.querySelector("iframe"), [], () => {
+ content.spawnedWorker.terminate();
+ content.spawnedWorker = null;
+ });
+ });
+ await waitFor(
+ () =>
+ destroyedTargets.includes(mainPageSpawnedWorkerTarget) &&
+ destroyedTargets.includes(iframeSpawnedWorkerTarget),
+ "Wait for the target list to notify us about the terminated workers"
+ );
+
+ ok(
+ true,
+ "The target list handled the terminated workers (from the main page and the iframe)"
+ );
+
+ info(
+ "Check that reloading the page will notify about the terminated worker and the new existing one"
+ );
+ const targetsCountBeforeReload = targets.length;
+ await reloadBrowser();
+
+ await waitFor(() => {
+ return (
+ destroyedTargets.includes(mainPageWorkerTarget) &&
+ destroyedTargets.includes(iframeWorkerTarget)
+ );
+ }, `Wait for the target list to notify us about the terminated workers when reloading`);
+ ok(
+ true,
+ "The target list notified us about all the expected workers being destroyed when reloading"
+ );
+
+ await waitFor(
+ () => targets.length === targetsCountBeforeReload + 2,
+ "Wait for the target list to notify us about the new workers after reloading"
+ );
+
+ const mainPageWorkerTargetAfterReload = targets.find(
+ t => t !== mainPageWorkerTarget && t.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorkerTargetAfterReload = targets.find(
+ t =>
+ t !== iframeWorkerTarget &&
+ t.url == `${REMOTE_IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+
+ ok(
+ mainPageWorkerTargetAfterReload,
+ "The target list handled the worker created once the page navigated"
+ );
+ ok(
+ iframeWorkerTargetAfterReload,
+ "The target list handled the worker created in the iframe once the page navigated"
+ );
+
+ const targetCount = targets.length;
+
+ info(
+ "Check that when removing an iframe we're notified about its workers being terminated"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.querySelector("iframe").remove();
+ });
+ await waitFor(() => {
+ return destroyedTargets.includes(iframeWorkerTargetAfterReload);
+ }, `Wait for the target list to notify us about the terminated workers when removing an iframe`);
+
+ info("Check that target list handles adding iframes with workers");
+ const iframeUrl = `${IFRAME_URL}?noServiceWorker=true&hashSuffix=in-created-iframe`;
+ const remoteIframeUrl = `${REMOTE_IFRAME_URL}?noServiceWorker=true&hashSuffix=in-created-remote-iframe`;
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [iframeUrl, remoteIframeUrl],
+ (url, remoteUrl) => {
+ const firstIframe = content.document.createElement("iframe");
+ content.document.body.append(firstIframe);
+ firstIframe.src = url + "-1";
+
+ const secondIframe = content.document.createElement("iframe");
+ content.document.body.append(secondIframe);
+ secondIframe.src = url + "-2";
+
+ const firstRemoteIframe = content.document.createElement("iframe");
+ content.document.body.append(firstRemoteIframe);
+ firstRemoteIframe.src = remoteUrl + "-1";
+
+ const secondRemoteIframe = content.document.createElement("iframe");
+ content.document.body.append(secondRemoteIframe);
+ secondRemoteIframe.src = remoteUrl + "-2";
+ }
+ );
+
+ // It's important to check the length of `targets` here to ensure we don't get unwanted
+ // worker targets.
+ await waitFor(
+ () => targets.length === targetCount + 4,
+ "Wait for the target list to notify us about the workers in the new iframes"
+ );
+ const firstSpawnedIframeWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker-in-created-iframe-1`
+ );
+ const secondSpawnedIframeWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker-in-created-iframe-2`
+ );
+ const firstSpawnedRemoteIframeWorkerTarget = targets.find(
+ worker =>
+ worker.url ==
+ `${REMOTE_IFRAME_WORKER_URL}#simple-worker-in-created-remote-iframe-1`
+ );
+ const secondSpawnedRemoteIframeWorkerTarget = targets.find(
+ worker =>
+ worker.url ==
+ `${REMOTE_IFRAME_WORKER_URL}#simple-worker-in-created-remote-iframe-2`
+ );
+
+ ok(
+ firstSpawnedIframeWorkerTarget,
+ "The target list handled the worker in the first new same-origin iframe"
+ );
+ ok(
+ secondSpawnedIframeWorkerTarget,
+ "The target list handled the worker in the second new same-origin iframe"
+ );
+ ok(
+ firstSpawnedRemoteIframeWorkerTarget,
+ "The target list handled the worker in the first new remote iframe"
+ );
+ ok(
+ secondSpawnedRemoteIframeWorkerTarget,
+ "The target list handled the worker in the second new remote iframe"
+ );
+
+ info("Check that navigating away does destroy all targets");
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ "data:text/html,<meta charset=utf8>Away"
+ );
+
+ await waitFor(
+ () => destroyedTargets.length === targets.length,
+ "Wait for all the targets to be reported as destroyed"
+ );
+
+ ok(
+ destroyedTargets.includes(mainPageWorkerTargetAfterReload),
+ "main page worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(firstSpawnedIframeWorkerTarget),
+ "first spawned same-origin iframe worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(secondSpawnedIframeWorkerTarget),
+ "second spawned same-origin iframe worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(firstSpawnedRemoteIframeWorkerTarget),
+ "first spawned remote iframe worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(secondSpawnedRemoteIframeWorkerTarget),
+ "second spawned remote iframe worker target was destroyed"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+ targetCommand.destroy();
+
+ info("Unregister service workers so they don't appear in other tests.");
+ await unregisterAllServiceWorkers(commands.client);
+
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js b/devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js
new file mode 100644
index 0000000000..e628f827e2
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test WORKER targets when doing history navigations (BF Cache)
+//
+// Use a distinct file as this test currently hits a DEBUG assertion
+// https://searchfox.org/mozilla-central/rev/352b525ab841278cd9b3098343f655ef85933544/dom/workers/WorkerPrivate.cpp#5218
+// and so is running only on OPT builds.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const WORKER_FILE = "test_worker.js";
+const WORKER_URL = URL_ROOT_SSL + WORKER_FILE;
+const IFRAME_WORKER_URL = URL_ROOT_ORG_SSL + WORKER_FILE;
+
+add_task(async function () {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // The WorkerDebuggerManager#getWorkerDebuggerEnumerator method we're using to retrieve
+ // workers loops through _all_ the workers in the process, which means it goes over workers
+ // from other tabs as well. Here we add a few tabs that are not going to be used in the
+ // test, just to check that their workers won't be retrieved by getAllTargets/watchTargets.
+ await addTab(`${FISSION_TEST_URL}?id=first-untargetted-tab&noServiceWorker`);
+ await addTab(`${FISSION_TEST_URL}?id=second-untargetted-tab&noServiceWorker`);
+
+ info("Test bfcache navigations");
+ const tab = await addTab(`${FISSION_TEST_URL}?&noServiceWorker`);
+
+ // Create a TargetCommand for the tab
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // Workaround to allow listening for workers in the content toolbox
+ // without the fission preferences
+ targetCommand.listenForWorkers = true;
+
+ await targetCommand.startListening();
+
+ const { TYPES } = targetCommand;
+
+ info(
+ "Assert that watchTargets will call the onAvailable callback for existing dedicated workers"
+ );
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = async ({ targetFront }) => {
+ info(`onAvailable called for ${targetFront.url}`);
+ is(
+ targetFront.targetType,
+ TYPES.WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(!targetFront.isTopLevel, "The workers are never top level");
+ targets.push(targetFront);
+ info(`Handled ${targets.length} new targets`);
+ };
+ const onDestroyed = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(!targetFront.isTopLevel, "The workers are never top level");
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetCommand.watchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ is(targets.length, 2, "watchTargets retrieved 2 workers…");
+ const mainPageWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorkerTarget = targets.find(
+ worker => worker.url == `${IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+
+ ok(
+ mainPageWorkerTarget,
+ "…the dedicated worker in main page, which is the same front we received from getAllTargets"
+ );
+ ok(
+ iframeWorkerTarget,
+ "…the dedicated worker in iframe, which is the same front we received from getAllTargets"
+ );
+
+ info("Check that navigating away does destroy all targets");
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ "data:text/html,<meta charset=utf8>Away"
+ );
+ await onBrowserLoaded;
+
+ await waitFor(
+ () => destroyedTargets.length === 2,
+ "Wait for all the targets to be reported as destroyed"
+ );
+
+ info("Navigate back to the first page");
+ gBrowser.goBack();
+
+ await waitFor(
+ () => targets.length === 4,
+ "Wait for the target list to notify us about the first page workers, restored from the BF Cache"
+ );
+
+ const mainPageWorkerTargetAfterGoingBack = targets.find(
+ t => t !== mainPageWorkerTarget && t.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorkerTargetAfterGoingBack = targets.find(
+ t =>
+ t !== iframeWorkerTarget &&
+ t.url == `${IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+
+ ok(
+ mainPageWorkerTargetAfterGoingBack,
+ "The target list handled the worker created from the BF Cache"
+ );
+ ok(
+ iframeWorkerTargetAfterGoingBack,
+ "The target list handled the worker created in the iframe from the BF Cache"
+ );
+
+ targetCommand.destroy();
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js b/devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js
new file mode 100644
index 0000000000..4ee5dd8b2f
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js
@@ -0,0 +1,284 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API with all possible descriptors
+
+const TEST_URL = "https://example.org/document-builder.sjs?html=org";
+const SECOND_TEST_URL = "https://example.com/document-builder.sjs?html=org";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + "test_worker.js";
+
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+add_task(async function () {
+ // Enabled fission prefs
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ await testLocalTab();
+ await testRemoteTab();
+ await testParentProcess();
+ await testWorker();
+ await testWebExtension();
+});
+
+async function testParentProcess() {
+ info("Test TargetCommand against parent process descriptor");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const { descriptorFront } = commands;
+
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.PROCESS,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isParentProcessDescriptor,
+ true,
+ "Descriptor front isParentProcessDescriptor is correct"
+ );
+ is(
+ descriptorFront.isProcessDescriptor,
+ true,
+ "Descriptor front isProcessDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ Assert.greater(
+ targets.length,
+ 1,
+ "We get many targets when debugging the parent process"
+ );
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the parent process target is of frame type, because it inherits from WindowGlobalTargetActor"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ await commands.destroy();
+}
+
+async function testLocalTab() {
+ info("Test TargetCommand against local tab descriptor (via getTab({ tab }))");
+
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.TAB,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isTabDescriptor,
+ true,
+ "Descriptor front isTabDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the tab target is of frame type"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testRemoteTab() {
+ info(
+ "Test TargetCommand against remote tab descriptor (via getTab({ browserId }))"
+ );
+
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forRemoteTab(
+ tab.linkedBrowser.browserId
+ );
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.TAB,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isTabDescriptor,
+ true,
+ "Descriptor front isTabDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(
+ targetFront,
+ targetCommand.targetFront,
+ "TargetCommand top target is the same as the first target"
+ );
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the tab target is of frame type"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, SECOND_TEST_URL);
+ await onLoaded;
+
+ info("Wait for the new target");
+ await waitFor(() => targetCommand.targetFront != targetFront);
+ isnot(
+ targetCommand.targetFront,
+ targetFront,
+ "The top level target changes on navigation"
+ );
+ ok(
+ !targetCommand.targetFront.isDestroyed(),
+ "The new target isn't destroyed"
+ );
+ ok(targetFront.isDestroyed(), "While the previous target is destroyed");
+
+ targetCommand.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testWebExtension() {
+ info("Test TargetCommand against webextension descriptor");
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Sample extension",
+ },
+ });
+
+ await extension.startup();
+
+ const commands = await CommandsFactory.forAddon(extension.id);
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.EXTENSION,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isWebExtensionDescriptor,
+ true,
+ "Descriptor front isWebExtensionDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the web extension target is of frame type, because it inherits from WindowGlobalTargetActor"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+
+ await extension.unload();
+
+ await commands.destroy();
+}
+
+// CommandsFactory expect the worker id, which is computed from the nsIWorkerDebugger.id attribute
+function getNextWorkerDebuggerId() {
+ return new Promise(resolve => {
+ const wdm = Cc[
+ "@mozilla.org/dom/workers/workerdebuggermanager;1"
+ ].createInstance(Ci.nsIWorkerDebuggerManager);
+ const listener = {
+ onRegister(dbg) {
+ wdm.removeListener(listener);
+ resolve(dbg.id);
+ },
+ };
+ wdm.addListener(listener);
+ });
+}
+async function testWorker() {
+ info("Test TargetCommand against worker descriptor");
+
+ const workerUrl = CHROME_WORKER_URL + "#descriptor";
+ const onNextWorker = getNextWorkerDebuggerId();
+ const worker = new Worker(workerUrl);
+ const workerId = await onNextWorker;
+ ok(workerId, "Found the worker Debugger ID");
+
+ const commands = await CommandsFactory.forWorker(workerId);
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.WORKER,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isWorkerDescriptor,
+ true,
+ "Descriptor front isWorkerDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.WORKER,
+ "the worker target is of worker type"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+
+ // Calling CommandsFactory.forWorker, will call RootFront.getWorker
+ // which will spawn lots of worker legacy code, firing lots of requests,
+ // which may still be pending
+ await commands.waitForRequestsToSettle();
+
+ await commands.destroy();
+ worker.terminate();
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_watchTargets.js b/devtools/shared/commands/target/tests/browser_target_command_watchTargets.js
new file mode 100644
index 0000000000..516780be01
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_watchTargets.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand's `watchTargets` function
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Enabled fission's pref as the TargetCommand is almost disabled without it
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ await testWatchTargets();
+ await testThrowingInOnAvailable();
+});
+
+async function testWatchTargets() {
+ info("Test TargetCommand watchTargets function");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
+ const originalProcessesCount = Services.ppmm.childCount - 1;
+
+ info(
+ "Check that onAvailable is called for processes already created *before* the call to watchTargets"
+ );
+ const targets = new Set();
+ const topLevelTarget = targetCommand.targetFront;
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ if (!targets.has(targetFront)) {
+ ok(
+ false,
+ "A target is declared destroyed via onDestroyed without being notified via onAvailable"
+ );
+ }
+ is(
+ targetFront.targetType,
+ TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ !targetFront.isTopLevel,
+ "We are not notified about the top level target destruction"
+ );
+ targets.delete(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+ is(
+ targets.size,
+ originalProcessesCount,
+ "retrieved the expected number of processes via watchTargets"
+ );
+ // Start from 1 in order to ignore the parent process target, which is considered as a frame rather than a process
+ for (let i = 1; i < Services.ppmm.childCount; i++) {
+ const process = Services.ppmm.getChildAt(i);
+ const hasTargetWithSamePID = [...targets].find(
+ processTarget => processTarget.targetForm.processID == process.osPid
+ );
+ ok(
+ hasTargetWithSamePID,
+ `Process with PID ${process.osPid} has been reported via onAvailable`
+ );
+ }
+
+ info(
+ "Check that onAvailable is called for processes created *after* the call to watchTargets"
+ );
+ const previousTargets = new Set(targets);
+ const onProcessCreated = new Promise(resolve => {
+ const onAvailable2 = ({ targetFront }) => {
+ if (previousTargets.has(targetFront)) {
+ return;
+ }
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable2,
+ });
+ resolve(targetFront);
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable2,
+ });
+ });
+ const tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+ const createdTarget = await onProcessCreated;
+
+ // For some reason, creating a new tab purges processes created from previous tests
+ // so it is not reasonable to assert the side of `targets` as it may be lower than expected.
+ ok(targets.has(createdTarget), "The new tab process is in the list");
+
+ const processCountAfterTabOpen = targets.size;
+
+ // Assert that onDestroyed is called for destroyed processes
+ const onProcessDestroyed = new Promise(resolve => {
+ const onAvailable3 = () => {};
+ const onDestroyed3 = ({ targetFront }) => {
+ resolve(targetFront);
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: onDestroyed3,
+ });
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: onDestroyed3,
+ });
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ const destroyedTarget = await onProcessDestroyed;
+ is(
+ targets.size,
+ processCountAfterTabOpen - 1,
+ "The closed tab's process has been reported as destroyed"
+ );
+ ok(
+ !targets.has(destroyedTarget),
+ "The destroyed target is no longer in the list"
+ );
+ is(
+ destroyedTarget,
+ createdTarget,
+ "The destroyed target is the one that has been reported as created"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+
+ targetCommand.destroy();
+
+ await commands.destroy();
+}
+
+async function testThrowingInOnAvailable() {
+ info(
+ "Test TargetCommand watchTargets function when an exception is thrown in onAvailable callback"
+ );
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
+ const originalProcessesCount = Services.ppmm.childCount - 1;
+
+ info(
+ "Check that onAvailable is called for processes already created *before* the call to watchTargets"
+ );
+ const targets = new Set();
+ let thrown = false;
+ const onAvailable = ({ targetFront }) => {
+ if (!thrown) {
+ thrown = true;
+ throw new Error("Force an exception when processing the first target");
+ }
+ targets.add(targetFront);
+ };
+ await targetCommand.watchTargets({ types: [TYPES.PROCESS], onAvailable });
+ is(
+ targets.size,
+ originalProcessesCount - 1,
+ "retrieved the expected number of processes via onAvailable. All but the first one where we have thrown."
+ );
+
+ targetCommand.destroy();
+
+ await commands.destroy();
+}
diff --git a/devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js b/devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js
new file mode 100644
index 0000000000..6dd99d243b
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that watcher front/actor APIs do not lead to create duplicate actors.
+
+const TEST_URL = "data:text/html;charset=utf-8,Actor caching test";
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab(TEST_URL);
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const { watcherFront } = targetCommand;
+ ok(watcherFront, "A watcherFront is available on targetCommand");
+
+ info("Check that getNetworkParentActor does not create duplicate actors");
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getNetworkParentActor(),
+ "networkParent"
+ );
+
+ info("Check that getBreakpointListActor does not create duplicate actors");
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getBreakpointListActor(),
+ "breakpoint-list"
+ );
+
+ info(
+ "Check that getTargetConfigurationActor does not create duplicate actors"
+ );
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getTargetConfigurationActor(),
+ "target-configuration"
+ );
+
+ info(
+ "Check that getThreadConfigurationActor does not create duplicate actors"
+ );
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getThreadConfigurationActor(),
+ "thread-configuration"
+ );
+
+ targetCommand.destroy();
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+});
+
+/**
+ * Check that calling an actor getter method on the watcher front leads to the
+ * creation of at most 1 actor.
+ */
+async function testActorGetter(watcherFront, actorGetterFn, typeName) {
+ checkPoolChildrenSize(watcherFront, typeName, 0);
+
+ const actor = await actorGetterFn();
+ checkPoolChildrenSize(watcherFront, typeName, 1);
+
+ const otherActor = await actorGetterFn();
+ is(actor, otherActor, "Returned the same actor for " + typeName);
+
+ checkPoolChildrenSize(watcherFront, typeName, 1);
+}
+
+/**
+ * Assert that a given parent pool has the expected number of children for
+ * a given typeName.
+ */
+function checkPoolChildrenSize(parentPool, typeName, expected) {
+ const children = [...parentPool.poolChildren()];
+ const childrenByType = children.filter(pool => pool.typeName === typeName);
+ is(
+ childrenByType.length,
+ expected,
+ `${parentPool.actorID} should have ${expected} children of type ${typeName}`
+ );
+}
diff --git a/devtools/shared/commands/target/tests/fission_document.html b/devtools/shared/commands/target/tests/fission_document.html
new file mode 100644
index 0000000000..62afe347e3
--- /dev/null
+++ b/devtools/shared/commands/target/tests/fission_document.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+
+ const params = new URLSearchParams(document.location.search);
+
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker("https://example.com/browser/devtools/shared/commands/target/tests/test_worker.js#simple-worker");
+
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("https://example.com/browser/devtools/shared/commands/target/tests/test_worker.js#shared-worker");
+
+ if (!params.has("noServiceWorker")) {
+ // Expose a reference to the registration so that tests can unregister it.
+ window.registrationPromise = navigator.serviceWorker.register("https://example.com/browser/devtools/shared/commands/target/tests/test_service_worker.js#service-worker");
+ }
+
+ /* exported logMessageInWorker */
+ function logMessageInWorker(message) {
+ worker.postMessage({
+ type: "log-in-worker",
+ message,
+ });
+ }
+ </script>
+</head>
+<body>
+<p>Test fission iframe</p>
+
+<script>
+ "use strict";
+ const iframe = document.createElement("iframe");
+ let iframeUrl = `https://example.org/browser/devtools/shared/commands/target/tests/fission_iframe.html`;
+ if (document.location.search) {
+ iframeUrl += `?${new URLSearchParams(document.location.search)}`;
+ }
+ iframe.src = iframeUrl;
+ document.body.append(iframe);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/commands/target/tests/fission_iframe.html b/devtools/shared/commands/target/tests/fission_iframe.html
new file mode 100644
index 0000000000..deae49f833
--- /dev/null
+++ b/devtools/shared/commands/target/tests/fission_iframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+ const params = new URLSearchParams(document.location.search);
+ const hashSuffix = params.get("hashSuffix") || "in-iframe";
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker("test_worker.js#simple-worker-" + hashSuffix);
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("test_worker.js#shared-worker-" + hashSuffix);
+
+ /* exported logMessageInWorker */
+ function logMessageInWorker(message) {
+ worker.postMessage({
+ type: "log-in-worker",
+ message,
+ });
+ }
+ </script>
+</head>
+<body>
+<p>remote iframe</p>
+</body>
+</html>
diff --git a/devtools/shared/commands/target/tests/head.js b/devtools/shared/commands/target/tests/head.js
new file mode 100644
index 0000000000..ecb3fc1828
--- /dev/null
+++ b/devtools/shared/commands/target/tests/head.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+async function createLocalClient() {
+ // Instantiate a minimal server
+ DevToolsServer.init();
+ DevToolsServer.allowChromeProcess = true;
+ if (!DevToolsServer.createRootActor) {
+ DevToolsServer.registerAllActors();
+ }
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ return client;
+}
diff --git a/devtools/shared/commands/target/tests/incremental-js-value-script.sjs b/devtools/shared/commands/target/tests/incremental-js-value-script.sjs
new file mode 100644
index 0000000000..a612a3cb59
--- /dev/null
+++ b/devtools/shared/commands/target/tests/incremental-js-value-script.sjs
@@ -0,0 +1,23 @@
+"use strict";
+
+function handleRequest(request, response) {
+ const Etag = '"4d881ab-b03-435f0a0f9ef00"';
+ const IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ const counter = getState("cache-counter") || 1;
+ const page = "<script>var jsValue = '" + counter + "';</script>" + counter;
+
+ setState("cache-counter", "" + (parseInt(counter, 10) + 1));
+
+ response.setHeader("Etag", Etag, false);
+
+ if (IfNoneMatch === Etag) {
+ response.setStatusLine(request.httpVersion, "304", "Not Modified");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+ }
+}
diff --git a/devtools/shared/commands/target/tests/simple_document.html b/devtools/shared/commands/target/tests/simple_document.html
new file mode 100644
index 0000000000..d6a449e489
--- /dev/null
+++ b/devtools/shared/commands/target/tests/simple_document.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test empty document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test empty document</p>
+</body>
+</html>
diff --git a/devtools/shared/commands/target/tests/test_service_worker.js b/devtools/shared/commands/target/tests/test_service_worker.js
new file mode 100644
index 0000000000..aabc3fda0f
--- /dev/null
+++ b/devtools/shared/commands/target/tests/test_service_worker.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We don't need any computation in the worker,
+// but at least register a fetch listener so that
+// we force instantiating the SW when loading the page.
+self.onfetch = function (event) {
+ // do nothing.
+};
diff --git a/devtools/shared/commands/target/tests/test_sw_page.html b/devtools/shared/commands/target/tests/test_sw_page.html
new file mode 100644
index 0000000000..38aad04259
--- /dev/null
+++ b/devtools/shared/commands/target/tests/test_sw_page.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test sw page</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test sw page</p>
+
+<script>
+"use strict";
+
+// Expose a reference to the registration so that tests can unregister it.
+window.registrationPromise = navigator.serviceWorker.register("test_sw_page_worker.js");
+</script>
+</body>
+</html>
diff --git a/devtools/shared/commands/target/tests/test_sw_page_worker.js b/devtools/shared/commands/target/tests/test_sw_page_worker.js
new file mode 100644
index 0000000000..29cda68560
--- /dev/null
+++ b/devtools/shared/commands/target/tests/test_sw_page_worker.js
@@ -0,0 +1,5 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// We don't need any computation in the worker,
+// just it to be alive
diff --git a/devtools/shared/commands/target/tests/test_worker.js b/devtools/shared/commands/target/tests/test_worker.js
new file mode 100644
index 0000000000..ce3dd39cea
--- /dev/null
+++ b/devtools/shared/commands/target/tests/test_worker.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+globalThis.onmessage = function (e) {
+ const { type, message } = e.data;
+
+ if (type === "log-in-worker") {
+ // Printing `e` so we can check that we have an object and not a stringified version
+ console.log("[WORKER]", message, e);
+ }
+};
diff --git a/devtools/shared/commands/thread-configuration/moz.build b/devtools/shared/commands/thread-configuration/moz.build
new file mode 100644
index 0000000000..28e8e0ffc4
--- /dev/null
+++ b/devtools/shared/commands/thread-configuration/moz.build
@@ -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/.
+
+DevToolsModules(
+ "thread-configuration-command.js",
+)
diff --git a/devtools/shared/commands/thread-configuration/tests/browser.toml b/devtools/shared/commands/thread-configuration/tests/browser.toml
new file mode 100644
index 0000000000..bbd5485874
--- /dev/null
+++ b/devtools/shared/commands/thread-configuration/tests/browser.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "head.js",
+]
diff --git a/devtools/shared/commands/thread-configuration/tests/head.js b/devtools/shared/commands/thread-configuration/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/thread-configuration/tests/head.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/thread-configuration/thread-configuration-command.js b/devtools/shared/commands/thread-configuration/thread-configuration-command.js
new file mode 100644
index 0000000000..0db1c2a285
--- /dev/null
+++ b/devtools/shared/commands/thread-configuration/thread-configuration-command.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/. */
+
+"use strict";
+
+/**
+ * The ThreadConfigurationCommand should be used to maintain thread settings
+ * sent from the client for the thread actor.
+ *
+ * See the ThreadConfigurationActor for a list of supported configuration options.
+ */
+class ThreadConfigurationCommand {
+ constructor({ commands, watcherFront }) {
+ this._commands = commands;
+ this._watcherFront = watcherFront;
+ }
+
+ /**
+ * Return a promise that resolves to the related thread configuration actor's front.
+ *
+ * @return {Promise<ThreadConfigurationFront>}
+ */
+ async getThreadConfigurationFront() {
+ const front = await this._watcherFront.getThreadConfigurationActor();
+ return front;
+ }
+
+ async updateConfiguration(configuration) {
+ if (this._commands.targetCommand.hasTargetWatcherSupport()) {
+ // Remove thread options that are not currently supported by
+ // the thread configuration actor.
+ const filteredConfiguration = Object.fromEntries(
+ Object.entries(configuration).filter(
+ ([key, value]) => !["breakpoints", "eventBreakpoints"].includes(key)
+ )
+ );
+
+ const threadConfigurationFront = await this.getThreadConfigurationFront();
+ const updatedConfiguration =
+ await threadConfigurationFront.updateConfiguration(
+ filteredConfiguration
+ );
+ this._configuration = updatedConfiguration;
+ }
+
+ let threadFronts = await this._commands.targetCommand.getAllFronts(
+ this._commands.targetCommand.ALL_TYPES,
+ "thread"
+ );
+
+ // Lets always call reconfigure for all the target types that do not
+ // have target watcher support yet. e.g In the browser, even
+ // though `hasTargetWatcherSupport()` is true, only
+ // FRAME and CONTENT PROCESS targets use watcher actors,
+ // WORKER targets are supported via the legacy listerners.
+ threadFronts = threadFronts.filter(
+ threadFront =>
+ !this._commands.targetCommand.hasTargetWatcherSupport(
+ threadFront.targetFront.targetType
+ )
+ );
+
+ // Ignore threads that fail to be configured.
+ // Some workers may be destroying and `reconfigure` would be rejected.
+ await Promise.allSettled(
+ threadFronts.map(threadFront => threadFront.reconfigure(configuration))
+ );
+ }
+}
+
+module.exports = ThreadConfigurationCommand;
diff --git a/devtools/shared/commands/tracer/moz.build b/devtools/shared/commands/tracer/moz.build
new file mode 100644
index 0000000000..63b3033655
--- /dev/null
+++ b/devtools/shared/commands/tracer/moz.build
@@ -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/.
+
+DevToolsModules(
+ "tracer-command.js",
+)
diff --git a/devtools/shared/commands/tracer/tracer-command.js b/devtools/shared/commands/tracer/tracer-command.js
new file mode 100644
index 0000000000..f512c15d9e
--- /dev/null
+++ b/devtools/shared/commands/tracer/tracer-command.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";
+
+class TracerCommand {
+ constructor({ commands }) {
+ this.#targetCommand = commands.targetCommand;
+ this.#targetConfigurationCommand = commands.targetConfigurationCommand;
+ this.#resourceCommand = commands.resourceCommand;
+ }
+
+ #resourceCommand;
+ #targetCommand;
+ #targetConfigurationCommand;
+ #isTracing = false;
+
+ async initialize() {
+ return this.#resourceCommand.watchResources(
+ [this.#resourceCommand.TYPES.JSTRACER_STATE],
+ { onAvailable: this.onResourcesAvailable }
+ );
+ }
+ destroy() {
+ this.#resourceCommand.unwatchResources(
+ [this.#resourceCommand.TYPES.JSTRACER_STATE],
+ { onAvailable: this.onResourcesAvailable }
+ );
+ }
+
+ onResourcesAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType != this.#resourceCommand.TYPES.JSTRACER_STATE) {
+ continue;
+ }
+ this.#isTracing = resource.enabled;
+ }
+ };
+
+ /**
+ * Get the dictionary passed to the server codebase as a SessionData.
+ * This contains all settings to fine tune the tracer actual behavior.
+ *
+ * @return {JSON}
+ * Configuration object.
+ */
+ #getTracingOptions() {
+ return {
+ logMethod: Services.prefs.getStringPref(
+ "devtools.debugger.javascript-tracing-log-method",
+ ""
+ ),
+ traceValues: Services.prefs.getBoolPref(
+ "devtools.debugger.javascript-tracing-values",
+ false
+ ),
+ traceOnNextInteraction: Services.prefs.getBoolPref(
+ "devtools.debugger.javascript-tracing-on-next-interaction",
+ false
+ ),
+ traceOnNextLoad: Services.prefs.getBoolPref(
+ "devtools.debugger.javascript-tracing-on-next-load",
+ false
+ ),
+ traceFunctionReturn: Services.prefs.getBoolPref(
+ "devtools.debugger.javascript-tracing-function-return",
+ false
+ ),
+ };
+ }
+
+ /**
+ * Toggle JavaScript tracing for all targets.
+ */
+ async toggle() {
+ this.#isTracing = !this.#isTracing;
+
+ await this.#targetConfigurationCommand.updateConfiguration({
+ tracerOptions: this.#isTracing ? this.#getTracingOptions() : undefined,
+ });
+ }
+}
+
+module.exports = TracerCommand;
diff --git a/devtools/shared/compatibility/README.md b/devtools/shared/compatibility/README.md
new file mode 100644
index 0000000000..b36dbfe10f
--- /dev/null
+++ b/devtools/shared/compatibility/README.md
@@ -0,0 +1,27 @@
+# Compatibility Dataset
+
+## How to update the MDN compatibility data
+
+The Compatibility panel detects issues by comparing against official [MDN compatibility data](https://github.com/mdn/browser-compat-data). It uses a local snapshot of the dataset. This dataset needs to be manually synchronized periodically to `devtools/shared/compatibility/dataset` (ideally with every Firefox release).
+
+The subsets from the dataset required by the Compatibility panel are:
+
+- browsers: [https://github.com/mdn/browser-compat-data/tree/master/browsers](https://github.com/mdn/browser-compat-data/tree/master/browsers)
+- css.properties: [https://github.com/mdn/browser-compat-data/tree/master/css](https://github.com/mdn/browser-compat-data/tree/master/css).
+
+In order to download up-to-date data, you need to run the following commands:
+
+- `cd devtools/shared/compatibility`
+- `yarn install --no-lockfile` and select the latest package version for the `@mdn/browser-compat-data` package
+- `yarn update`
+
+This should save the `css-properties.json` JSON file directly in `devtools/shared/compatibility/dataset/`.
+
+The browsers data are stored in a RemoteSettings collection, and updates are handled by a script in https://github.com/firefox-devtools/remote-settings-mdn-browser-compat-data .
+The script is run every day in automation, and if the data are updated, the team should receive a data review email.
+
+To review the data update, you need to be connected to the Mozilla Corporate VPN (See https://mana.mozilla.org/wiki/display/SD/VPN), log into https://remote-settings.allizom.org/v1/admin/#/buckets/main/collections/devtools-compatibility-browsers/records (Using `OpenID Connect (LDAP)`)
+Then run Firefox, and use the [RemoteSettings DevTools WebExtension](https://github.com/mozilla-extensions/remote-settings-devtools) to use the `Prod (preview)` environment and restart the browser.
+Then open the compatibility panel and make sure that the updated browsers do appear in the `Settings` panel.
+
+Check that all tests still pass. It is possible that changes in the structure or contents of the latest dataset will cause tests to fail. If that is the case, fix the tests. **Do not manually change the contents or structure of the local dataset** because any changes will be overwritten by the next update from the official dataset.
diff --git a/devtools/shared/compatibility/bin/update.js b/devtools/shared/compatibility/bin/update.js
new file mode 100644
index 0000000000..4caa1efe01
--- /dev/null
+++ b/devtools/shared/compatibility/bin/update.js
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// The Compatibility panel detects issues by comparing against official MDN compatibility data
+// at https://github.com/mdn/browser-compat-data). It uses a local snapshot of the dataset.
+// This dataset needs to be manually synchronized periodically
+
+// The subsets from the dataset required by the Compatibility panel are:
+// * css.properties: https://github.com/mdn/browser-compat-data/tree/master/css
+
+// The MDN compatibility data is available as a node package ("@mdn/browser-compat-data"),
+// which is used here to update `../dataset/css-properties.json`.
+
+/* eslint-disable mozilla/reject-relative-requires */
+
+"use strict";
+
+const compatData = require("@mdn/browser-compat-data");
+const { properties } = compatData.css;
+
+const { TARGET_BROWSER_ID } = require("../constants.js");
+const { getCompatTable } = require("../helpers.js");
+
+// Flatten all CSS properties aliases here so we don't have to do it at runtime,
+// which is costly.
+flattenAliases(properties);
+parseBrowserVersion(properties);
+removeUnusedData(properties);
+exportData(properties, "css-properties.json");
+
+/**
+ * Builds a list of aliases between CSS properties, like flex and -webkit-flex,
+ * and mutates individual entries in the web compat data store for CSS properties to
+ * add their corresponding aliases.
+ */
+function flattenAliases(compatNode) {
+ for (const term in compatNode) {
+ if (term.startsWith("_")) {
+ // Ignore exploring if the term is _aliasOf or __compat.
+ continue;
+ }
+
+ const compatTable = getCompatTable(compatNode, [term]);
+ if (compatTable) {
+ const aliases = findAliasesFrom(compatTable);
+
+ for (const { alternative_name: name, prefix } of aliases) {
+ const alias = name || prefix + term;
+ compatNode[alias] = { _aliasOf: term };
+ }
+
+ if (aliases.length) {
+ // Make the term accessible as the alias.
+ compatNode[term]._aliasOf = term;
+ }
+ }
+
+ // Flatten deeper node as well.
+ flattenAliases(compatNode[term]);
+ }
+}
+
+function findAliasesFrom(compatTable) {
+ const aliases = [];
+
+ for (const browser in compatTable.support) {
+ let supportStates = compatTable.support[browser] || [];
+ supportStates = Array.isArray(supportStates)
+ ? supportStates
+ : [supportStates];
+
+ for (const { alternative_name: name, prefix } of supportStates) {
+ if (!prefix && !name) {
+ continue;
+ }
+
+ aliases.push({ alternative_name: name, prefix });
+ }
+ }
+
+ return aliases;
+}
+
+function parseBrowserVersion(compatNode) {
+ for (const term in compatNode) {
+ if (term.startsWith("_")) {
+ // Ignore exploring if the term is _aliasOf or __compat.
+ continue;
+ }
+
+ const compatTable = getCompatTable(compatNode, [term]);
+ if (compatTable?.support) {
+ for (const [browserId, supportItem] of Object.entries(
+ compatTable.support
+ )) {
+ // supportItem is an array when there are info for both prefixed and non-prefixed
+ // versions. If it's not an array in the original data, transform it into one
+ // since we'd have to do it in MDNCompatibility at runtime otherwise.
+ if (!Array.isArray(supportItem)) {
+ compatTable.support[browserId] = [supportItem];
+ }
+ for (const item of compatTable.support[browserId]) {
+ replaceVersionsInBrowserSupport(item);
+ }
+ }
+ }
+
+ // Handle deeper node as well.
+ parseBrowserVersion(compatNode[term]);
+ }
+}
+
+function replaceVersionsInBrowserSupport(browserSupport) {
+ browserSupport.added = asFloatVersion(browserSupport.version_added);
+ browserSupport.removed = asFloatVersion(browserSupport.version_removed);
+}
+
+function asFloatVersion(version) {
+ // `version` is not always a string (can be null, or a boolean) and in such case,
+ // we want to keep it that way.
+ if (typeof version !== "string") {
+ return version;
+ }
+
+ if (version.startsWith("\u2264")) {
+ // MDN compatibility data started to use an expression like "≤66" for version.
+ // We just ignore the character here.
+ version = version.substring(1);
+ }
+
+ return parseFloat(version);
+}
+
+/**
+ * Remove all unused data from the file so it's smaller and faster to load
+ */
+function removeUnusedData(compatNode) {
+ for (const term in compatNode) {
+ if (term.startsWith("_")) {
+ // Ignore exploring if the term is _aliasOf or __compat.
+ continue;
+ }
+
+ const compatTable = getCompatTable(compatNode, [term]);
+
+ // A term may only have a `_aliasOf` property (e.g. for word-wrap), so we don't have
+ // compat data in it directly.
+ if (compatTable) {
+ // source_file references the name of the file in the MDN compat data repo where the
+ // property is handled. We don't make use of it so we can remove it.
+ delete compatTable.source_file;
+
+ // Not used at the moment. Doesn't seem to have much information anyway
+ delete compatTable.description;
+
+ if (compatTable?.support) {
+ for (const [browserId, supportItem] of Object.entries(
+ compatTable.support
+ )) {
+ // Remove any browser we won't handle
+ if (!TARGET_BROWSER_ID.includes(browserId)) {
+ delete compatTable.support[browserId];
+ continue;
+ }
+
+ // Remove `version_added` and `version_removed`, that are parsed in `replaceVersionsInBrowserSupport`
+ // and which we don't need anymore.
+ for (const item of supportItem) {
+ delete item.version_added;
+ delete item.version_removed;
+ // Those might be interesting, but we're not using them at the moment, so let's
+ // remove them as they can be quite lengthy
+ delete item.notes;
+ }
+ }
+ }
+ }
+
+ // Handle deeper node as well.
+ removeUnusedData(compatNode[term]);
+ }
+}
+
+function exportData(data, fileName) {
+ const fs = require("fs");
+ const path = require("path");
+
+ const content = `${JSON.stringify(data)}`;
+
+ fs.writeFile(
+ path.resolve(__dirname, "../dataset", fileName),
+ content,
+ err => {
+ if (err) {
+ console.error(err);
+ } else {
+ console.log(`${fileName} downloaded`);
+ }
+ }
+ );
+}
diff --git a/devtools/shared/compatibility/compatibility-user-settings.js b/devtools/shared/compatibility/compatibility-user-settings.js
new file mode 100644
index 0000000000..cfd0552968
--- /dev/null
+++ b/devtools/shared/compatibility/compatibility-user-settings.js
@@ -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/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+loader.lazyRequireGetter(
+ this,
+ ["TARGET_BROWSER_ID", "TARGET_BROWSER_STATUS", "TARGET_BROWSER_PREF"],
+ "resource://devtools/shared/compatibility/constants.js",
+ true
+);
+
+/**
+ * Returns the full list of browsers in the RemoteSetting devtools-compatibility-browsers
+ * collection (which is a flat copy of MDN compat data), sorted by browser and version.
+ *
+ * @returns Promise<Array<Object>> : Objects in the array have the following shape:
+ * - {string} id: The browser id (e.g. `firefox`,`safari_ios`). Should be one of TARGET_BROWSER_ID
+ * - {string} name: The browser display name (e.g. `Firefox`,`Safari for IOS`, …)
+ * - {string} version: The browser version (e.g. `99`,`15.3`, `1.0.4`, …)
+ * - {string} status: The browser status (e.g. `current`,`beta`, …). Should be one of TARGET_BROWSER_STATUS
+ */
+async function getBrowsersList() {
+ const records = await RemoteSettings("devtools-compatibility-browsers", {
+ filterFunc: record => {
+ if (
+ !TARGET_BROWSER_ID.includes(record.browserid) ||
+ !TARGET_BROWSER_STATUS.includes(record.status)
+ ) {
+ return null;
+ }
+ return {
+ id: record.browserid,
+ name: record.name,
+ version: record.version,
+ status: record.status,
+ };
+ },
+ }).get();
+
+ const numericCollator = new Intl.Collator([], { numeric: true });
+ records.sort((a, b) => {
+ if (a.id == b.id) {
+ return numericCollator.compare(a.version, b.version);
+ }
+ return a.id > b.id ? 1 : -1;
+ });
+
+ // MDN compat data might have browser data that have the same id and status.
+ // e.g. https://github.com/mdn/browser-compat-data/commit/53453400ecb2a85e7750d99e2e0a1611648d1d56#diff-31a16f09157f13354db27821261604aa
+ // In this case, only keep the newer version to keep uniqueness by id and status.
+ // This needs to be done after sorting since we rely on the order of the records.
+ return records.filter((record, index, arr) => {
+ const nextRecord = arr[index + 1];
+ // If the next record in the array is the same browser and has the same status, filter
+ // out this one since it's a lower version.
+ if (
+ nextRecord &&
+ record.id === nextRecord.id &&
+ record.status === nextRecord.status
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+}
+
+/**
+ * Returns the list of browsers for which we should check compatibility issues.
+ *
+ * @returns Promise<Array<Object>> : Objects in the array have the following shape:
+ * - {string} id: The browser id (e.g. `firefox`,`safari_ios`). Should be one of TARGET_BROWSER_ID
+ * - {string} name: The browser display name (e.g. `Firefox`,`Safari for IOS`, …)
+ * - {string} version: The browser version (e.g. `99`,`15.3`, `1.0.4`, …)
+ * - {string} status: The browser status (e.g. `current`,`beta`, …). Should be one of TARGET_BROWSER_STATUS
+ */
+async function getTargetBrowsers() {
+ const targetsString = Services.prefs.getCharPref(TARGET_BROWSER_PREF, "");
+ const browsers = await getBrowsersList();
+
+ // If not value are stored in the pref, it means the user did not chose specific browsers,
+ // so we need to return the full list.
+ if (!targetsString) {
+ return browsers;
+ }
+
+ const selectedBrowsersAndStatuses = JSON.parse(targetsString);
+ return browsers.filter(
+ browser =>
+ !!selectedBrowsersAndStatuses.find(
+ ({ id, status }) => browser.id == id && browser.status == status
+ )
+ );
+}
+
+/**
+ * Store the list of browser id and status that should be used for checking compatibility
+ * issues.
+ *
+ * @param {Object[]} browsers
+ * @param {string} browsers[].id: The browser id. Should be one of TARGET_BROWSER_ID
+ * @param {string} browsers[].status: The browser status. Should be one of TARGET_BROWSER_STATUS
+ */
+function setTargetBrowsers(browsers) {
+ Services.prefs.setCharPref(
+ TARGET_BROWSER_PREF,
+ JSON.stringify(
+ // Only store the id and the status
+ browsers.map(browser => ({
+ id: browser.id,
+ status: browser.status,
+ }))
+ )
+ );
+}
+
+module.exports = {
+ getBrowsersList,
+ getTargetBrowsers,
+ setTargetBrowsers,
+};
diff --git a/devtools/shared/compatibility/constants.js b/devtools/shared/compatibility/constants.js
new file mode 100644
index 0000000000..472b7728aa
--- /dev/null
+++ b/devtools/shared/compatibility/constants.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 might be required from a node script (./bin/update.js), so don't use
+// Chrome API here.
+
+const TARGET_BROWSER_ID = [
+ "firefox",
+ "firefox_android",
+ "chrome",
+ "chrome_android",
+ "safari",
+ "safari_ios",
+ "edge",
+ "ie",
+];
+const TARGET_BROWSER_STATUS = ["esr", "current", "beta", "nightly"];
+const TARGET_BROWSER_PREF = "devtools.inspector.compatibility.target-browsers";
+
+module.exports = {
+ TARGET_BROWSER_ID,
+ TARGET_BROWSER_PREF,
+ TARGET_BROWSER_STATUS,
+};
diff --git a/devtools/shared/compatibility/dataset/css-properties.json b/devtools/shared/compatibility/dataset/css-properties.json
new file mode 100644
index 0000000000..0fd34ad026
--- /dev/null
+++ b/devtools/shared/compatibility/dataset/css-properties.json
@@ -0,0 +1 @@
+{"-moz-float-edge":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-float-edge","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-moz-force-broken-image-icon":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-force-broken-image-icon","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-moz-image-region":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-image-region","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"111","added":1,"removed":112}],"firefox_android":[{"version_last":"111","added":4,"removed":112}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-moz-orient":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-orient","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":6}],"firefox_android":[{"added":6}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"auto":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"39","added":21,"removed":40}],"firefox_android":[{"version_last":"39","added":21,"removed":40}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"inline_and_block":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":40}],"firefox_android":[{"added":40}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"-moz-user-focus":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-user-focus","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-moz-user-input":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-user-input","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"auto":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"disabled":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"59","added":1,"removed":60}],"firefox_android":[{"version_last":"59","added":4,"removed":60}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"enabled":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"59","added":1,"removed":60}],"firefox_android":[{"version_last":"59","added":4,"removed":60}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"none":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"-webkit-app-region":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-webkit-border-before":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-border-before","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":8}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"-webkit-border-horizontal-spacing":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-border-vertical-spacing":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-box-reflect":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-box-reflect","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"-webkit-column-axis":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-column-break-after":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-column-break-before":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-column-break-inside":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-column-progression":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-cursor-visibility":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-hyphenate-character":{"_aliasOf":"hyphenate-character"},"-webkit-hyphenate-limit-after":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-hyphenate-limit-before":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-hyphenate-limit-lines":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-initial-letter":{"_aliasOf":"initial-letter"},"-webkit-line-align":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-line-box-contain":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-line-clamp":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-line-clamp","spec_url":"https://drafts.csswg.org/css-overflow-4/#propdef--webkit-line-clamp","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":17}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]},"tags":["web-features:line-clamp"]},"none":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"-webkit-line-grid":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-line-snap":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-locale":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-logical-height":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-logical-width":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-margin-after":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-margin-before":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-mask-attachment":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-attachment","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"version_last":"23","added":1,"removed":24}],"chrome_android":[{"version_last":"18","added":18,"removed":25}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"version_last":"6","added":4,"removed":7}],"safari_ios":[{"version_last":"6","added":3.2,"removed":7}]}}},"-webkit-mask-box-image":{"_aliasOf":"mask-border"},"-webkit-mask-box-image-outset":{"_aliasOf":"mask-border-outset"},"-webkit-mask-box-image-repeat":{"_aliasOf":"mask-border-repeat"},"-webkit-mask-box-image-slice":{"_aliasOf":"mask-border-slice"},"-webkit-mask-box-image-source":{"_aliasOf":"mask-border-source"},"-webkit-mask-box-image-width":{"_aliasOf":"mask-border-width"},"-webkit-mask-composite":{"_aliasOf":"mask-composite"},"-webkit-mask-position-x":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-position-x","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":18}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":3.1}],"safari_ios":[{"added":2}]}}},"-webkit-mask-position-y":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-position-y","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":18}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":3.1}],"safari_ios":[{"added":2}]}}},"-webkit-mask-repeat-x":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-repeat-x","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"version_last":"119","added":3,"removed":120}],"chrome_android":[{"version_last":"119","added":18,"removed":120}],"edge":[{"version_last":"119","added":79,"removed":120}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"version_last":"14.1","added":5,"removed":15}],"safari_ios":[{"version_last":"14.5","added":5,"removed":15}]}}},"-webkit-mask-repeat-y":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-repeat-y","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"version_last":"119","added":3,"removed":120}],"chrome_android":[{"version_last":"119","added":18,"removed":120}],"edge":[{"version_last":"119","added":79,"removed":120}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"version_last":"14.1","added":5,"removed":15}],"safari_ios":[{"version_last":"14.5","added":5,"removed":15}]}}},"-webkit-mask-source-type":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-max-logical-height":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-max-logical-width":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-min-logical-height":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-min-logical-width":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-nbsp-mode":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-overflow-scrolling":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-overflow-scrolling","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"version_last":"12.2","added":5,"removed":13}]}}},"-webkit-perspective-origin-x":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-perspective-origin-y":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-rtl-ordering":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-tap-highlight-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-tap-highlight-color","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":16}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":4}]}}},"-webkit-text-combine":{"_aliasOf":"text-combine-upright"},"-webkit-text-decoration-skip":{"_aliasOf":"text-decoration-skip"},"-webkit-text-decorations-in-effect":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-text-fill-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-text-fill-color","spec_url":"https://compat.spec.whatwg.org/#the-webkit-text-fill-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}}},"-webkit-text-security":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-text-security","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":114}],"firefox_android":[{"added":114}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"-webkit-text-stroke":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-text-stroke","spec_url":"https://compat.spec.whatwg.org/#the-webkit-text-stroke","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":15}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}}},"-webkit-text-stroke-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-text-stroke-color","spec_url":"https://compat.spec.whatwg.org/#the-webkit-text-stroke-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":15}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}}},"-webkit-text-stroke-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-text-stroke-width","spec_url":"https://compat.spec.whatwg.org/#the-webkit-text-stroke-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":15}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}}},"-webkit-text-zoom":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-touch-callout":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-webkit-touch-callout","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":2}]}}},"-webkit-transform-origin-x":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-transform-origin-y":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-transform-origin-z":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-user-drag":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"-webkit-user-modify":{"_aliasOf":"user-modify"},"accent-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/accent-color","spec_url":"https://drafts.csswg.org/css-ui/#widget-accent","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93}],"chrome_android":[{"added":93}],"edge":[{"added":93}],"firefox":[{"added":92}],"firefox_android":[{"added":92}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"align-content":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/align-content","spec_url":["https://drafts.csswg.org/css-align/#align-justify-content","https://drafts.csswg.org/css-flexbox/#align-content-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":28},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":28},{"prefix":"-webkit-","added":49}],"ie":[{"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]}},"flex_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-justify-content","https://drafts.csswg.org/css-flexbox/#align-content-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":11}],"safari":[{"added":9}],"safari_ios":[{"added":9}]},"tags":["web-features:flexbox"]},"baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}}},"first_baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"last_baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"version_last":"85","added":59,"removed":86}],"chrome_android":[{"partial_implementation":true,"version_last":"85","added":59,"removed":86}],"edge":[{"partial_implementation":true,"version_last":"85","added":79,"removed":86}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"partial_implementation":true,"added":11}],"safari_ios":[{"partial_implementation":true,"added":11}]}}},"safe_unsafe":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"space-evenly":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":60}],"chrome_android":[{"added":60}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"start_end":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93}],"chrome_android":[{"added":93}],"edge":[{"added":93}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":15.6}],"safari_ios":[{"added":15.6}]}}},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]},"tags":["web-features:flexbox"]}}},"grid_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-justify-content","https://drafts.csswg.org/css-flexbox/#align-content-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":52}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"_aliasOf":"align-content"},"align-items":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/align-items","spec_url":["https://drafts.csswg.org/css-align/#align-items-property","https://drafts.csswg.org/css-flexbox/#align-items-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":20},{"prefix":"-webkit-","added":49}],"ie":[{"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]}},"flex_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-items-property","https://drafts.csswg.org/css-flexbox/#align-items-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52},{"partial_implementation":true,"version_last":"51","added":21,"removed":52}],"chrome_android":[{"added":52},{"partial_implementation":true,"version_last":"51","added":25,"removed":52}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11}],"safari":[{"added":7}],"safari_ios":[{"added":7}]},"tags":["web-features:flexbox"]},"baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]},"tags":["web-features:flexbox"]}},"first_baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"last_baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":108}],"chrome_android":[{"added":108}],"edge":[{"added":108}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]}}},"safe_unsafe":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"start_end":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93}],"chrome_android":[{"added":93}],"edge":[{"added":93}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}}},"grid_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-items-property","https://drafts.csswg.org/css-flexbox/#align-items-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":52}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"start_end":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:grid"]}}},"_aliasOf":"align-items"},"align-self":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/align-self","spec_url":["https://drafts.csswg.org/css-align/#align-self-property","https://drafts.csswg.org/css-flexbox/#align-items-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":20},{"prefix":"-webkit-","added":49}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]}},"flex_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-self-property","https://drafts.csswg.org/css-flexbox/#align-items-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"partial_implementation":true,"version_last":"35","added":21,"removed":36}],"chrome_android":[{"added":36},{"partial_implementation":true,"version_last":"35","added":25,"removed":36}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11}],"safari":[{"added":7}],"safari_ios":[{"added":7}]},"tags":["web-features:flexbox"]},"baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]},"tags":["web-features:flexbox"]}},"first_baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"last_baseline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":108}],"chrome_android":[{"added":108}],"edge":[{"added":108}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]}}},"safe_unsafe":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"start_end":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93}],"chrome_android":[{"added":93}],"edge":[{"added":93}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]},"tags":["web-features:flexbox"]}}},"grid_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-self-property","https://drafts.csswg.org/css-flexbox/#align-items-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":52}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"partial_implementation":true,"prefix":"-ms-","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid_context"},"_aliasOf":"align-self","-ms-grid_context":{"_aliasOf":"grid_context"}},"align-tracks":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/align-tracks","spec_url":"https://drafts.csswg.org/css-grid-3/#tracks-alignment","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.grid-template-masonry-value.enabled","type":"preference","value_to_set":"true"}],"added":77}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]},"tags":["web-features:masonry"]}},"alignment-baseline":{"__compat":{"spec_url":["https://drafts.csswg.org/css-inline-3/#alignment-baseline-property","https://svgwg.org/svg2-draft/text.html#AlignmentBaselineProperty"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"all":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/all","spec_url":"https://drafts.csswg.org/css-cascade/#all-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":27}],"firefox_android":[{"added":27}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"alt":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/alt","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":9},{"prefix":"-webkit-","added":8}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":8}]}},"_aliasOf":"alt"},"animation":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation","spec_url":"https://drafts.csswg.org/css-animations/#animation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"animation-timeline":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":115}],"chrome_android":[{"partial_implementation":true,"added":115}],"edge":[{"partial_implementation":true,"added":115}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"animation"},"animation-composition":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-composition","spec_url":"https://drafts.csswg.org/css-animations-2/#animation-composition","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":115}],"firefox_android":[{"added":115}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"animation-delay":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-delay","spec_url":"https://drafts.csswg.org/css-animations/#animation-delay","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"_aliasOf":"animation-delay"},"animation-direction":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-direction","spec_url":"https://drafts.csswg.org/css-animations/#animation-direction","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"alternate-reverse":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":19}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":16}],"firefox_android":[{"added":16}],"ie":[{"added":10}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"reverse":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":19}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":16}],"firefox_android":[{"added":16}],"ie":[{"added":10}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"_aliasOf":"animation-direction"},"animation-duration":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-duration","spec_url":"https://drafts.csswg.org/css-animations/#animation-duration","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"partial_implementation":true,"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":4.2}]}},"auto":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-duration#Values","spec_url":"https://drafts.csswg.org/css-animations-2/#valdef-animation-duration-auto","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"animation-duration"},"animation-fill-mode":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-fill-mode","spec_url":"https://drafts.csswg.org/css-animations/#animation-fill-mode","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":5}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":4}]}},"_aliasOf":"animation-fill-mode"},"animation-iteration-count":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-iteration-count","spec_url":"https://drafts.csswg.org/css-animations/#animation-iteration-count","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"_aliasOf":"animation-iteration-count"},"animation-name":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-name","spec_url":"https://drafts.csswg.org/css-animations/#animation-name","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"_aliasOf":"animation-name"},"animation-play-state":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-play-state","spec_url":"https://drafts.csswg.org/css-animations/#animation-play-state","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"animation-play-state"},"animation-range":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-range","spec_url":"https://drafts.csswg.org/scroll-animations/#animation-range","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"animation-range-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-range-end","spec_url":"https://drafts.csswg.org/scroll-animations/#animation-range-end","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"animation-range-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-range-start","spec_url":"https://drafts.csswg.org/scroll-animations/#animation-range-start","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"animation-timeline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-timeline","spec_url":"https://drafts.csswg.org/css-animations-2/#animation-timeline","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":110}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"scroll":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-timeline/scroll","spec_url":"https://drafts.csswg.org/scroll-animations/#scroll-notation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":110}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"view":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-timeline/view","spec_url":"https://drafts.csswg.org/scroll-animations/#view-notation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":114}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"animation-timing-function":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/animation-timing-function","spec_url":"https://drafts.csswg.org/css-animations/#animation-timing-function","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":43},{"prefix":"-webkit-","added":3}],"chrome_android":[{"added":43},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":5}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"jump":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":77}],"chrome_android":[{"added":77}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":14}],"safari_ios":[{"added":14}]}}},"_aliasOf":"animation-timing-function"},"appearance":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/appearance","spec_url":"https://drafts.csswg.org/css-ui/#appearance-switching","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":84},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":84},{"prefix":"-webkit-","added":18}],"edge":[{"added":84},{"prefix":"-webkit-","added":12}],"firefox":[{"added":80},{"prefix":"-webkit-","added":64},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":80},{"prefix":"-webkit-","added":64},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":83}],"chrome_android":[{"added":83}],"edge":[{"added":83}],"firefox":[{"added":80}],"firefox_android":[{"added":80}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"compat-auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"menulist-button":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":80},{"partial_implementation":true,"added":1}],"firefox_android":[{"added":80},{"partial_implementation":true,"added":4}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"none":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":54},{"partial_implementation":true,"added":1}],"firefox_android":[{"added":54},{"partial_implementation":true,"added":4}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":3}]}}},"textfield":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"_aliasOf":"appearance"},"aspect-ratio":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/aspect-ratio","spec_url":"https://drafts.csswg.org/css-sizing-4/#aspect-ratio","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":88}],"chrome_android":[{"added":88}],"edge":[{"added":88}],"firefox":[{"added":89}],"firefox_android":[{"added":89}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"backdrop-filter":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/backdrop-filter","spec_url":"https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":76}],"chrome_android":[{"added":76}],"edge":[{"added":17}],"firefox":[{"added":103}],"firefox_android":[{"added":103}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":9}],"safari_ios":[{"prefix":"-webkit-","added":9}]}},"_aliasOf":"backdrop-filter"},"backface-visibility":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/backface-visibility","spec_url":"https://drafts.csswg.org/css-transforms-2/#backface-visibility-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"prefix":"-webkit-","added":12}],"chrome_android":[{"added":36},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":10,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":10,"removed":false}],"ie":[{"added":10}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":5.1}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":5}]}},"_aliasOf":"backface-visibility"},"background":{"SVG_image_as_background":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":3.1}],"safari_ios":[{"added":1}]}}},"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-background","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"background-clip":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":22}],"firefox_android":[{"added":22}],"ie":[{"added":9}],"safari":[{"added":5.1}],"safari_ios":[{"added":4}]}}},"background-origin":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":22}],"firefox_android":[{"added":22}],"ie":[{"added":9}],"safari":[{"added":5.1}],"safari_ios":[{"added":4}]}}},"background-size":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":9}],"firefox_android":[{"added":18}],"ie":[{"added":9}],"safari":[{"added":5.1}],"safari_ios":[{"added":4}]}}},"multiple_backgrounds":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}}},"background-attachment":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-attachment","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-background-attachment","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":3.2}]}},"fixed":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":2}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":15.4},{"partial_implementation":true,"version_last":"15.3","added":14,"removed":15.4},{"version_last":"13.1","added":3.1,"removed":14}],"safari_ios":[{"added":15.4},{"partial_implementation":true,"version_last":"15.3","added":5,"removed":15.4}]}}},"local":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":25}],"firefox_android":[{"added":25}],"ie":[{"added":9}],"safari":[{"added":15.4},{"partial_implementation":true,"version_last":"15.3","added":13,"removed":15.4},{"version_last":"12.1","added":5,"removed":13}],"safari_ios":[{"added":15.4},{"partial_implementation":true,"version_last":"15.3","added":13,"removed":15.4},{"version_last":"12.2","added":4.2,"removed":13}]}}},"multiple_backgrounds":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":1.3}],"safari_ios":[{"added":3.2}]}}}},"background-blend-mode":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-blend-mode","spec_url":"https://drafts.fxtf.org/compositing/#background-blend-mode","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":35}],"chrome_android":[{"added":35}],"edge":[{"added":79}],"firefox":[{"added":30}],"firefox_android":[{"added":30}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"background-clip":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-clip","spec_url":"https://drafts.csswg.org/css-backgrounds/#background-clip","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"partial_implementation":true,"prefix":"-moz-","version_last":"3.6","added":1,"removed":4}],"firefox_android":[{"added":14},{"prefix":"-webkit-","added":49}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":5},{"prefix":"-webkit-","added":1}]}},"content-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":14}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"text":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":3}],"chrome_android":[{"partial_implementation":true,"added":18}],"edge":[{"partial_implementation":true,"added":79},{"version_last":"18","added":15,"removed":79},{"partial_implementation":true,"version_last":"14","added":12,"removed":15}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":14},{"partial_implementation":true,"added":4}],"safari_ios":[{"added":14},{"partial_implementation":true,"added":3.2}]}}},"_aliasOf":"background-clip"},"background-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-color","spec_url":"https://drafts.csswg.org/css-backgrounds/#background-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"background-image":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-image","spec_url":"https://drafts.csswg.org/css-backgrounds/#background-image","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"element":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/element()","spec_url":"https://drafts.csswg.org/css-images-4/#element-notation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"prefix":"-moz-","added":4}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"element"},"gradients":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/gradient","spec_url":"https://drafts.csswg.org/css-images-4/#gradients","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":10}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]},"tags":["web-features:background-gradients"]}},"image-rect":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/-moz-image-rect","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"prefix":"-moz-","version_last":"119","added":4,"removed":120}],"firefox_android":[{"prefix":"-moz-","version_last":"119","added":4,"removed":120}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"image-rect"},"image-set":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/image-set()","spec_url":"https://drafts.csswg.org/css-images-4/#image-set-notation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":113},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":113},{"prefix":"-webkit-","added":25}],"edge":[{"added":113},{"prefix":"-webkit-","added":79}],"firefox":[{"added":88},{"prefix":"-webkit-","added":90}],"firefox_android":[{"added":88},{"prefix":"-webkit-","added":90}],"ie":[{"added":false}],"safari":[{"added":14},{"partial_implementation":true,"prefix":"-webkit-","added":6}],"safari_ios":[{"added":14},{"partial_implementation":true,"prefix":"-webkit-","added":6}]}},"_aliasOf":"image-set"},"multiple_backgrounds":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}},"svg_images":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":8}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":5}]}}},"-moz-element":{"_aliasOf":"element"},"-moz-image-rect":{"_aliasOf":"image-rect"},"-webkit-image-set":{"_aliasOf":"image-set"}},"background-origin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-origin","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-background-origin","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"partial_implementation":true,"prefix":"-moz-","version_last":"3.6","added":1,"removed":4}],"firefox_android":[{"added":14},{"prefix":"-webkit-","added":49}],"ie":[{"added":9}],"safari":[{"added":3},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":1},{"prefix":"-webkit-","added":1}]}},"content-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":14}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"_aliasOf":"background-origin"},"background-position":{"4_value_syntax":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":25}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":13}],"firefox_android":[{"added":14}],"ie":[{"added":9}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-position","spec_url":"https://drafts.csswg.org/css-backgrounds/#background-position","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"multiple_backgrounds":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}}},"background-position-x":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-position-x","spec_url":"https://drafts.csswg.org/css-backgrounds-4/#background-position-longhands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"side-relative_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":9}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}}},"background-position-y":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-position-y","spec_url":"https://drafts.csswg.org/css-backgrounds-4/#background-position-longhands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"side-relative_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":9}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}}},"background-repeat":{"2-value":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":3}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":13}],"firefox_android":[{"added":14}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4}]}}},"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-repeat","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-background-repeat","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"multiple_backgrounds":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}},"round_space":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":30}],"chrome_android":[{"added":30}],"edge":[{"added":12}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":9}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}}},"background-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/background-size","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-background-size","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":3},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"3.6","added":3.6,"removed":4}],"firefox_android":[{"added":4},{"prefix":"-webkit-","added":49}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":4.2},{"prefix":"-webkit-","added":1}]}},"contain_and_cover":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":3}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"_aliasOf":"background-size"},"baseline-shift":{"__compat":{"spec_url":["https://drafts.csswg.org/css-inline-3/#baseline-shift-property","https://svgwg.org/svg2-draft/text.html#BaselineShiftProperty"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"baseline-source":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/baseline-source","spec_url":"https://drafts.csswg.org/css-inline/#baseline-source","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":115}],"firefox_android":[{"added":115}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"block-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/block-size","spec_url":["https://drafts.csswg.org/css-logical/#dimension-properties","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"min-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"border":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border","spec_url":"https://drafts.csswg.org/css-backgrounds/#propdef-border","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-block","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-block-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-color","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-block-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-block-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-end","spec_url":"https://drafts.csswg.org/css-logical/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-end-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-end-color","spec_url":"https://drafts.csswg.org/css-logical/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-end-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-end-style","spec_url":"https://drafts.csswg.org/css-logical/#border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-end-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-end-width","spec_url":"https://drafts.csswg.org/css-logical/#border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-start","spec_url":"https://drafts.csswg.org/css-logical/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-start-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-start-color","spec_url":"https://drafts.csswg.org/css-logical/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-start-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-start-style","spec_url":"https://drafts.csswg.org/css-logical/#border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-start-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-start-width","spec_url":"https://drafts.csswg.org/css-logical/#border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-block-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-style","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-block-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-block-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-block-width","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-block-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-bottom","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-bottom-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-bottom-color","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-bottom-left-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-bottom-left-radius","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-radius","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-bottomleft","version_last":"11","added":1,"removed":12}],"firefox_android":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-bottomleft","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":4.2},{"prefix":"-webkit-","added":1}]}},"elliptical_corners":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"partial_implementation":true,"version_last":"3.6","added":1,"removed":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"_aliasOf":"border-bottom-left-radius"},"border-bottom-right-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-bottom-right-radius","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-radius","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-bottomright","version_last":"11","added":1,"removed":12}],"firefox_android":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-bottomright","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":4.2},{"prefix":"-webkit-","added":1}]}},"elliptical_corners":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"partial_implementation":true,"version_last":"3.6","added":1,"removed":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"_aliasOf":"border-bottom-right-radius"},"border-bottom-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-bottom-style","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-bottom-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-bottom-width","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-collapse":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-collapse","spec_url":"https://drafts.csswg.org/css2/#propdef-border-collapse","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5}],"safari":[{"added":1.2}],"safari_ios":[{"added":3}]}}},"border-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-color","spec_url":["https://drafts.csswg.org/css-logical/#logical-shorthand-keyword","https://drafts.csswg.org/css-backgrounds/#border-color"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-end-end-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-end-end-radius","spec_url":"https://drafts.csswg.org/css-logical/#border-radius-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":89}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"border-end-start-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-end-start-radius","spec_url":"https://drafts.csswg.org/css-logical/#border-radius-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":89}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"border-image":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-image","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-image","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":16},{"prefix":"-webkit-","added":7}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":15},{"prefix":"-moz-","added":3.5}],"firefox_android":[{"added":15},{"prefix":"-moz-","added":4}],"ie":[{"added":11}],"safari":[{"added":6},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":6},{"prefix":"-webkit-","added":3.2}]},"tags":["web-features:border-image"]},"fill":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":16}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":6}]},"tags":["web-features:border-image"]}},"gradient":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":7}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":29}],"firefox_android":[{"added":29}],"ie":[{"added":11}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]},"tags":["web-features:border-image"]}},"optional_border_image_slice":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":16}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":6}]},"tags":["web-features:border-image"]}},"_aliasOf":"border-image"},"border-image-outset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-image-outset","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-image-outset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":6}]},"tags":["web-features:border-image"]}},"border-image-repeat":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-image-repeat","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":9.3}]},"tags":["web-features:border-image"]},"round":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":30}],"chrome_android":[{"added":30}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]},"tags":["web-features:border-image"]}},"space":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56}],"chrome_android":[{"added":56}],"edge":[{"added":12}],"firefox":[{"added":50}],"firefox_android":[{"added":50}],"ie":[{"added":11}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]},"tags":["web-features:border-image"]}}},"border-image-slice":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-image-slice","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-image-slice","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":6}]},"tags":["web-features:border-image"]},"_aliasOf":"border-image-slice"},"border-image-source":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-image-source","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-image-source","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":6}]},"tags":["web-features:border-image"]}},"border-image-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-image-width","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-image-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":13}],"firefox_android":[{"added":14}],"ie":[{"added":11}],"safari":[{"added":6}],"safari_ios":[{"added":6}]},"tags":["web-features:border-image"]}},"border-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-inline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-inline-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-color","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-inline-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-inline-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-end","spec_url":"https://drafts.csswg.org/css-logical/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-inline-end-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-end-color","spec_url":"https://drafts.csswg.org/css-logical/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-border-end-color","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-border-end-color","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"border-inline-end-color"},"border-inline-end-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-end-style","spec_url":"https://drafts.csswg.org/css-logical/#border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-border-end-style","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-border-end-style","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"border-inline-end-style"},"border-inline-end-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-end-width","spec_url":"https://drafts.csswg.org/css-logical/#border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-border-end-width","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-border-end-width","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"border-inline-end-width"},"border-inline-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-start","spec_url":"https://drafts.csswg.org/css-logical/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-inline-start-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-start-color","spec_url":"https://drafts.csswg.org/css-logical/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-border-start-color","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-border-start-color","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"border-inline-start-color"},"border-inline-start-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-start-style","spec_url":"https://drafts.csswg.org/css-logical/#border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-border-start-style","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-border-start-style","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"border-inline-start-style"},"border-inline-start-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-start-width","spec_url":"https://drafts.csswg.org/css-logical/#border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"border-inline-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-style","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-inline-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-inline-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-inline-width","spec_url":"https://drafts.csswg.org/css-logical/#propdef-border-inline-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"border-left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-left","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-left-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-left-color","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-left-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-left-style","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":14}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-left-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-left-width","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-radius":{"4_values_for_4_corners":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-radius","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-radius","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-moz-","version_last":"11","added":1,"removed":12}],"firefox_android":[{"added":4},{"prefix":"-moz-","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":4.2},{"prefix":"-webkit-","added":1}]}},"elliptical_borders":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":4.2}]}}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":8}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"_aliasOf":"border-radius"},"border-right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-right","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":14}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-right-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-right-color","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-right-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-right-style","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":14}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-right-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-right-width","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-spacing":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-spacing","spec_url":"https://drafts.csswg.org/css2/#separated-borders","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-start-end-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-start-end-radius","spec_url":"https://drafts.csswg.org/css-logical/#border-radius-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":89}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"border-start-start-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-start-start-radius","spec_url":"https://drafts.csswg.org/css-logical/#border-radius-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":89}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"border-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-style","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":3}]}}},"border-top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-top","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-top-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-top-color","spec_url":"https://drafts.csswg.org/css-backgrounds/#border-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-top-left-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-top-left-radius","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-radius","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-topleft","version_last":"11","added":1,"removed":12}],"firefox_android":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-topleft","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":4.2},{"prefix":"-webkit-","added":1}]}},"elliptical_corners":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"partial_implementation":true,"version_last":"3.6","added":1,"removed":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"_aliasOf":"border-top-left-radius"},"border-top-right-radius":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-top-right-radius","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-radius","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-topright","version_last":"11","added":1,"removed":12}],"firefox_android":[{"added":4},{"prefix":"-webkit-","added":49},{"alternative_name":"-moz-border-radius-topright","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":4.2},{"prefix":"-webkit-","added":1}]}},"elliptical_corners":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"partial_implementation":true,"version_last":"3.6","added":1,"removed":4}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"_aliasOf":"border-top-right-radius"},"border-top-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-top-style","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-top-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-top-width","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"border-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/border-width","spec_url":"https://drafts.csswg.org/css-backgrounds/#the-border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":3}]}}},"bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/bottom","spec_url":"https://drafts.csswg.org/css-position/#insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"box-align":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-align","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-align"},"box-decoration-break":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-decoration-break","spec_url":"https://drafts.csswg.org/css-break/#break-decoration","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"prefix":"-webkit-","added":22}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79}],"firefox":[{"added":32},{"alternative_name":"-moz-background-inline-policy","version_last":"31","added":1,"removed":32}],"firefox_android":[{"added":32},{"alternative_name":"-moz-background-inline-policy","version_last":"31","added":4,"removed":32}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":7}],"safari_ios":[{"prefix":"-webkit-","added":7}]}},"_aliasOf":"box-decoration-break"},"box-direction":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-direction","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-direction"},"box-flex":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-flex","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-flex"},"box-flex-group":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-flex-group","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","version_last":"66","added":1,"removed":67}],"chrome_android":[{"prefix":"-webkit-","version_last":"66","added":18,"removed":67}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-flex-group"},"box-lines":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-lines","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","version_last":"66","added":1,"removed":67}],"chrome_android":[{"prefix":"-webkit-","version_last":"66","added":18,"removed":67}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-lines"},"box-ordinal-group":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-ordinal-group","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-ordinal-group"},"box-orient":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-orient","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-orient"},"box-pack":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-pack","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":1.1,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"_aliasOf":"box-pack"},"box-shadow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-shadow","spec_url":"https://drafts.csswg.org/css-backgrounds/#box-shadow","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":10},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"12","added":3.5,"removed":13}],"firefox_android":[{"added":4},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5.1},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":5},{"prefix":"-webkit-","added":1}]}},"inset":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":10},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"prefix":"-moz-","version_last":"12","added":3.5,"removed":13}],"firefox_android":[{"added":4},{"prefix":"-moz-","version_last":"10","added":4,"removed":14}],"ie":[{"partial_implementation":true,"added":9}],"safari":[{"added":5.1},{"prefix":"-webkit-","added":5}],"safari_ios":[{"added":5},{"prefix":"-webkit-","added":4.2}]}},"_aliasOf":"inset"},"multiple_shadows":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":10},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"prefix":"-moz-","version_last":"12","added":3.5,"removed":13}],"firefox_android":[{"added":4},{"prefix":"-moz-","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5.1},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":5},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"multiple_shadows"},"spread_radius":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":10},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12}],"firefox":[{"added":4},{"prefix":"-moz-","version_last":"12","added":3.5,"removed":13}],"firefox_android":[{"added":4},{"prefix":"-moz-","version_last":"10","added":4,"removed":14}],"ie":[{"added":9}],"safari":[{"added":5.1},{"prefix":"-webkit-","added":5}],"safari_ios":[{"added":5},{"prefix":"-webkit-","added":4.2}]}},"_aliasOf":"spread_radius"},"_aliasOf":"box-shadow","-webkit-inset":{"_aliasOf":"inset"},"-moz-inset":{"_aliasOf":"inset"},"-webkit-multiple_shadows":{"_aliasOf":"multiple_shadows"},"-moz-multiple_shadows":{"_aliasOf":"multiple_shadows"},"-webkit-spread_radius":{"_aliasOf":"spread_radius"},"-moz-spread_radius":{"_aliasOf":"spread_radius"}},"box-sizing":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/box-sizing","spec_url":"https://drafts.csswg.org/css-sizing/#box-sizing","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":10},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":29},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":29},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"added":8}],"safari":[{"added":5.1},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":6},{"prefix":"-webkit-","added":1}]}},"padding-box":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"49","added":1,"removed":50}],"firefox_android":[{"version_last":"49","added":4,"removed":50}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"box-sizing"},"break-after":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/break-after","spec_url":["https://drafts.csswg.org/css-break/#break-between","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"always":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid-column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid-page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"left":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"multicol_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-break/#break-between","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"partial_implementation":true,"added":65}],"firefox_android":[{"partial_implementation":true,"added":65}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"always":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"partial_implementation":true,"added":65}],"firefox_android":[{"partial_implementation":true,"added":65}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"avoid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":102}],"chrome_android":[{"added":102}],"edge":[{"added":102}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"avoid-column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":102}],"chrome_android":[{"added":102}],"edge":[{"added":102},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"paged_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-break/#break-between","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"always":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"avoid-page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}}},"page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}}},"recto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"impl_url":"https://crbug.com/538475","added":false}],"chrome_android":[{"impl_url":"https://crbug.com/538475","added":false}],"edge":[{"impl_url":"https://crbug.com/538475","added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"recto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"right":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"verso":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}}},"break-before":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/break-before","spec_url":["https://drafts.csswg.org/css-break/#break-between","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"always":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid-column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid-page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"left":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"multicol_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-break/#break-between","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"partial_implementation":true,"added":65}],"firefox_android":[{"partial_implementation":true,"added":65}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"always":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"partial_implementation":true,"added":65}],"firefox_android":[{"partial_implementation":true,"added":65}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"avoid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":102}],"chrome_android":[{"added":102}],"edge":[{"added":102}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"avoid-column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":102}],"chrome_android":[{"added":102}],"edge":[{"added":102},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":51}],"chrome_android":[{"added":51}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"paged_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-break/#break-between","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"always":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}}},"recto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"impl_url":"https://crbug.com/538475","added":false}],"chrome_android":[{"impl_url":"https://crbug.com/538475","added":false}],"edge":[{"impl_url":"https://crbug.com/538475","added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"recto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"right":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"verso":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}}},"break-inside":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/break-inside","spec_url":["https://drafts.csswg.org/css-break/#break-within","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid-column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":92}],"firefox_android":[{"added":92}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"avoid-page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":92}],"firefox_android":[{"added":92}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"multicol_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-break/#break-within","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"avoid-column":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":92}],"firefox_android":[{"added":92}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}}}},"paged_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-break/#break-within","https://drafts.csswg.org/css-regions/#region-flow-break","https://drafts.csswg.org/css-multicol/#break-before-break-after-break-inside"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":10}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"avoid-page":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":51}],"chrome_android":[{"added":51}],"edge":[{"added":12}],"firefox":[{"added":92}],"firefox_android":[{"added":92}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}}},"caption-side":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/caption-side","spec_url":["https://drafts.csswg.org/css2/#propdef-caption-side","https://drafts.csswg.org/css-logical/#caption-side"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"non_standard_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"86","added":1,"removed":87}],"firefox_android":[{"version_last":"86","added":4,"removed":87}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"writing-mode_relative_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":42}],"firefox_android":[{"added":42}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"caret-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/caret-color","spec_url":"https://drafts.csswg.org/css-ui/#caret-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":11.1}],"safari_ios":[{"added":11.3}]}}},"clear":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/clear","spec_url":["https://drafts.csswg.org/css2/#propdef-clear","https://drafts.csswg.org/css-logical/#float-clear"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"flow_relative_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":118}],"chrome_android":[{"added":118}],"edge":[{"added":118}],"firefox":[{"added":55}],"firefox_android":[{"added":55}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}}},"clip":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/clip","spec_url":"https://drafts.fxtf.org/css-masking/#clip-property","status":{"deprecated":true,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"clip-path":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/clip-path","spec_url":["https://drafts.fxtf.org/css-masking/#the-clip-path","https://drafts.csswg.org/css-shapes/#supported-basic-shapes"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55},{"prefix":"-webkit-","added":23}],"chrome_android":[{"added":55},{"prefix":"-webkit-","added":25}],"edge":[{"added":79},{"partial_implementation":true,"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"partial_implementation":true,"added":10}],"safari":[{"added":9.1},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9.3},{"prefix":"-webkit-","added":7}]}},"animations":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55}],"chrome_android":[{"added":55}],"edge":[{"added":79}],"firefox":[{"added":49}],"firefox_android":[{"added":49}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"basic_shape":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":23}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":54}],"firefox_android":[{"added":54}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"fill_and_stroke_box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":119}],"chrome_android":[{"added":119}],"edge":[{"added":119}],"firefox":[{"added":51}],"firefox_android":[{"added":51}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"html":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":23}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"path":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":88}],"chrome_android":[{"added":88}],"edge":[{"added":88}],"firefox":[{"added":71}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1},{"prefix":"-webkit-","added":10}],"safari_ios":[{"added":13},{"prefix":"-webkit-","added":10}]}},"_aliasOf":"path"},"svg":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":23}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":10}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"_aliasOf":"clip-path","-webkit-path":{"_aliasOf":"path"}},"clip-rule":{"__compat":{"spec_url":"https://drafts.fxtf.org/css-masking-1/#the-clip-rule","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/color","spec_url":"https://drafts.csswg.org/css-color/#the-color-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"color-adjust":{"_aliasOf":"print-color-adjust"},"color-interpolation":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/painting.html#ColorInterpolation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"color-interpolation-filters":{"__compat":{"spec_url":"https://drafts.fxtf.org/filter-effects-1/#ColorInterpolationFiltersProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"color-scheme":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/color-scheme","spec_url":"https://drafts.csswg.org/css-color-adjust/#color-scheme-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":81}],"chrome_android":[{"added":81}],"edge":[{"added":81}],"firefox":[{"added":96}],"firefox_android":[{"added":96}],"ie":[{"added":false}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}},"only_dark":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":98},{"version_last":"84","added":81,"removed":85}],"chrome_android":[{"added":98},{"version_last":"84","added":81,"removed":85}],"edge":[{"added":98},{"version_last":"84","added":81,"removed":85}],"firefox":[{"added":96}],"firefox_android":[{"added":96}],"ie":[{"added":false}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}}},"only_light":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":98},{"version_last":"84","added":81,"removed":85}],"chrome_android":[{"added":98},{"version_last":"84","added":81,"removed":85}],"edge":[{"added":98},{"version_last":"84","added":81,"removed":85}],"firefox":[{"added":96}],"firefox_android":[{"added":96}],"ie":[{"added":false}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}}}},"column-count":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-count","spec_url":"https://drafts.csswg.org/css-multicol/#cc","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":1.5,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"column-count"},"column-fill":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-fill","spec_url":"https://drafts.csswg.org/css-multicol/#cf","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":13,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":14}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":8}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":8}]}},"balance-all":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"impl_url":"https://crbug.com/909596","added":false}],"chrome_android":[{"impl_url":"https://crbug.com/909596","added":false}],"edge":[{"impl_url":"https://crbug.com/909596","added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"column-fill"},"column-gap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-gap","spec_url":["https://drafts.csswg.org/css-align/#column-row-gap","https://drafts.csswg.org/css-grid/#gutters","https://drafts.csswg.org/css-multicol/#column-gap"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":4}],"ie":[{"added":10}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}},"flex_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#column-row-gap","https://drafts.csswg.org/css-grid/#gutters","https://drafts.csswg.org/css-multicol/#column-gap"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":84}],"chrome_android":[{"added":84}],"edge":[{"added":84}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]},"tags":["web-features:flexbox-gap"]}},"grid_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#column-row-gap","https://drafts.csswg.org/css-grid/#gutters","https://drafts.csswg.org/css-multicol/#column-gap"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66},{"alternative_name":"grid-column-gap","added":57}],"chrome_android":[{"added":66},{"alternative_name":"grid-column-gap","added":57}],"edge":[{"added":16},{"alternative_name":"grid-column-gap","added":16}],"firefox":[{"added":61},{"alternative_name":"grid-column-gap","added":52}],"firefox_android":[{"added":61},{"alternative_name":"grid-column-gap","added":52}],"ie":[{"added":false}],"safari":[{"added":12},{"alternative_name":"grid-column-gap","added":10.1}],"safari_ios":[{"added":12},{"alternative_name":"grid-column-gap","added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid_context"},"multicol_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#column-row-gap","https://drafts.csswg.org/css-grid/#gutters","https://drafts.csswg.org/css-multicol/#column-gap"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":1.5,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":10},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":10},{"prefix":"-webkit-","added":3}]}},"calc_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66}],"chrome_android":[{"added":66}],"edge":[{"added":16}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"percentage_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66}],"chrome_android":[{"added":66}],"edge":[{"added":16}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"multicol_context"},"grid-column-gap":{"_aliasOf":"grid_context"},"-webkit-multicol_context":{"_aliasOf":"multicol_context"},"-moz-multicol_context":{"_aliasOf":"multicol_context"}},"column-rule":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-rule","spec_url":"https://drafts.csswg.org/css-multicol/#column-rule","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":3.5,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"column-rule"},"column-rule-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-rule-color","spec_url":"https://drafts.csswg.org/css-multicol/#crc","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":3.5,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"column-rule-color"},"column-rule-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-rule-style","spec_url":"https://drafts.csswg.org/css-multicol/#crs","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":3.5,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"column-rule-style"},"column-rule-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-rule-width","spec_url":"https://drafts.csswg.org/css-multicol/#crw","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":3.5,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"column-rule-width"},"column-span":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-span","spec_url":"https://drafts.csswg.org/css-multicol/#column-span","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":6}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":71}],"firefox_android":[{"added":79}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":5.1}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":5}]}},"_aliasOf":"column-span"},"column-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/column-width","spec_url":["https://drafts.csswg.org/css-sizing/#column-sizing","https://drafts.csswg.org/css-multicol/#cw"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":50},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":50},{"prefix":"-moz-","version_last":"73","added":1.5,"removed":74}],"firefox_android":[{"added":50},{"prefix":"-moz-","added":4}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"column-width"},"columns":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/columns","spec_url":"https://drafts.csswg.org/css-multicol/#columns","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":50},{"prefix":"-webkit-","added":50}],"chrome_android":[{"added":50}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":52},{"prefix":"-moz-","version_last":"73","added":9,"removed":74}],"firefox_android":[{"added":52},{"prefix":"-moz-","added":22}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"_aliasOf":"columns"},"contain":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain","spec_url":"https://drafts.csswg.org/css-contain/#contain-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":79}],"firefox":[{"added":69}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]},"tags":["web-features:container-queries"]},"inline-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain#inline-size","spec_url":"https://drafts.csswg.org/css-contain-3/#valdef-contain-inline-size","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":105}],"chrome_android":[{"added":105}],"edge":[{"added":105}],"firefox":[{"added":101}],"firefox_android":[{"added":101}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]},"tags":["web-features:container-queries"]}},"style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain#style","spec_url":"https://drafts.csswg.org/css-contain/#valdef-contain-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":79}],"firefox":[{"added":103}],"firefox_android":[{"added":103}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]},"tags":["web-features:container-queries"]}}},"contain-intrinsic-block-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-contain-intrinsic-block-size","spec_url":"https://drafts.csswg.org/css-sizing-4/#propdef-contain-intrinsic-block-size","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":95}],"chrome_android":[{"added":95}],"edge":[{"added":95}],"firefox":[{"added":107}],"firefox_android":[{"added":107}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"contain-intrinsic-height":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-height","spec_url":"https://drafts.csswg.org/css-sizing-4/#propdef-contain-intrinsic-height","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":95}],"chrome_android":[{"added":95}],"edge":[{"added":95}],"firefox":[{"added":107}],"firefox_android":[{"added":107}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"contain-intrinsic-inline-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-contain-intrinsic-inline-size","spec_url":"https://drafts.csswg.org/css-sizing-4/#propdef-contain-intrinsic-inline-size","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":95}],"chrome_android":[{"added":95}],"edge":[{"added":95}],"firefox":[{"added":107}],"firefox_android":[{"added":107}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"contain-intrinsic-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-size","spec_url":"https://drafts.csswg.org/css-sizing-4/#propdef-contain-intrinsic-size","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":83}],"chrome_android":[{"added":83}],"edge":[{"added":83}],"firefox":[{"added":107}],"firefox_android":[{"added":107}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}},"auto_none":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":117}],"firefox_android":[{"added":117}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}}},"contain-intrinsic-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-width","spec_url":"https://drafts.csswg.org/css-sizing-4/#propdef-contain-intrinsic-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":95}],"chrome_android":[{"added":95}],"edge":[{"added":95}],"firefox":[{"added":107}],"firefox_android":[{"added":107}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"container":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/container","spec_url":"https://drafts.csswg.org/css-contain-3/#container-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":105}],"chrome_android":[{"added":105}],"edge":[{"added":105}],"firefox":[{"added":110}],"firefox_android":[{"added":110}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:container-queries"]}},"container-name":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/container-name","spec_url":"https://drafts.csswg.org/css-contain-3/#container-name","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":105}],"chrome_android":[{"added":105}],"edge":[{"added":105}],"firefox":[{"added":110}],"firefox_android":[{"added":110}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:container-queries"]}},"container-type":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/container-type","spec_url":"https://drafts.csswg.org/css-contain-3/#container-type","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":105}],"chrome_android":[{"added":105}],"edge":[{"added":105}],"firefox":[{"added":110}],"firefox_android":[{"added":110}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:container-queries"]}},"content":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/content","spec_url":"https://drafts.csswg.org/css-content/#content-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"alt_text":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":77}],"chrome_android":[{"added":77}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"element_replacement":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":28}],"chrome_android":[{"added":28}],"edge":[{"added":79}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}}},"gradient":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/gradient","spec_url":"https://drafts.csswg.org/css-images-4/#gradients","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26}],"chrome_android":[{"added":26}],"edge":[{"added":12}],"firefox":[{"partial_implementation":true,"added":113}],"firefox_android":[{"partial_implementation":true,"added":113}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"image-set":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/image/image-set","spec_url":"https://drafts.csswg.org/css-images-4/#image-set-notation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":113}],"chrome_android":[{"added":113}],"edge":[{"added":113}],"firefox":[{"partial_implementation":true,"added":113}],"firefox_android":[{"partial_implementation":true,"added":113}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"none_applies_to_elements":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.element-content-none.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"url":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/url()","spec_url":"https://drafts.csswg.org/css-values/#urls","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"content-visibility":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/content-visibility","spec_url":"https://drafts.csswg.org/css-contain/#content-visibility","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":85}],"chrome_android":[{"added":85}],"edge":[{"added":85}],"firefox":[{"added":null},{"flags":[{"name":"layout.css.content-visibility.enabled","type":"preference","value_to_set":"true"}],"added":109}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]},"tags":["web-features:content-visibility"]},"keyframe_animatable":{"__compat":{"spec_url":"https://drafts.csswg.org/css-contain-3/#content-visibility-animation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"transitionable":{"__compat":{"spec_url":"https://drafts.csswg.org/css-display-4/#display-animation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"counter-increment":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/counter-increment","spec_url":"https://drafts.csswg.org/css-lists/#increment-set","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":25}],"ie":[{"added":8}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"counter-reset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/counter-reset","spec_url":"https://drafts.csswg.org/css-lists/#counter-reset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":25}],"ie":[{"added":8}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}},"reset_does_not_affect_siblings":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":82}],"firefox_android":[{"added":82}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"reversed":{"__compat":{"spec_url":"https://drafts.csswg.org/css-lists/#css-counter-reversed","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":96}],"firefox_android":[{"added":96}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"counter-set":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/counter-set","spec_url":"https://drafts.csswg.org/css-lists/#propdef-counter-set","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":85}],"chrome_android":[{"added":85}],"edge":[{"added":85}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":17.2}],"safari_ios":[{"added":17.2}]}}},"cursor":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/cursor","spec_url":"https://drafts.csswg.org/css-ui/#cursor","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}},"alias":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":10}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"all-scroll":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"bidirectional_resize":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":10}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"cell":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":10}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"col-resize":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"context-menu":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":10}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"copy":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":10}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"crosshair":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"default":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"grab":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":68},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":68},{"prefix":"-webkit-","added":18}],"edge":[{"added":14}],"firefox":[{"added":27},{"prefix":"-moz-","added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":1}]}},"_aliasOf":"grab"},"help":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"inherit":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":8}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"move":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"no-drop":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"none":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":5}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":95}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"not-allowed":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"pointer":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"progress":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"row-resize":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"text":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"unidirectional_resize":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"url":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":6}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"url_positioning_syntax":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"vertical-text":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":95}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"wait":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":95}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"zoom":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":37},{"prefix":"-webkit-","added":18}],"edge":[{"added":12}],"firefox":[{"added":24},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":95}],"ie":[{"added":false}],"safari":[{"added":9},{"prefix":"-webkit-","added":3}],"safari_ios":[{"added":1}]}},"_aliasOf":"zoom"},"-webkit-grab":{"_aliasOf":"grab"},"-moz-grab":{"_aliasOf":"grab"},"-webkit-zoom":{"_aliasOf":"zoom"},"-moz-zoom":{"_aliasOf":"zoom"}},"custom-property":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/--*","spec_url":"https://drafts.csswg.org/css-variables/#defining-variables","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":49}],"chrome_android":[{"added":49}],"edge":[{"added":15}],"firefox":[{"added":31}],"firefox_android":[{"added":31}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]},"tags":["web-features:custom-properties"]},"env":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://drafts.csswg.org/css-env/#env-function","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":11.1},{"alternative_name":"constant","version_last":"11","added":11,"removed":11.1}],"safari_ios":[{"added":11.3},{"alternative_name":"constant","version_last":"11","added":11,"removed":11.3}]}},"safe-area-inset-bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://drafts.csswg.org/css-env/#safe-area-insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"safe-area-inset-left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://drafts.csswg.org/css-env/#safe-area-insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"safe-area-inset-right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://drafts.csswg.org/css-env/#safe-area-insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"safe-area-inset-top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://drafts.csswg.org/css-env/#safe-area-insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"titlebar-area-height":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://wicg.github.io/window-controls-overlay/#title-bar-area-env-variables","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"chrome_android":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"edge":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"titlebar-area-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://wicg.github.io/window-controls-overlay/#title-bar-area-env-variables","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"chrome_android":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"edge":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"titlebar-area-x":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://wicg.github.io/window-controls-overlay/#title-bar-area-env-variables","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"chrome_android":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"edge":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"titlebar-area-y":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/env()","spec_url":"https://wicg.github.io/window-controls-overlay/#title-bar-area-env-variables","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"chrome_android":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"edge":[{"added":93},{"partial_implementation":true,"version_last":"92","added":92,"removed":93}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"env"},"var":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/var()","spec_url":"https://drafts.csswg.org/css-variables/#using-variables","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":49}],"chrome_android":[{"added":49}],"edge":[{"added":15}],"firefox":[{"added":31}],"firefox_android":[{"added":31}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]},"tags":["web-features:custom-properties"]}},"constant":{"_aliasOf":"env"}},"cx":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#CX","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"cy":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#CY","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"d":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/paths.html#TheDProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":97}],"firefox_android":[{"added":97}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"direction":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/direction","spec_url":"https://drafts.csswg.org/css-writing-modes/#direction","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"display":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/display","spec_url":"https://drafts.csswg.org/css-display/#the-display-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"contents":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":65}],"chrome_android":[{"added":65}],"edge":[{"added":79}],"firefox":[{"added":37}],"firefox_android":[{"added":37}],"ie":[{"added":false}],"safari":[{"added":11.1}],"safari_ios":[{"added":11.3}]}},"contents_unusual":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":65}],"chrome_android":[{"added":65}],"edge":[{"added":79}],"firefox":[{"added":59}],"firefox_android":[{"added":59}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"display-outside":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/display-outside","spec_url":"https://drafts.csswg.org/css-display/#typedef-display-outside","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"flex":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"partial_implementation":true,"added":11},{"alternative_name":"-ms-flexbox","partial_implementation":true,"added":8}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"flex"},"flow-root":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":58}],"chrome_android":[{"added":58}],"edge":[{"added":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}}},"grid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16},{"prefix":"-ms-","added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"partial_implementation":true,"prefix":"-ms-","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid"},"inline-block":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8},{"partial_implementation":true,"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"inline-flex":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11},{"alternative_name":"-ms-inline-flexbox","added":8}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"inline-flex"},"inline-grid":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16},{"prefix":"-ms-","added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"partial_implementation":true,"prefix":"-ms-","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"inline-grid"},"inline-table":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"keyframe_animatable":{"__compat":{"spec_url":"https://drafts.csswg.org/css-display-4/#display-animation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"list-item":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/display-listitem","spec_url":"https://drafts.csswg.org/css-display/#typedef-display-listitem","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"legend-support":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":71}],"chrome_android":[{"added":71}],"edge":[{"added":79}],"firefox":[{"added":64}],"firefox_android":[{"added":64}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"math":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"multi-keyword_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"none":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"ruby_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":38}],"firefox_android":[{"added":38}],"ie":[{"added":7}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"table_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"transitionable":{"__compat":{"spec_url":"https://drafts.csswg.org/css-display-4/#display-animation","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-webkit-flex":{"_aliasOf":"flex"},"-ms-flexbox":{"_aliasOf":"flex"},"-ms-grid":{"_aliasOf":"grid"},"-webkit-inline-flex":{"_aliasOf":"inline-flex"},"-ms-inline-flexbox":{"_aliasOf":"inline-flex"},"-ms-inline-grid":{"_aliasOf":"inline-grid"}},"dominant-baseline":{"__compat":{"spec_url":["https://svgwg.org/svg2-draft/text.html#DominantBaselineProperty","https://drafts.csswg.org/css-inline/#dominant-baseline-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"empty-cells":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/empty-cells","spec_url":"https://drafts.csswg.org/css2/#empty-cells","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"fill":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#fill-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"fill-opacity":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#fill-opacity","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"fill-rule":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#fill-rule","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"filter":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/filter","spec_url":"https://drafts.fxtf.org/filter-effects/#FilterProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":53},{"prefix":"-webkit-","added":18}],"chrome_android":[{"added":53}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":35},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":35},{"prefix":"-webkit-","added":49}],"ie":[{"added":false}],"safari":[{"added":9.1},{"prefix":"-webkit-","added":6}],"safari_ios":[{"added":9.3},{"prefix":"-webkit-","added":6}]}},"svg":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":53}],"chrome_android":[{"added":53}],"edge":[{"added":79}],"firefox":[{"added":35}],"firefox_android":[{"added":35}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"_aliasOf":"filter"},"flex":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":20},{"prefix":"-webkit-","added":49}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"flex"},"flex-basis":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex-basis","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-basis-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":22}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":22},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":22},{"prefix":"-webkit-","added":49}],"ie":[{"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":22}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":22}],"firefox_android":[{"added":22}],"ie":[{"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"auto"},"content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":94}],"chrome_android":[{"added":94}],"edge":[{"added":94},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":94}],"chrome_android":[{"added":94}],"edge":[{"added":94}],"firefox":[{"added":94},{"prefix":"-moz-","added":22}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":22}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}},"_aliasOf":"fit-content"},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":94}],"chrome_android":[{"added":94}],"edge":[{"added":94}],"firefox":[{"added":66},{"prefix":"-moz-","added":22}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":22}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":94}],"chrome_android":[{"added":94}],"edge":[{"added":94}],"firefox":[{"added":66},{"prefix":"-moz-","added":22}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":22}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}},"_aliasOf":"min-content"},"_aliasOf":"flex-basis","-webkit-auto":{"_aliasOf":"auto"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"flex-direction":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex-direction","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-direction-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":81},{"prefix":"-webkit-","added":49},{"partial_implementation":true,"added":20}],"firefox_android":[{"added":81},{"prefix":"-webkit-","added":49},{"partial_implementation":true,"added":20}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"flex-direction"},"flex-flow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex-flow","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-flow-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12}],"firefox":[{"added":28},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":28},{"prefix":"-webkit-","added":49}],"ie":[{"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"flex-flow"},"flex-grow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex-grow","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-grow-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":22}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11},{"alternative_name":"-ms-flex-positive","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"less_than_zero_animate":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":49}],"chrome_android":[{"added":49}],"edge":[{"added":79}],"firefox":[{"added":32}],"firefox_android":[{"added":32}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"flex-grow"},"flex-shrink":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex-shrink","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-shrink-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":22}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":20},{"prefix":"-webkit-","added":49}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":8}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":8}]},"tags":["web-features:flexbox"]},"_aliasOf":"flex-shrink"},"flex-wrap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/flex-wrap","spec_url":"https://drafts.csswg.org/css-flexbox/#flex-wrap-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12}],"firefox":[{"added":28}],"firefox_android":[{"added":52}],"ie":[{"partial_implementation":true,"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"flex-wrap"},"float":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/float","spec_url":["https://drafts.csswg.org/css2/#propdef-float","https://drafts.csswg.org/css-logical/#float-clear"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"flow_relative_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":118}],"chrome_android":[{"added":118}],"edge":[{"added":118}],"firefox":[{"added":55}],"firefox_android":[{"added":55}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}}},"flood-color":{"__compat":{"spec_url":"https://drafts.fxtf.org/filter-effects-1/#FloodColorProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":5}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":true}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"flood-opacity":{"__compat":{"spec_url":"https://drafts.fxtf.org/filter-effects-1/#FloodOpacityProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":5}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":true}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"font":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font","spec_url":"https://drafts.csswg.org/css-fonts/#font-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"font_stretch_support":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":60}],"chrome_android":[{"added":60}],"edge":[{"added":79}],"firefox":[{"added":43}],"firefox_android":[{"added":43}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"system_fonts":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"font-family":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-family","spec_url":["https://drafts.csswg.org/css-fonts/#generic-font-families","https://drafts.csswg.org/css-fonts/#font-family-prop"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"math":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"system-ui":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56}],"chrome_android":[{"added":56}],"edge":[{"added":79}],"firefox":[{"added":92},{"alternative_name":"-apple-system","added":43}],"firefox_android":[{"added":92}],"ie":[{"added":false}],"safari":[{"added":11},{"alternative_name":"-apple-system","added":9}],"safari_ios":[{"added":11},{"alternative_name":"-apple-system","added":9}]}},"_aliasOf":"system-ui"},"-apple-system":{"_aliasOf":"system-ui"}},"font-feature-settings":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-feature-settings","spec_url":"https://drafts.csswg.org/css-fonts/#font-feature-settings-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48},{"prefix":"-webkit-","added":16}],"chrome_android":[{"added":48}],"edge":[{"added":15}],"firefox":[{"added":34},{"prefix":"-moz-","added":15}],"firefox_android":[{"added":34},{"prefix":"-moz-","added":15}],"ie":[{"added":10}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}},"_aliasOf":"font-feature-settings"},"font-kerning":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-kerning","spec_url":"https://drafts.csswg.org/css-fonts/#font-kerning-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":33},{"prefix":"-webkit-","version_last":"32","added":29,"removed":33}],"chrome_android":[{"added":33},{"prefix":"-webkit-","version_last":"32","added":29,"removed":33}],"edge":[{"added":79}],"firefox":[{"added":32}],"firefox_android":[{"added":32}],"ie":[{"added":false}],"safari":[{"added":9},{"prefix":"-webkit-","added":6}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":6}]}},"_aliasOf":"font-kerning"},"font-language-override":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-language-override","spec_url":"https://drafts.csswg.org/css-fonts/#font-language-override-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":34},{"prefix":"-moz-","added":4}],"firefox_android":[{"added":34},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"font-language-override"},"font-optical-sizing":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-optical-sizing","spec_url":"https://drafts.csswg.org/css-fonts/#font-optical-sizing-def","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":79}],"chrome_android":[{"added":79}],"edge":[{"added":17}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"font-palette":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-palette","spec_url":"https://drafts.csswg.org/css-fonts/#font-palette-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":101}],"chrome_android":[{"added":101}],"edge":[{"added":101}],"firefox":[{"added":107}],"firefox_android":[{"added":107}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"font-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-size","spec_url":"https://drafts.csswg.org/css-fonts/#font-size-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"math":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":117}],"firefox_android":[{"added":117}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"rem_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":31}],"chrome_android":[{"added":42}],"edge":[{"added":12}],"firefox":[{"added":31}],"firefox_android":[{"added":31}],"ie":[{"added":9}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"xxx-large":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":79}],"chrome_android":[{"added":79}],"edge":[{"added":79}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16.4}],"safari_ios":[{"added":16.4}]}}}},"font-size-adjust":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-size-adjust","spec_url":"https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":3},{"partial_implementation":true,"version_last":"2","added":1,"removed":3}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":16.4}],"safari_ios":[{"added":16.4}]}},"from-font":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":118}],"firefox_android":[{"added":118}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"two-values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":92}],"firefox_android":[{"added":92}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}}},"font-smooth":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-smooth","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"alternative_name":"-webkit-font-smoothing","added":5}],"chrome_android":[{"alternative_name":"-webkit-font-smoothing","added":18}],"edge":[{"alternative_name":"-webkit-font-smoothing","added":79}],"firefox":[{"alternative_name":"-moz-osx-font-smoothing","added":25}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"alternative_name":"-webkit-font-smoothing","added":4}],"safari_ios":[{"alternative_name":"-webkit-font-smoothing","added":3.2}]}},"_aliasOf":"font-smooth"},"font-stretch":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-stretch","spec_url":"https://drafts.csswg.org/css-fonts/#font-stretch-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":60}],"chrome_android":[{"added":60}],"edge":[{"added":12}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":9}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}},"percentage":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":62}],"chrome_android":[{"added":62}],"edge":[{"added":18}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":11.1}],"safari_ios":[{"added":11.3}]}}}},"font-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-style","spec_url":"https://drafts.csswg.org/css-fonts/#font-style-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"oblique-angle":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":62}],"chrome_android":[{"added":62}],"edge":[{"added":79}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":11.1}],"safari_ios":[{"added":11.3}]}}}},"font-synthesis":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-synthesis","spec_url":"https://drafts.csswg.org/css-fonts/#font-synthesis","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}},"position":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":118}],"firefox_android":[{"added":118}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"small-caps":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":93}],"firefox_android":[{"added":93}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"style":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":111}],"firefox_android":[{"added":111}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"weight":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":111}],"firefox_android":[{"added":111}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}}},"font-synthesis-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-position","spec_url":"https://drafts.csswg.org/css-fonts/#font-synthesis-position","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":118}],"firefox_android":[{"added":118}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"font-synthesis-small-caps":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-small-caps","spec_url":"https://drafts.csswg.org/css-fonts/#font-synthesis-small-caps","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":111}],"firefox_android":[{"added":111}],"ie":[{"added":false}],"safari":[{"added":16.4}],"safari_ios":[{"added":16.4}]}}},"font-synthesis-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-style","spec_url":"https://drafts.csswg.org/css-fonts/#font-synthesis-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":111}],"firefox_android":[{"added":111}],"ie":[{"added":false}],"safari":[{"added":16.4}],"safari_ios":[{"added":16.4}]}}},"font-synthesis-weight":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-weight","spec_url":"https://drafts.csswg.org/css-fonts/#font-synthesis-weight","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":97}],"chrome_android":[{"added":97}],"edge":[{"added":97}],"firefox":[{"added":111}],"firefox_android":[{"added":111}],"ie":[{"added":false}],"safari":[{"added":16.4}],"safari_ios":[{"added":16.4}]}}},"font-variant":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"css_fonts_shorthand":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":79}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"greek_accented_characters":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"historical-forms":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"sub":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":110}],"chrome_android":[{"added":110}],"edge":[{"added":110}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"super":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":110}],"chrome_android":[{"added":110}],"edge":[{"added":110}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"turkic_is":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":31}],"chrome_android":[{"added":31}],"edge":[{"added":12}],"firefox":[{"added":14}],"firefox_android":[{"added":14}],"ie":[{"added":4}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"uppercase_eszett":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"font-variant-alternates":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]},"tags":["web-features:font-variant-alternates"]},"annotation":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates#annotation()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]},"tags":["web-features:font-variant-alternates"]}},"character_variant":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates#character-variant()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]},"tags":["web-features:font-variant-alternates"]}},"ornaments":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates#ornaments()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]},"tags":["web-features:font-variant-alternates"]}},"styleset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates#styleset()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]},"tags":["web-features:font-variant-alternates"]}},"stylistic":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates#stylistic()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]},"tags":["web-features:font-variant-alternates"]}},"swash":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates#swash()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":16.2}],"safari_ios":[{"added":16.2}]},"tags":["web-features:font-variant-alternates"]}}},"font-variant-caps":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-caps","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-caps-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":79}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"font-variant-east-asian":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-east-asian","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-east-asian-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":63}],"chrome_android":[{"added":63}],"edge":[{"added":79}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"font-variant-emoji":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-emoji","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-emoji-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.font-variant-emoji.enabled","type":"preference","value_to_set":"true"}],"added":108}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"font-variant-ligatures":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-ligatures","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-ligatures-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":34},{"prefix":"-webkit-","added":31}],"chrome_android":[{"added":34}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9.3},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"font-variant-ligatures"},"font-variant-numeric":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-numeric","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-numeric-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":79}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"font-variant-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variant-position","spec_url":"https://drafts.csswg.org/css-fonts/#font-variant-position-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":34}],"firefox_android":[{"added":34}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"font-variation-settings":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-variation-settings","spec_url":"https://drafts.csswg.org/css-fonts/#font-variation-settings-def","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":62}],"chrome_android":[{"added":62}],"edge":[{"added":17}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"font-weight":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/font-weight","spec_url":"https://drafts.csswg.org/css-fonts/#font-weight-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"number":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":62}],"chrome_android":[{"added":62}],"edge":[{"added":17}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}}},"forced-color-adjust":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/forced-color-adjust","spec_url":"https://drafts.csswg.org/css-color-adjust/#forced-color-adjust-prop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":79},{"alternative_name":"-ms-high-contrast-adjust","added":12}],"firefox":[{"added":113}],"firefox_android":[{"added":113}],"ie":[{"alternative_name":"-ms-high-contrast-adjust","added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"forced-color-adjust"},"gap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/gap","spec_url":"https://drafts.csswg.org/css-align/#gap-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}},"flex_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#gap-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":84}],"chrome_android":[{"added":84}],"edge":[{"added":84}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]},"tags":["web-features:flexbox-gap"]}},"grid_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#gap-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66},{"alternative_name":"grid-gap","added":57}],"chrome_android":[{"added":66},{"alternative_name":"grid-gap","added":57}],"edge":[{"added":16},{"alternative_name":"grid-gap","added":16}],"firefox":[{"added":61},{"alternative_name":"grid-gap","added":52}],"firefox_android":[{"added":61},{"alternative_name":"grid-gap","added":52}],"ie":[{"added":false}],"safari":[{"added":12},{"alternative_name":"grid-gap","added":10.1}],"safari_ios":[{"added":12},{"alternative_name":"grid-gap","added":10.3}]},"tags":["web-features:grid"]},"calc_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66}],"chrome_android":[{"added":66}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]},"tags":["web-features:grid"]}},"percentage_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66}],"chrome_android":[{"added":66}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:grid"]}},"_aliasOf":"grid_context"},"multicol_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#gap-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66}],"chrome_android":[{"added":66}],"edge":[{"added":16}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"grid-gap":{"_aliasOf":"grid_context"}},"glyph-orientation-vertical":{"__compat":{"spec_url":"https://drafts.csswg.org/css-writing-modes-4/#glyph-orientation","status":{"deprecated":true,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"grid":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid","spec_url":"https://drafts.csswg.org/css-grid/#grid-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-area":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-area","spec_url":"https://drafts.csswg.org/css-grid/#propdef-grid-area","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-auto-columns":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-auto-columns","spec_url":"https://drafts.csswg.org/css-grid/#auto-tracks","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16},{"alternative_name":"-ms-grid-columns","version_last":"18","added":12,"removed":79}],"firefox":[{"added":70},{"partial_implementation":true,"version_last":"69","added":52,"removed":70}],"firefox_android":[{"added":79},{"partial_implementation":true,"version_last":"68","added":52,"removed":79}],"ie":[{"alternative_name":"-ms-grid-columns","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid-auto-columns"},"grid-auto-flow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-auto-flow","spec_url":"https://drafts.csswg.org/css-grid/#grid-auto-flow-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-auto-rows":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-auto-rows","spec_url":"https://drafts.csswg.org/css-grid/#auto-tracks","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16},{"alternative_name":"-ms-grid-rows","version_last":"18","added":12,"removed":79}],"firefox":[{"added":70},{"partial_implementation":true,"version_last":"69","added":52,"removed":70}],"firefox_android":[{"added":79},{"partial_implementation":true,"version_last":"68","added":52,"removed":79}],"ie":[{"alternative_name":"-ms-grid-rows","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid-auto-rows"},"grid-column":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-column","spec_url":"https://drafts.csswg.org/css-grid/#placement-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-column-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-column-end","spec_url":"https://drafts.csswg.org/css-grid/#line-placement","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-column-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-column-start","spec_url":"https://drafts.csswg.org/css-grid/#line-placement","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-row":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-row","spec_url":"https://drafts.csswg.org/css-grid/#placement-shorthands","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-row-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-row-end","spec_url":"https://drafts.csswg.org/css-grid/#line-placement","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-row-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-row-start","spec_url":"https://drafts.csswg.org/css-grid/#line-placement","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-template":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-template","spec_url":"https://drafts.csswg.org/css-grid/#explicit-grid-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-template-areas":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-template-areas","spec_url":"https://drafts.csswg.org/css-grid/#grid-template-areas-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"grid-template-columns":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-template-columns","spec_url":["https://drafts.csswg.org/css-grid/#track-sizing","https://drafts.csswg.org/css-grid/#subgrids","https://drafts.csswg.org/css-grid-3/#masonry-layout"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16},{"alternative_name":"-ms-grid-columns","version_last":"18","added":12,"removed":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"alternative_name":"-ms-grid-columns","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"animation":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":107}],"chrome_android":[{"added":107}],"edge":[{"added":107}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:grid-animation"]}},"fit-content":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/fit-content","spec_url":"https://drafts.csswg.org/css-sizing-4/#sizing-values","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"masonry":{"__compat":{"spec_url":"https://drafts.csswg.org/css-grid-3/#masonry-layout","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.grid-template-masonry-value.enabled","type":"preference","value_to_set":"true"}],"added":77}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]},"tags":["web-features:masonry"]}},"minmax":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/minmax()","spec_url":"https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-minmax","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"repeat":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/repeat()","spec_url":"https://drafts.csswg.org/css-grid/#repeat-notation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":76},{"partial_implementation":true,"version_last":"75","added":57,"removed":76},{"partial_implementation":true,"version_last":"56","added":52,"removed":57}],"firefox_android":[{"added":79},{"partial_implementation":true,"version_last":"68","added":57,"removed":79},{"partial_implementation":true,"version_last":"56","added":52,"removed":57}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"subgrid":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/CSS_Grid_Layout/Subgrid","spec_url":"https://drafts.csswg.org/css-grid/#subgrids","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":71}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:subgrid"]}},"_aliasOf":"grid-template-columns"},"grid-template-rows":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/grid-template-rows","spec_url":["https://drafts.csswg.org/css-grid/#track-sizing","https://drafts.csswg.org/css-grid/#subgrids","https://drafts.csswg.org/css-grid-3/#masonry-layout"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16},{"alternative_name":"-ms-grid-rows","version_last":"18","added":12,"removed":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"alternative_name":"-ms-grid-rows","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"animation":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":107}],"chrome_android":[{"added":107}],"edge":[{"added":107}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:grid-animation"]}},"fit-content":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/fit-content","spec_url":"https://drafts.csswg.org/css-sizing-4/#sizing-values","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"masonry":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/CSS_Grid_Layout/Masonry_Layout","spec_url":"https://drafts.csswg.org/css-grid-3/#masonry-layout","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.grid-template-masonry-value.enabled","type":"preference","value_to_set":"true"}],"added":77}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]},"tags":["web-features:masonry"]}},"minmax":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/minmax()","spec_url":"https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-minmax","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"repeat":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/repeat()","spec_url":"https://drafts.csswg.org/css-grid/#repeat-notation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":76},{"partial_implementation":true,"version_last":"75","added":57,"removed":76},{"partial_implementation":true,"version_last":"56","added":52,"removed":57}],"firefox_android":[{"added":79},{"partial_implementation":true,"version_last":"68","added":57,"removed":79},{"partial_implementation":true,"version_last":"56","added":52,"removed":57}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"subgrid":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/CSS_Grid_Layout/Subgrid","spec_url":"https://drafts.csswg.org/css-grid/#subgrids","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":71}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:subgrid"]}},"_aliasOf":"grid-template-rows"},"hanging-punctuation":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/hanging-punctuation","spec_url":"https://drafts.csswg.org/css-text/#hanging-punctuation-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"partial_implementation":true,"added":10}],"safari_ios":[{"partial_implementation":true,"added":10}]}}},"height":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/height","spec_url":["https://drafts.csswg.org/css-sizing/#preferred-size-properties","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":9}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}},"_aliasOf":"min-content"},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-fill-available","added":28}],"chrome_android":[{"alternative_name":"-webkit-fill-available","added":28}],"edge":[{"alternative_name":"-webkit-fill-available","added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"alternative_name":"-webkit-fill-available","added":9}],"safari_ios":[{"alternative_name":"-webkit-fill-available","added":9}]}},"_aliasOf":"stretch"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-webkit-fit-content":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"stretch"}},"hyphenate-character":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/hyphenate-character","spec_url":"https://drafts.csswg.org/css-text-4/#propdef-hyphenate-character","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":106},{"prefix":"-webkit-","added":6}],"chrome_android":[{"added":106},{"prefix":"-webkit-","added":18}],"edge":[{"added":106},{"prefix":"-webkit-","added":79}],"firefox":[{"added":98}],"firefox_android":[{"added":98}],"ie":[{"added":false}],"safari":[{"added":17},{"prefix":"-webkit-","added":5.1}],"safari_ios":[{"added":17},{"prefix":"-webkit-","added":5}]}},"_aliasOf":"hyphenate-character"},"hyphenate-limit-chars":{"__compat":{"spec_url":"https://drafts.csswg.org/css-text-4/#propdef-hyphenate-limit-chars","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"hyphens":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/hyphens","spec_url":"https://drafts.csswg.org/css-text/#hyphens-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55},{"prefix":"-webkit-","added":13}],"chrome_android":[{"added":55},{"prefix":"-webkit-","added":18}],"edge":[{"added":79},{"prefix":"-webkit-","added":79},{"partial_implementation":true,"prefix":"-ms-","version_last":"18","added":12,"removed":79}],"firefox":[{"added":43},{"prefix":"-moz-","added":6}],"firefox_android":[{"added":43},{"prefix":"-moz-","added":6}],"ie":[{"partial_implementation":true,"prefix":"-ms-","added":10}],"safari":[{"added":17},{"prefix":"-webkit-","added":5.1}],"safari_ios":[{"added":17},{"prefix":"-webkit-","added":4.2}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":88},{"partial_implementation":true,"added":55}],"chrome_android":[{"added":55}],"edge":[{"added":88},{"partial_implementation":true,"added":79}],"firefox":[{"added":6}],"firefox_android":[{"added":6}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":4.2}]}}},"language_afrikaans":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_albanian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_amharic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_armenian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_assamese":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_basque":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_belarusian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_bengali":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_bosnian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_bulgarian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_catalan":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_croatian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_cyrillic_mongolian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_czech":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_danish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_dutch":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_english":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55}],"chrome_android":[{"added":55}],"edge":[{"added":12}],"firefox":[{"added":6}],"firefox_android":[{"added":6}],"ie":[{"added":10}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_esperanto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_estonian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_ethiopic_script_mul":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_ethiopic_script_und":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_finnish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_french":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_galician":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_georgian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_german_reformed_orthography":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_german_swiss_orthography":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_german_traditional_orthography":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_gujarati":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_hindi":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_hungarian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_icelandic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_interlingua":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_irish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_italian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_kannada":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_kurmanji":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_latin":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_latvian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_lithuanian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_malayalam":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_marathi":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_modern_greek":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_mongolian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_norwegian_nn":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":5}]}}},"language_norwegian_no":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_old_slavonic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_oriya":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_polish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":31}],"firefox_android":[{"added":31}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_portuguese":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_portuguese_brazilian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_punjabi":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_russian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_slovak":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_slovenian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_spanish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_swedish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_tamil":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_telugu":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_turkish":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"language_turkmen":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_ukrainian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":112}],"chrome_android":[{"added":112}],"edge":[{"added":112}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"language_upper_sorbian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"language_welsh":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":8}],"firefox_android":[{"added":8}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"hyphens"},"image-orientation":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/image-orientation","spec_url":"https://drafts.csswg.org/css-images/#the-image-orientation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":81}],"chrome_android":[{"added":81}],"edge":[{"added":81}],"firefox":[{"added":26}],"firefox_android":[{"added":26}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}},"flip_and_angle":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"version_last":"62","added":26,"removed":63}],"firefox_android":[{"version_last":"62","added":26,"removed":63}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"image-rendering":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/image-rendering","spec_url":"https://drafts.csswg.org/css-images/#the-image-rendering","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":13}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}},"crisp-edges":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-optimize-contrast","added":13}],"chrome_android":[{"alternative_name":"-webkit-optimize-contrast","added":18}],"edge":[{"alternative_name":"-webkit-optimize-contrast","added":79}],"firefox":[{"added":65},{"prefix":"-moz-","added":3.6}],"firefox_android":[{"added":65},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":7},{"alternative_name":"-webkit-optimize-contrast","added":6}],"safari_ios":[{"added":7},{"alternative_name":"-webkit-optimize-contrast","added":6}]}},"_aliasOf":"crisp-edges"},"optimizeQuality":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"optimizeSpeed":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"pixelated":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":41}],"chrome_android":[{"added":41}],"edge":[{"added":79}],"firefox":[{"added":93}],"firefox_android":[{"added":93}],"ie":[{"added":false}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}}},"smooth":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":93}],"firefox_android":[{"added":93}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-webkit-optimize-contrast":{"_aliasOf":"crisp-edges"},"-moz-crisp-edges":{"_aliasOf":"crisp-edges"}},"ime-mode":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/ime-mode","spec_url":"https://drafts.csswg.org/css-ui/#input-method-editor","status":{"deprecated":true,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79},{"prefix":"-ms-","version_last":"18","added":12,"removed":79}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"ime-mode"},"initial-letter":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/initial-letter","spec_url":"https://drafts.csswg.org/css-inline/#sizing-drop-initials","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":110}],"chrome_android":[{"added":110}],"edge":[{"added":110}],"firefox":[{"impl_url":"https://bugzil.la/1223880","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1223880","added":false}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":9}],"safari_ios":[{"prefix":"-webkit-","added":9}]}},"_aliasOf":"initial-letter"},"initial-letter-align":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/initial-letter-align","spec_url":"https://drafts.csswg.org/css-inline/#aligning-initial-letter","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"impl_url":"https://bugzil.la/1273021","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1273021","added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"inline-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inline-size","spec_url":["https://drafts.csswg.org/css-logical/#dimension-properties","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"min-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-webkit-fill-available":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"inset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset","spec_url":"https://drafts.csswg.org/css-logical/#propdef-inset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"inset-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset-block","spec_url":"https://drafts.csswg.org/css-logical/#propdef-inset-block","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":63},{"alternative_name":"offset-block","version_last":"62","added":41,"removed":63}],"firefox_android":[{"added":63},{"alternative_name":"offset-block","version_last":"62","added":41,"removed":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"_aliasOf":"inset-block"},"inset-block-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset-block-end","spec_url":"https://drafts.csswg.org/css-logical/#position-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":63},{"alternative_name":"offset-block-end","version_last":"62","added":41,"removed":63}],"firefox_android":[{"added":63},{"alternative_name":"offset-block-end","version_last":"62","added":41,"removed":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"_aliasOf":"inset-block-end"},"inset-block-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset-block-start","spec_url":"https://drafts.csswg.org/css-logical/#position-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":63},{"alternative_name":"offset-block-start","version_last":"62","added":41,"removed":63}],"firefox_android":[{"added":63},{"alternative_name":"offset-block-start","version_last":"62","added":41,"removed":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"_aliasOf":"inset-block-start"},"inset-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset-inline","spec_url":"https://drafts.csswg.org/css-logical/#propdef-inset-inline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":63},{"alternative_name":"offset-inline","version_last":"62","added":41,"removed":63}],"firefox_android":[{"added":63},{"alternative_name":"offset-inline","version_last":"62","added":41,"removed":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"_aliasOf":"inset-inline"},"inset-inline-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset-inline-end","spec_url":"https://drafts.csswg.org/css-logical/#position-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":63},{"alternative_name":"offset-inline-end","version_last":"62","added":41,"removed":63}],"firefox_android":[{"added":63},{"alternative_name":"offset-inline-end","version_last":"62","added":41,"removed":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"_aliasOf":"inset-inline-end"},"inset-inline-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/inset-inline-start","spec_url":"https://drafts.csswg.org/css-logical/#position-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":63},{"alternative_name":"offset-inline-start","version_last":"62","added":41,"removed":63}],"firefox_android":[{"added":63},{"alternative_name":"offset-inline-start","version_last":"62","added":41,"removed":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"_aliasOf":"inset-inline-start"},"isolation":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/isolation","spec_url":"https://drafts.fxtf.org/compositing/#isolation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":41}],"chrome_android":[{"added":41}],"edge":[{"added":79}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"justify-content":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/justify-content","spec_url":["https://drafts.csswg.org/css-align/#align-justify-content","https://drafts.csswg.org/css-flexbox/#justify-content-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":20},{"prefix":"-webkit-","added":49}],"ie":[{"added":11}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]}},"flex_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-justify-content","https://drafts.csswg.org/css-flexbox/#justify-content-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52},{"partial_implementation":true,"version_last":"51","added":21,"removed":52}],"chrome_android":[{"added":52},{"partial_implementation":true,"version_last":"51","added":25,"removed":52}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11}],"safari":[{"added":7}],"safari_ios":[{"added":7}]},"tags":["web-features:flexbox"]},"left_right":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93}],"chrome_android":[{"added":93}],"edge":[{"added":93}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}}},"safe_unsafe":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"space-evenly":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":60}],"chrome_android":[{"added":60}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"start_end":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":93}],"chrome_android":[{"added":93}],"edge":[{"added":93}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]},"tags":["web-features:flexbox"]}}},"grid_context":{"__compat":{"spec_url":["https://drafts.csswg.org/css-align/#align-justify-content","https://drafts.csswg.org/css-flexbox/#justify-content-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":52}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}},"_aliasOf":"justify-content"},"justify-items":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/justify-items","spec_url":"https://drafts.csswg.org/css-align/#justify-items-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}},"flex_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#justify-items-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":12}],"firefox":[{"added":20}],"firefox_android":[{"added":20}],"ie":[{"added":11}],"safari":[{"added":9}],"safari_ios":[{"added":9}]},"tags":["web-features:flexbox"]}},"grid_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#justify-items-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]}}},"justify-self":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/justify-self","spec_url":"https://drafts.csswg.org/css-align/#justify-self-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}},"flex_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#justify-self-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:flexbox"]}},"grid_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#justify-self-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":16}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"partial_implementation":true,"prefix":"-ms-","added":10}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid_context"},"-ms-grid_context":{"_aliasOf":"grid_context"}},"justify-tracks":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/justify-tracks","spec_url":"https://drafts.csswg.org/css-grid-3/#tracks-alignment","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.grid-template-masonry-value.enabled","type":"preference","value_to_set":"true"}],"added":77}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]},"tags":["web-features:masonry"]}},"left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/left","spec_url":"https://drafts.csswg.org/css-position/#insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"letter-spacing":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/letter-spacing","spec_url":"https://drafts.csswg.org/css-text/#letter-spacing-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"svg":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":9}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}}},"lighting-color":{"__compat":{"spec_url":"https://drafts.fxtf.org/filter-effects-1/#LightingColorProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":5}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":true}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"line-break":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/line-break","spec_url":"https://drafts.csswg.org/css-text/#line-break-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":58},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":58},{"prefix":"-webkit-","added":18}],"edge":[{"added":14}],"firefox":[{"added":69}],"firefox_android":[{"added":79}],"ie":[{"added":5.5},{"prefix":"-ms-","added":8}],"safari":[{"added":11},{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":2,"removed":3}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":1}]}},"_aliasOf":"line-break"},"line-height":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/line-height","spec_url":"https://drafts.csswg.org/css-inline/#line-height-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"line-height-step":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/line-height-step","spec_url":"https://drafts.csswg.org/css-rhythm/#line-height-step","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"flags":[{"name":"--enable-blink-features=CSSSnapSize","type":"runtime_flag"}],"added":60}],"chrome_android":[{"flags":[{"name":"--enable-blink-features=CSSSnapSize","type":"runtime_flag"}],"added":60}],"edge":[{"flags":[{"name":"--enable-blink-features=CSSSnapSize","type":"runtime_flag"}],"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"list-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/list-style","spec_url":"https://drafts.csswg.org/css-lists/#list-style-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"symbols":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/symbols()","spec_url":"https://drafts.csswg.org/css-counter-styles/#symbols-function","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":35}],"firefox_android":[{"added":35}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"list-style-image":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/list-style-image","spec_url":"https://drafts.csswg.org/css-lists/#image-markers","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"list-style-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/list-style-position","spec_url":"https://drafts.csswg.org/css-lists/#list-style-position-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"list-style-type":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/list-style-type","spec_url":["https://drafts.csswg.org/css-lists/#text-markers","https://drafts.csswg.org/css-counter-styles/#extending-css2"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"afar":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"amharic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"amharic-abegede":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"arabic-indic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"arabic-indic"},"armenian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"asterisks":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":13,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"bengali":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"bengali"},"binary":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":13,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"cambodian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"circle":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"cjk-decimal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91}],"chrome_android":[{"added":91}],"edge":[{"added":91}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"cjk-earthly-branch":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"cjk-earthly-branch"},"cjk-heavenly-stem":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"cjk-heavenly-stem"},"cjk-ideographic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"decimal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"decimal-leading-zero":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"devanagari":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"devanagari"},"disc":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"disclosure-closed":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":89}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"disclosure-open":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89}],"chrome_android":[{"added":89}],"edge":[{"added":89}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"ethiopic":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-abegede":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-abegede-am-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-abegede-gez":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-abegede-ti-er":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-abegede-ti-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}},"_aliasOf":"ethiopic-halehame"},"ethiopic-halehame-aa-er":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-aa-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-am":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}},"_aliasOf":"ethiopic-halehame-am"},"ethiopic-halehame-am-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-gez":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-om-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-sid-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-so-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-halehame-ti-er":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"ethiopic-halehame-ti-er"},"ethiopic-halehame-ti-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"ethiopic-halehame-ti-et"},"ethiopic-halehame-tig":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"ethiopic-numeric":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91}],"chrome_android":[{"added":91}],"edge":[{"added":91}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"ethiopic-numeric"},"footnotes":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":13,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"georgian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"gujarati":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"gujarati"},"gurmukhi":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"gurmukhi"},"hangul":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"hangul"},"hangul-consonant":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":1}],"firefox_android":[{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"hangul-consonant"},"hebrew":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"hiragana":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"hiragana-iroha":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"japanese-formal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91}],"chrome_android":[{"added":91}],"edge":[{"added":91}],"firefox":[{"added":28},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":28},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"japanese-formal"},"japanese-informal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91}],"chrome_android":[{"added":91}],"edge":[{"added":91}],"firefox":[{"added":28},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":28},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"japanese-informal"},"kannada":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"kannada"},"katakana":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"katakana-iroha":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"khmer":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"khmer"},"korean-hangul-formal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"korean-hanja-formal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"korean-hanja-informal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28}],"firefox_android":[{"added":28}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"lao":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"lao"},"lower-alpha":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"lower-armenian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":13}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"lower-greek":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"lower-hexadecimal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"lower-latin":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"lower-norwegian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"lower-roman":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"malayalam":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"malayalam"},"mongolian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"myanmar":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"myanmar"},"octal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"oriya":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"oriya"},"oromo":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"persian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"persian"},"sidama":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"simp-chinese-formal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":28},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"simp-chinese-formal"},"simp-chinese-informal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":28},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"simp-chinese-informal"},"somali":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"square":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"string":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":79}],"chrome_android":[{"added":79}],"edge":[{"added":79}],"firefox":[{"added":39}],"firefox_android":[{"added":39}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"symbols":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/symbols()","spec_url":"https://drafts.csswg.org/css-counter-styles/#symbols-function","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":35}],"firefox_android":[{"added":35}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"tamil":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91}],"chrome_android":[{"added":91}],"edge":[{"added":91}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"tamil"},"telugu":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"telugu"},"thai":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":33},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"thai"},"tibetan":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"tigre":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"tigrinya-er":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"tigrinya-er-abegede":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"tigrinya-et":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"tigrinya-et-abegede":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"trad-chinese-formal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":28},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"trad-chinese-formal"},"trad-chinese-informal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":45}],"chrome_android":[{"added":45}],"edge":[{"added":79}],"firefox":[{"added":28},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":28},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}},"_aliasOf":"trad-chinese-informal"},"upper-alpha":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"upper-armenian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":13}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":33}],"firefox_android":[{"added":33}],"ie":[{"added":false}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}},"upper-greek":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":1,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"upper-hexadecimal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"upper-latin":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"upper-norwegian":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":91},{"version_last":"44","added":6,"removed":45}],"chrome_android":[{"added":91},{"version_last":"44","added":18,"removed":45}],"edge":[{"added":91}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"upper-roman":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"urdu":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":6}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"prefix":"-moz-","added":33}],"firefox_android":[{"prefix":"-moz-","added":33}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"_aliasOf":"urdu"},"-moz-arabic-indic":{"_aliasOf":"arabic-indic"},"-moz-bengali":{"_aliasOf":"bengali"},"-moz-cjk-earthly-branch":{"_aliasOf":"cjk-earthly-branch"},"-moz-cjk-heavenly-stem":{"_aliasOf":"cjk-heavenly-stem"},"-moz-devanagari":{"_aliasOf":"devanagari"},"-moz-ethiopic-halehame":{"_aliasOf":"ethiopic-halehame"},"-moz-ethiopic-halehame-am":{"_aliasOf":"ethiopic-halehame-am"},"-moz-ethiopic-halehame-ti-er":{"_aliasOf":"ethiopic-halehame-ti-er"},"-moz-ethiopic-halehame-ti-et":{"_aliasOf":"ethiopic-halehame-ti-et"},"-moz-ethiopic-numeric":{"_aliasOf":"ethiopic-numeric"},"-moz-gujarati":{"_aliasOf":"gujarati"},"-moz-gurmukhi":{"_aliasOf":"gurmukhi"},"-moz-hangul":{"_aliasOf":"hangul"},"-moz-hangul-consonant":{"_aliasOf":"hangul-consonant"},"-moz-japanese-formal":{"_aliasOf":"japanese-formal"},"-moz-japanese-informal":{"_aliasOf":"japanese-informal"},"-moz-kannada":{"_aliasOf":"kannada"},"-moz-khmer":{"_aliasOf":"khmer"},"-moz-lao":{"_aliasOf":"lao"},"-moz-malayalam":{"_aliasOf":"malayalam"},"-moz-myanmar":{"_aliasOf":"myanmar"},"-moz-oriya":{"_aliasOf":"oriya"},"-moz-persian":{"_aliasOf":"persian"},"-moz-simp-chinese-formal":{"_aliasOf":"simp-chinese-formal"},"-moz-simp-chinese-informal":{"_aliasOf":"simp-chinese-informal"},"-moz-tamil":{"_aliasOf":"tamil"},"-moz-telugu":{"_aliasOf":"telugu"},"-moz-thai":{"_aliasOf":"thai"},"-moz-trad-chinese-formal":{"_aliasOf":"trad-chinese-formal"},"-moz-trad-chinese-informal":{"_aliasOf":"trad-chinese-informal"},"-moz-urdu":{"_aliasOf":"urdu"}},"margin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin","spec_url":"https://drafts.csswg.org/css-box/#margin","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"margin-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-block","spec_url":"https://drafts.csswg.org/css-logical/#propdef-margin-block","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"margin-block-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-block-end","spec_url":"https://drafts.csswg.org/css-logical/#margin-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"margin-block-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-block-start","spec_url":"https://drafts.csswg.org/css-logical/#margin-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"margin-bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-bottom","spec_url":"https://drafts.csswg.org/css-box/#margin-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"margin-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-inline","spec_url":"https://drafts.csswg.org/css-logical/#propdef-margin-inline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"margin-inline-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-inline-end","spec_url":"https://drafts.csswg.org/css-logical/#margin-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69},{"alternative_name":"-webkit-margin-end","added":2}],"chrome_android":[{"added":69},{"alternative_name":"-webkit-margin-end","added":18}],"edge":[{"added":79},{"alternative_name":"-webkit-margin-end","added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-margin-end","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-margin-end","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1},{"alternative_name":"-webkit-margin-end","added":3}],"safari_ios":[{"added":12.2},{"alternative_name":"-webkit-margin-end","added":3}]}},"_aliasOf":"margin-inline-end"},"margin-inline-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-inline-start","spec_url":"https://drafts.csswg.org/css-logical/#margin-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69},{"alternative_name":"-webkit-margin-start","added":2}],"chrome_android":[{"added":69},{"alternative_name":"-webkit-margin-start","added":18}],"edge":[{"added":79},{"alternative_name":"-webkit-margin-start","added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-margin-start","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-margin-start","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1},{"alternative_name":"-webkit-margin-start","added":3}],"safari_ios":[{"added":12.2},{"alternative_name":"-webkit-margin-start","added":3}]}},"_aliasOf":"margin-inline-start"},"margin-left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-left","spec_url":"https://drafts.csswg.org/css-box/#margin-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"margin-right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-right","spec_url":"https://drafts.csswg.org/css-box/#margin-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"margin-top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-top","spec_url":"https://drafts.csswg.org/css-box/#margin-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"margin-trim":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/margin-trim","spec_url":"https://drafts.csswg.org/css-box-4/#margin-trim","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"impl_url":"https://bugzil.la/1506241","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1506241","added":false}],"ie":[{"added":false}],"safari":[{"added":16.4}],"safari_ios":[{"added":16.4}]}}},"marker":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/painting.html#MarkerShorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"marker-end":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"marker-mid":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"marker-start":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"mask":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"prefix":"-webkit-","added":1},{"partial_implementation":true,"added":1}],"chrome_android":[{"prefix":"-webkit-","added":18},{"partial_implementation":true,"added":18}],"edge":[{"prefix":"-webkit-","added":79},{"partial_implementation":true,"added":79},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":53},{"partial_implementation":true,"version_last":"52","added":2,"removed":53}],"firefox_android":[{"added":53},{"partial_implementation":true,"version_last":"52","added":4,"removed":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":3.1},{"partial_implementation":true,"version_last":"15.3","added":3.1,"removed":15.4}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":2},{"partial_implementation":true,"version_last":"15.3","added":2,"removed":15.4}]}},"_aliasOf":"mask"},"mask-border":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-border","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-border","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-mask-box-image","added":1}],"chrome_android":[{"alternative_name":"-webkit-mask-box-image","added":18}],"edge":[{"alternative_name":"-webkit-mask-box-image","added":79}],"firefox":[{"impl_url":"https://bugzil.la/877294","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/877294","added":false}],"ie":[{"added":false}],"safari":[{"added":17.2},{"alternative_name":"-webkit-mask-box-image","added":3.1}],"safari_ios":[{"alternative_name":"-webkit-mask-box-image","added":3}]}},"_aliasOf":"mask-border"},"mask-border-outset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-border-outset","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-border-outset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-mask-box-image-outset","added":1}],"chrome_android":[{"alternative_name":"-webkit-mask-box-image-outset","added":18}],"edge":[{"alternative_name":"-webkit-mask-box-image-outset","added":79}],"firefox":[{"impl_url":"https://bugzil.la/877294","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/877294","added":false}],"ie":[{"added":false}],"safari":[{"added":17.2},{"alternative_name":"-webkit-mask-box-image-outset","added":3.1}],"safari_ios":[{"alternative_name":"-webkit-mask-box-image-outset","added":3}]}},"_aliasOf":"mask-border-outset"},"mask-border-repeat":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-border-repeat","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-border-repeat","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-mask-box-image-repeat","added":1}],"chrome_android":[{"alternative_name":"-webkit-mask-box-image-repeat","added":18}],"edge":[{"alternative_name":"-webkit-mask-box-image-repeat","added":79}],"firefox":[{"impl_url":"https://bugzil.la/877294","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/877294","added":false}],"ie":[{"added":false}],"safari":[{"added":17.2},{"alternative_name":"-webkit-mask-box-image-repeat","added":3.1}],"safari_ios":[{"alternative_name":"-webkit-mask-box-image-repeat","added":3}]}},"_aliasOf":"mask-border-repeat"},"mask-border-slice":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-border-slice","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-border-slice","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-mask-box-image-slice","added":1}],"chrome_android":[{"alternative_name":"-webkit-mask-box-image-slice","added":18}],"edge":[{"alternative_name":"-webkit-mask-box-image-slice","added":79}],"firefox":[{"impl_url":"https://bugzil.la/877294","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/877294","added":false}],"ie":[{"added":false}],"safari":[{"added":17.2},{"alternative_name":"-webkit-mask-box-image-slice","added":3.1}],"safari_ios":[{"alternative_name":"-webkit-mask-box-image-slice","added":3}]}},"_aliasOf":"mask-border-slice"},"mask-border-source":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-border-source","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-border-source","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-mask-box-image-source","added":1}],"chrome_android":[{"alternative_name":"-webkit-mask-box-image-source","added":18}],"edge":[{"alternative_name":"-webkit-mask-box-image-source","added":79}],"firefox":[{"impl_url":"https://bugzil.la/877294","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/877294","added":false}],"ie":[{"added":false}],"safari":[{"added":17.2},{"alternative_name":"-webkit-mask-box-image-source","added":3.1}],"safari_ios":[{"alternative_name":"-webkit-mask-box-image-source","added":3}]}},"_aliasOf":"mask-border-source"},"mask-border-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-border-width","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-border-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-mask-box-image-width","added":1}],"chrome_android":[{"alternative_name":"-webkit-mask-box-image-width","added":18}],"edge":[{"alternative_name":"-webkit-mask-box-image-width","added":79}],"firefox":[{"impl_url":"https://bugzil.la/877294","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/877294","added":false}],"ie":[{"added":false}],"safari":[{"added":17.2},{"alternative_name":"-webkit-mask-box-image-width","added":3.1}],"safari_ios":[{"alternative_name":"-webkit-mask-box-image-width","added":3}]}},"_aliasOf":"mask-border-width"},"mask-clip":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-clip","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-clip","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":120},{"prefix":"-webkit-","added":18}],"edge":[{"added":120},{"prefix":"-webkit-","added":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":3.2}]}},"border":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"padding":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"text":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"_aliasOf":"mask-clip"},"mask-composite":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-composite","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-composite","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120}],"chrome_android":[{"added":120}],"edge":[{"version_last":"18","added":18,"removed":79}],"firefox":[{"added":53},{"prefix":"-webkit-","added":53}],"firefox_android":[{"added":53},{"prefix":"-webkit-","added":53}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}},"_aliasOf":"mask-composite"},"mask-image":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-image","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-image","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":120},{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79},{"version_last":"18","added":16,"removed":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":3.2}]}},"multiple_mask_images":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":18}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"svg_masks":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":8}],"chrome_android":[{"added":18}],"edge":[{"added":18}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"_aliasOf":"mask-image"},"mask-mode":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-mode","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-mode","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120}],"chrome_android":[{"added":120}],"edge":[{"added":120}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"mask-origin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-origin","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-origin","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":120},{"prefix":"-webkit-","added":18}],"edge":[{"added":120},{"prefix":"-webkit-","added":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":3.2}]}},"fill-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120}],"chrome_android":[{"added":120}],"edge":[{"added":120}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"impl_url":"https://webkit.org/b/137293","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/137293","added":false}]}}},"non_standard_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":3.2}]}},"_aliasOf":"non_standard_values"},"stroke-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120}],"chrome_android":[{"added":120}],"edge":[{"added":120}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"impl_url":"https://webkit.org/b/137293","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/137293","added":false}]}}},"view-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120}],"chrome_android":[{"added":120}],"edge":[{"added":120}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"impl_url":"https://webkit.org/b/137293","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/137293","added":false}]}}},"_aliasOf":"mask-origin","-webkit-non_standard_values":{"_aliasOf":"non_standard_values"}},"mask-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-position","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-position","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":120},{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79},{"version_last":"18","added":18,"removed":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"mask-position"},"mask-repeat":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-repeat","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-repeat","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":120},{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79},{"version_last":"18","added":18,"removed":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"mask-repeat"},"mask-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-size","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-size","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":120},{"prefix":"-webkit-","added":4}],"chrome_android":[{"added":120},{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79},{"version_last":"18","added":18,"removed":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"mask-size"},"mask-type":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mask-type","spec_url":"https://drafts.fxtf.org/css-masking/#the-mask-type","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":24}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":35}],"firefox_android":[{"added":35}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"masonry-auto-flow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/masonry-auto-flow","spec_url":"https://drafts.csswg.org/css-grid-3/#masonry-auto-flow","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]},"tags":["web-features:masonry"]}},"math-depth":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/math-depth","spec_url":"https://w3c.github.io/mathml-core/#the-math-script-level-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":117}],"firefox_android":[{"added":117}],"ie":[{"added":false}],"safari":[{"impl_url":"https://webkit.org/b/202303","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/202303","added":false}]}}},"math-shift":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/math-shift","spec_url":"https://w3c.github.io/mathml-core/#the-math-shift","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"math-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/math-style","spec_url":"https://w3c.github.io/mathml-core/#the-math-style-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":117}],"firefox_android":[{"added":117}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"max-block-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/max-block-size","spec_url":["https://drafts.csswg.org/css-logical/#propdef-max-block-size","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":94}],"firefox_android":[{"added":94}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"max-height":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/max-height","spec_url":["https://drafts.csswg.org/css-sizing-4/#width-height-keywords","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":18}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":7}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":46},{"prefix":"-webkit-","added":25}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":94},{"partial_implementation":true,"prefix":"-moz-","added":3}],"firefox_android":[{"added":94},{"partial_implementation":true,"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":9}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":9}]}},"_aliasOf":"min-content"},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-fill-available","added":28}],"chrome_android":[{"alternative_name":"-webkit-fill-available","added":28}],"edge":[{"alternative_name":"-webkit-fill-available","added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"stretch"},"-webkit-fit-content":{"_aliasOf":"fit-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"intrinsic":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-webkit-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"},"-webkit-min-content":{"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"stretch"}},"max-inline-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/max-inline-size","spec_url":["https://drafts.csswg.org/css-logical/#propdef-max-inline-size","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1},{"prefix":"-webkit-","added":10.1}],"safari_ios":[{"added":12.2},{"prefix":"-webkit-","added":10.3}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"min-content"},"_aliasOf":"max-inline-size","-moz-fit-content":{"_aliasOf":"fit-content"},"-webkit-fill-available":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"max-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/max-width","spec_url":["https://drafts.csswg.org/css-sizing-4/#width-height-keywords","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":7}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":94},{"partial_implementation":true,"prefix":"-moz-","added":3}],"firefox_android":[{"added":94},{"partial_implementation":true,"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":22}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"min-content"},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-fill-available","added":22}],"chrome_android":[{"alternative_name":"-webkit-fill-available","added":25}],"edge":[{"alternative_name":"-webkit-fill-available","added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"stretch"},"-webkit-fit-content":{"_aliasOf":"fit-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"intrinsic":{"_aliasOf":"min-content"},"-webkit-max-content":{"_aliasOf":"max-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-webkit-min-content":{"_aliasOf":"min-content"},"-moz-min-content":{"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"stretch"}},"min-block-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/min-block-size","spec_url":["https://drafts.csswg.org/css-logical/#propdef-min-block-size","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":94}],"firefox_android":[{"added":94}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"min-height":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/min-height","spec_url":["https://drafts.csswg.org/css-sizing/#width-height-keywords","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":7}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"version_last":"21","added":16,"removed":22}],"firefox_android":[{"version_last":"21","added":16,"removed":22}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":9}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46}],"chrome_android":[{"added":46}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":9}]}},"_aliasOf":"min-content"},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-fill-available","added":28}],"chrome_android":[{"alternative_name":"-webkit-fill-available","added":28}],"edge":[{"alternative_name":"-webkit-fill-available","added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"alternative_name":"-webkit-fill-available","added":9}],"safari_ios":[{"alternative_name":"-webkit-fill-available","added":9}]}},"_aliasOf":"stretch"},"-webkit-fit-content":{"_aliasOf":"fit-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"intrinsic":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-webkit-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"},"-webkit-min-content":{"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"stretch"}},"min-inline-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/min-inline-size","spec_url":["https://drafts.csswg.org/css-logical/#propdef-min-inline-size","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":41}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"_aliasOf":"min-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-webkit-fill-available":{"_aliasOf":"fit-content"},"-moz-max-content":{"_aliasOf":"max-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"min-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/min-width","spec_url":["https://drafts.csswg.org/css-sizing/#min-size-properties","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":7}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":34},{"version_last":"21","added":16,"removed":22}],"firefox_android":[{"added":34},{"version_last":"21","added":16,"removed":22}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"fit-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":25},{"alternative_name":"min-intrinsic","version_last":"47","added":25,"removed":48}],"chrome_android":[{"added":46},{"prefix":"-webkit-","added":25},{"alternative_name":"min-intrinsic","version_last":"47","added":25,"removed":48}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"alternative_name":"min-intrinsic","added":2}],"safari_ios":[{"added":11},{"alternative_name":"min-intrinsic","added":1}]}},"_aliasOf":"min-content"},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-fill-available","added":22}],"chrome_android":[{"alternative_name":"-webkit-fill-available","added":25}],"edge":[{"alternative_name":"-webkit-fill-available","added":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"stretch"},"-webkit-fit-content":{"_aliasOf":"fit-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-webkit-fill-available":{"_aliasOf":"stretch"},"-webkit-max-content":{"_aliasOf":"max-content"},"-moz-max-content":{"_aliasOf":"max-content"},"intrinsic":{"_aliasOf":"max-content"},"-webkit-min-content":{"_aliasOf":"min-content"},"min-intrinsic":{"_aliasOf":"min-content"},"-moz-min-content":{"_aliasOf":"min-content"}},"mix-blend-mode":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/mix-blend-mode","spec_url":"https://drafts.fxtf.org/compositing/#mix-blend-mode","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":41}],"chrome_android":[{"added":41}],"edge":[{"added":79}],"firefox":[{"added":32}],"firefox_android":[{"added":32}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}},"plus-lighter":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":100}],"chrome_android":[{"added":100}],"edge":[{"added":100}],"firefox":[{"added":99}],"firefox_android":[{"added":99}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"svg":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":41}],"chrome_android":[{"added":false}],"edge":[{"added":79}],"firefox":[{"added":32}],"firefox_android":[{"added":32}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"object-fit":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/object-fit","spec_url":"https://drafts.csswg.org/css-images/#the-object-fit","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":32}],"chrome_android":[{"added":32}],"edge":[{"added":79},{"partial_implementation":true,"version_last":"18","added":16,"removed":79}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":false}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"_aliasOf":"object-fit"},"object-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/object-position","spec_url":"https://drafts.csswg.org/css-images/#the-object-position","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":32}],"chrome_android":[{"added":32}],"edge":[{"added":79},{"partial_implementation":true,"version_last":"18","added":16,"removed":79}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":false}],"safari":[{"added":10}],"safari_ios":[{"added":10}]}},"_aliasOf":"object-position"},"object-view-box":{"__compat":{"spec_url":"https://drafts.csswg.org/css-images-4/#the-object-view-box","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":104}],"chrome_android":[{"added":104}],"edge":[{"added":104}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"offset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/offset","spec_url":"https://drafts.fxtf.org/motion/#offset-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55},{"alternative_name":"motion","added":46}],"chrome_android":[{"added":55},{"alternative_name":"motion","added":46}],"edge":[{"added":79},{"alternative_name":"motion","added":79}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:motion-path"]},"_aliasOf":"offset"},"offset-anchor":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/offset-anchor","spec_url":"https://drafts.fxtf.org/motion/#offset-anchor-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:motion-path"]}},"offset-distance":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/offset-distance","spec_url":"https://drafts.fxtf.org/motion/#offset-distance-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55},{"alternative_name":"motion-distance","added":46}],"chrome_android":[{"added":55},{"alternative_name":"motion-distance","added":46}],"edge":[{"added":79},{"alternative_name":"motion-distance","added":79}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:motion-path"]},"_aliasOf":"offset-distance"},"offset-path":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/offset-path","spec_url":"https://drafts.fxtf.org/motion/#offset-path-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55},{"alternative_name":"motion-path","added":46}],"chrome_android":[{"added":55},{"alternative_name":"motion-path","added":46}],"edge":[{"added":79},{"alternative_name":"motion-path","added":79}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]},"tags":["web-features:motion-path"]},"basic-shape":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"flags":[{"name":"layout.css.motion-path-basic-shapes.enabled","type":"preference","value_to_set":"true"}],"added":116}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"coord-box":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"flags":[{"name":"layout.css.motion-path-coord-box.enabled","type":"preference","value_to_set":"true"}],"added":116}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"path":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":64}],"chrome_android":[{"added":64}],"edge":[{"added":79}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:motion-path"]}},"ray":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"flags":[{"name":"layout.css.motion-path-ray.enabled","type":"preference","value_to_set":"true"}],"added":112},{"flags":[{"name":"layout.css.motion-path-ray.enabled","type":"preference","value_to_set":"true"}],"added":72}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"url":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"flags":[{"name":"layout.css.motion-path-url.enabled","type":"preference","value_to_set":"true"}],"added":118}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"_aliasOf":"offset-path"},"offset-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/offset-position","spec_url":"https://drafts.fxtf.org/motion/#offset-position-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"flags":[{"name":"layout.css.motion-path-offset-position.enabled","type":"preference","value_to_set":"true"}],"added":116}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}},"normal":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"flags":[{"name":"layout.css.motion-path-offset-position.enabled","type":"preference","value_to_set":"true"}],"added":116}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"offset-rotate":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/offset-rotate","spec_url":"https://drafts.fxtf.org/motion/#offset-rotate-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56},{"alternative_name":"offset-rotation","added":55},{"alternative_name":"motion-rotation","added":46}],"chrome_android":[{"added":56},{"alternative_name":"offset-rotation","added":55},{"alternative_name":"motion-rotation","added":46}],"edge":[{"added":79},{"alternative_name":"offset-rotation","added":79},{"alternative_name":"motion-rotation","added":79}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:motion-path"]},"_aliasOf":"offset-rotate"},"opacity":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/opacity","spec_url":"https://drafts.csswg.org/css-color/#transparency","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1},{"prefix":"-moz-","version_last":"3","added":1,"removed":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":9}],"safari":[{"added":2},{"prefix":"-khtml-","version_last":"1.3","added":1.1,"removed":2}],"safari_ios":[{"added":1}]}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":78}],"chrome_android":[{"added":78}],"edge":[{"added":79}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"_aliasOf":"opacity"},"order":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/order","spec_url":"https://drafts.csswg.org/css-display/#order-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":29},{"prefix":"-webkit-","added":21}],"chrome_android":[{"added":29},{"prefix":"-webkit-","added":25}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":20},{"prefix":"-webkit-","added":49}],"firefox_android":[{"added":20},{"prefix":"-webkit-","added":49}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":7}]},"tags":["web-features:flexbox"]},"_aliasOf":"order"},"orphans":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/orphans","spec_url":"https://drafts.csswg.org/css-break/#widows-orphans","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":25}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":8}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}},"outline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/outline","spec_url":"https://drafts.csswg.org/css-ui/#outline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":94},{"partial_implementation":true,"version_last":"93","added":1,"removed":94}],"chrome_android":[{"added":94},{"partial_implementation":true,"version_last":"93","added":18,"removed":94}],"edge":[{"added":94},{"partial_implementation":true,"version_last":"93","added":12,"removed":94}],"firefox":[{"added":88},{"partial_implementation":true,"version_last":"87","added":1.5,"removed":88},{"prefix":"-moz-","version_last":"3.5","added":1,"removed":3.6}],"firefox_android":[{"added":88},{"partial_implementation":true,"version_last":"87","added":4,"removed":88}],"ie":[{"added":8}],"safari":[{"added":16.4},{"partial_implementation":true,"version_last":"16.3","added":1.2,"removed":16.4}],"safari_ios":[{"added":16.4},{"partial_implementation":true,"version_last":"16.3","added":1,"removed":16.4}]}},"invert":{"__compat":{"spec_url":"https://drafts.csswg.org/css-ui/#valdef-outline-color-invert","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"version_last":"2","added":1,"removed":3}],"firefox_android":[{"added":false}],"ie":[{"added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"outline"},"outline-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/outline-color","spec_url":"https://drafts.csswg.org/css-ui/#outline-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5},{"prefix":"-moz-","version_last":"3.5","added":1,"removed":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}},"invert":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"version_last":"2","added":1,"removed":3}],"firefox_android":[{"added":false}],"ie":[{"added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"outline-color"},"outline-offset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/outline-offset","spec_url":"https://drafts.csswg.org/css-ui/#outline-offset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":15}],"firefox":[{"added":1.5}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"outline-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/outline-style","spec_url":"https://drafts.csswg.org/css-ui/#outline-style","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5},{"prefix":"-moz-","version_last":"3.5","added":1,"removed":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1.5}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"_aliasOf":"outline-style"},"outline-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/outline-width","spec_url":"https://drafts.csswg.org/css-ui/#outline-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5},{"prefix":"-moz-","version_last":"3.5","added":1,"removed":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}},"_aliasOf":"outline-width"},"overflow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow","spec_url":"https://drafts.csswg.org/css-overflow/#propdef-overflow","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]},"tags":["web-features:overflow-shorthand"]},"clip":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":90}],"chrome_android":[{"added":90}],"edge":[{"added":90}],"firefox":[{"added":81},{"alternative_name":"-moz-hidden-unscrollable","version_last":"80","added":1.5,"removed":81}],"firefox_android":[{"added":81},{"alternative_name":"-moz-hidden-unscrollable","version_last":"80","added":4,"removed":81}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:overflow-shorthand"]},"_aliasOf":"clip"},"multiple_keywords":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":68}],"chrome_android":[{"added":68}],"edge":[{"added":79}],"firefox":[{"added":61}],"firefox_android":[{"added":61}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]},"tags":["web-features:overflow-shorthand"]}},"overlay":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":100}],"edge":[{"added":79}],"firefox":[{"added":112}],"firefox_android":[{"added":112}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-moz-hidden-unscrollable":{"_aliasOf":"clip"}},"overflow-anchor":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-anchor","spec_url":"https://drafts.csswg.org/css-scroll-anchoring/#exclusion-api","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56}],"chrome_android":[{"added":56}],"edge":[{"added":79}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"overflow-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-block","spec_url":"https://drafts.csswg.org/css-overflow/#overflow-control","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":69}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"overflow-clip-margin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-clip-margin","spec_url":"https://drafts.csswg.org/css-overflow/#overflow-clip-margin","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":90}],"chrome_android":[{"partial_implementation":true,"added":90}],"edge":[{"partial_implementation":true,"added":90}],"firefox":[{"partial_implementation":true,"added":102}],"firefox_android":[{"partial_implementation":true,"added":102}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"overflow-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-inline","spec_url":"https://drafts.csswg.org/css-overflow/#overflow-control","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":69}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"overflow-wrap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-wrap","spec_url":"https://drafts.csswg.org/css-text/#overflow-wrap-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":23},{"alternative_name":"word-wrap","added":1}],"chrome_android":[{"added":25},{"alternative_name":"word-wrap","added":18}],"edge":[{"added":18},{"alternative_name":"word-wrap","added":12}],"firefox":[{"added":49},{"alternative_name":"word-wrap","added":3.5}],"firefox_android":[{"added":49},{"alternative_name":"word-wrap","added":4}],"ie":[{"alternative_name":"word-wrap","added":5.5}],"safari":[{"added":7},{"alternative_name":"word-wrap","added":1}],"safari_ios":[{"added":7},{"alternative_name":"word-wrap","added":1}]}},"anywhere":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"break-word":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"_aliasOf":"overflow-wrap"},"overflow-x":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-x","spec_url":"https://drafts.csswg.org/css-overflow/#overflow-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":3}],"safari_ios":[{"added":1}]},"tags":["web-features:overflow-shorthand"]},"clip":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":90}],"chrome_android":[{"added":90}],"edge":[{"added":90}],"firefox":[{"added":81},{"alternative_name":"-moz-hidden-unscrollable","version_last":"80","added":3.5,"removed":81}],"firefox_android":[{"added":81},{"alternative_name":"-moz-hidden-unscrollable","version_last":"80","added":4,"removed":81}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:overflow-shorthand"]},"_aliasOf":"clip"},"overlay":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":100}],"edge":[{"added":79}],"firefox":[{"added":112}],"firefox_android":[{"added":112}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"overflow-x","-moz-hidden-unscrollable":{"_aliasOf":"clip"}},"overflow-y":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overflow-y","spec_url":"https://drafts.csswg.org/css-overflow/#overflow-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":3}],"safari_ios":[{"added":1}]},"tags":["web-features:overflow-shorthand"]},"clip":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":90}],"chrome_android":[{"added":90}],"edge":[{"added":90}],"firefox":[{"added":81},{"alternative_name":"-moz-hidden-unscrollable","version_last":"80","added":3.5,"removed":81}],"firefox_android":[{"added":81},{"alternative_name":"-moz-hidden-unscrollable","version_last":"80","added":4,"removed":81}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]},"tags":["web-features:overflow-shorthand"]},"_aliasOf":"clip"},"overlay":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":15}],"chrome_android":[{"added":100}],"edge":[{"added":79}],"firefox":[{"added":112}],"firefox_android":[{"added":112}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"overflow-y","-moz-hidden-unscrollable":{"_aliasOf":"clip"}},"overlay":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overlay","spec_url":"https://drafts.csswg.org/css-position-4/#overlay","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"overscroll-behavior":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior","spec_url":"https://drafts.csswg.org/css-overscroll/#overscroll-behavior-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":63}],"chrome_android":[{"added":63}],"edge":[{"partial_implementation":true,"added":18}],"firefox":[{"added":59}],"firefox_android":[{"added":59}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"overscroll-behavior-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-block","spec_url":"https://drafts.csswg.org/css-overscroll/#overscroll-behavior-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":77}],"chrome_android":[{"added":77}],"edge":[{"added":79}],"firefox":[{"added":73}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"overscroll-behavior-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-inline","spec_url":"https://drafts.csswg.org/css-overscroll/#overscroll-behavior-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":77}],"chrome_android":[{"added":77}],"edge":[{"added":79}],"firefox":[{"added":73}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"overscroll-behavior-x":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-x","spec_url":"https://drafts.csswg.org/css-overscroll/#overscroll-behavior-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":63}],"chrome_android":[{"added":63}],"edge":[{"partial_implementation":true,"added":18}],"firefox":[{"added":59}],"firefox_android":[{"added":59}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"overscroll-behavior-y":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-y","spec_url":"https://drafts.csswg.org/css-overscroll/#overscroll-behavior-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":63}],"chrome_android":[{"added":63}],"edge":[{"partial_implementation":true,"added":18}],"firefox":[{"added":59}],"firefox_android":[{"added":59}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"padding":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding","spec_url":"https://drafts.csswg.org/css-box/#padding-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"padding-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-block","spec_url":"https://drafts.csswg.org/css-logical/#propdef-padding-block","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"padding-block-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-block-end","spec_url":"https://drafts.csswg.org/css-logical/#padding-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"padding-block-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-block-start","spec_url":"https://drafts.csswg.org/css-logical/#padding-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"padding-bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-bottom","spec_url":"https://drafts.csswg.org/css-box/#padding-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"padding-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-inline","spec_url":"https://drafts.csswg.org/css-logical/#propdef-padding-inline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":66}],"firefox_android":[{"added":66}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"padding-inline-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-inline-end","spec_url":"https://drafts.csswg.org/css-logical/#padding-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69},{"alternative_name":"-webkit-padding-end","added":2}],"chrome_android":[{"added":69},{"alternative_name":"-webkit-padding-end","added":18}],"edge":[{"added":79},{"alternative_name":"-webkit-padding-end","added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-padding-end","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-padding-end","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1},{"alternative_name":"-webkit-padding-end","added":3}],"safari_ios":[{"added":12.2},{"alternative_name":"-webkit-padding-end","added":3}]}},"_aliasOf":"padding-inline-end"},"padding-inline-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-inline-start","spec_url":"https://drafts.csswg.org/css-logical/#padding-properties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69},{"alternative_name":"-webkit-padding-start","added":2}],"chrome_android":[{"added":69},{"alternative_name":"-webkit-padding-start","added":18}],"edge":[{"added":79},{"alternative_name":"-webkit-padding-start","added":79}],"firefox":[{"added":41},{"alternative_name":"-moz-padding-start","added":3}],"firefox_android":[{"added":41},{"alternative_name":"-moz-padding-start","added":4}],"ie":[{"added":false}],"safari":[{"added":12.1},{"alternative_name":"-webkit-padding-start","added":3}],"safari_ios":[{"added":12.2},{"alternative_name":"-webkit-padding-start","added":3}]}},"_aliasOf":"padding-inline-start"},"padding-left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-left","spec_url":"https://drafts.csswg.org/css-box/#padding-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"padding-right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-right","spec_url":"https://drafts.csswg.org/css-box/#padding-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"padding-top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/padding-top","spec_url":"https://drafts.csswg.org/css-box/#padding-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"page":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/page","spec_url":"https://drafts.csswg.org/css-page/#using-named-pages","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":85}],"chrome_android":[{"added":85}],"edge":[{"added":85}],"firefox":[{"added":110}],"firefox_android":[{"added":110}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"page-break-after":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/page-break-after","spec_url":["https://drafts.csswg.org/css-logical/#page","https://drafts.csswg.org/css-page/#page-break-after"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"page-break-before":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/page-break-before","spec_url":["https://drafts.csswg.org/css-logical/#page","https://drafts.csswg.org/css-page/#page-break-before"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1.2}],"safari_ios":[{"added":1}]}}},"page-break-inside":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/page-break-inside","spec_url":"https://drafts.csswg.org/css-page/#page-break-inside","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":19}],"firefox_android":[{"added":19}],"ie":[{"added":8}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}},"paint-order":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/paint-order","spec_url":"https://svgwg.org/svg2-draft/painting.html#PaintOrder","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":35}],"chrome_android":[{"partial_implementation":true,"added":35}],"edge":[{"partial_implementation":true,"added":79}],"firefox":[{"added":60}],"firefox_android":[{"added":60}],"ie":[{"added":false}],"safari":[{"added":11},{"partial_implementation":true,"added":8}],"safari_ios":[{"added":11},{"partial_implementation":true,"added":8}]}}},"perspective":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/perspective","spec_url":"https://drafts.csswg.org/css-transforms-2/#perspective-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"prefix":"-webkit-","added":12}],"chrome_android":[{"added":36},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":10,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":10,"removed":false}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"perspective"},"perspective-origin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/perspective-origin","spec_url":"https://drafts.csswg.org/css-transforms-2/#perspective-origin-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"prefix":"-webkit-","added":12}],"chrome_android":[{"added":36},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":10,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":10,"removed":false}],"ie":[{"added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"perspective-origin"},"place-content":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/place-content","spec_url":"https://drafts.csswg.org/css-align/#place-content","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}},"flex_context":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]},"tags":["web-features:flexbox"]}},"grid_context":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:grid"]}}},"place-items":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/place-items","spec_url":"https://drafts.csswg.org/css-align/#place-items-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}},"flex_context":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:flexbox"]}},"grid_context":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:grid"]}}},"place-self":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/place-self","spec_url":"https://drafts.csswg.org/css-align/#place-self-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}},"flex_context":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:flexbox"]}},"grid_context":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":59}],"chrome_android":[{"added":59}],"edge":[{"added":79}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]},"tags":["web-features:grid"]}}},"pointer-events":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/pointer-events","spec_url":["https://drafts.csswg.org/css-ui/#pointer-events-control","https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":4}],"ie":[{"added":11}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}},"html_elements":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":11}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}}},"position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/position","spec_url":"https://drafts.csswg.org/css-position/#position-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"absolutely_positioned_flex_children":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":52}],"chrome_android":[{"added":52}],"edge":[{"added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":10}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"fixed":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":7}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"position_sticky_table_elements":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56}],"chrome_android":[{"added":56}],"edge":[{"added":16}],"firefox":[{"added":59}],"firefox_android":[{"added":59}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]},"tags":["web-features:sticky-positioning"]}},"sticky":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56}],"chrome_android":[{"added":56}],"edge":[{"added":16}],"firefox":[{"added":32}],"firefox_android":[{"added":32}],"ie":[{"added":false}],"safari":[{"added":13},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":13},{"prefix":"-webkit-","added":7}]},"tags":["web-features:sticky-positioning"]},"_aliasOf":"sticky"},"-webkit-sticky":{"_aliasOf":"sticky"}},"print-color-adjust":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/print-color-adjust","spec_url":"https://drafts.csswg.org/css-color-adjust/#propdef-print-color-adjust","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"prefix":"-webkit-","added":17}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79}],"firefox":[{"added":97},{"alternative_name":"color-adjust","added":48}],"firefox_android":[{"added":97},{"alternative_name":"color-adjust","added":48}],"ie":[{"added":false}],"safari":[{"added":15.4},{"prefix":"-webkit-","added":6}],"safari_ios":[{"added":15.4},{"prefix":"-webkit-","added":6}]}},"_aliasOf":"print-color-adjust"},"quotes":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/quotes","spec_url":"https://drafts.csswg.org/css-content/#quotes","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":11}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1.5}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}},"quotes_auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}}},"r":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#R","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"resize":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/resize","spec_url":"https://drafts.csswg.org/css-ui/#resize","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":4}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}},"block_level_support":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":5}],"firefox_android":[{"added":5}],"ie":[{"added":false}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"flow_relative_support":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":118}],"chrome_android":[{"added":118}],"edge":[{"added":118}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}}},"right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/right","spec_url":"https://drafts.csswg.org/css-position/#insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"rotate":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/rotate","spec_url":"https://drafts.csswg.org/css-transforms-2/#individual-transforms","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":104}],"chrome_android":[{"added":104}],"edge":[{"added":104}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}},"x_y_z_angle":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":104}],"chrome_android":[{"added":104}],"edge":[{"added":104}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}}},"row-gap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/row-gap","spec_url":"https://drafts.csswg.org/css-align/#column-row-gap","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":47}],"chrome_android":[{"added":47}],"edge":[{"added":16}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}},"flex_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#column-row-gap","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":84}],"chrome_android":[{"added":84}],"edge":[{"added":84}],"firefox":[{"added":63}],"firefox_android":[{"added":63}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]},"tags":["web-features:flexbox-gap"]}},"grid_context":{"__compat":{"spec_url":"https://drafts.csswg.org/css-align/#column-row-gap","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":66},{"alternative_name":"grid-row-gap","added":57}],"chrome_android":[{"added":66},{"alternative_name":"grid-row-gap","added":57}],"edge":[{"added":16},{"alternative_name":"grid-row-gap","added":16}],"firefox":[{"added":61},{"alternative_name":"grid-row-gap","added":52}],"firefox_android":[{"added":61},{"alternative_name":"grid-row-gap","added":52}],"ie":[{"added":false}],"safari":[{"added":12},{"alternative_name":"grid-row-gap","added":10.1}],"safari_ios":[{"added":12},{"alternative_name":"grid-row-gap","added":10.3}]},"tags":["web-features:grid"]},"_aliasOf":"grid_context"},"grid-row-gap":{"_aliasOf":"grid_context"}},"ruby-align":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/ruby-align","spec_url":"https://drafts.csswg.org/css-ruby/#ruby-align-property","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":38}],"firefox_android":[{"added":38}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"ruby-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/ruby-position","spec_url":"https://drafts.csswg.org/css-ruby/#rubypos","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":84},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":84},{"prefix":"-webkit-","added":18}],"edge":[{"added":84},{"prefix":"-webkit-","added":79},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":38}],"firefox_android":[{"added":38}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":7}],"safari_ios":[{"prefix":"-webkit-","added":7}]}},"alternate":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"impl_url":"https://crbug.com/1191394","added":false}],"chrome_android":[{"impl_url":"https://crbug.com/1191394","added":false}],"edge":[{"impl_url":"https://crbug.com/1191394","added":false}],"firefox":[{"added":88}],"firefox_android":[{"added":88}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"inter-character":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"impl_url":"https://crbug.com/1258284","added":false}],"chrome_android":[{"impl_url":"https://crbug.com/1258284","added":false}],"edge":[{"impl_url":"https://crbug.com/1258284","added":false}],"firefox":[{"impl_url":"https://bugzil.la/1055672","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1055672","added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"ruby-position"},"rx":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#RX","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"ry":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#RY","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"scale":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scale","spec_url":"https://drafts.csswg.org/css-transforms-2/#individual-transforms","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":104}],"chrome_android":[{"added":104}],"edge":[{"added":104}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"scroll-behavior":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-behavior","spec_url":"https://drafts.csswg.org/css-overflow/#smooth-scrolling","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":61}],"chrome_android":[{"added":61}],"edge":[{"added":79}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}},"scroll-margin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin","spec_url":"https://drafts.csswg.org/css-scroll-snap/#scroll-margin","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":90},{"partial_implementation":true,"version_last":"89","added":68,"removed":90}],"firefox_android":[{"added":90},{"partial_implementation":true,"version_last":"89","added":68,"removed":90}],"ie":[{"added":false}],"safari":[{"added":14.1},{"alternative_name":"scroll-snap-margin","partial_implementation":true,"added":11}],"safari_ios":[{"added":14.5},{"alternative_name":"scroll-snap-margin","partial_implementation":true,"added":11}]}},"_aliasOf":"scroll-margin"},"scroll-margin-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block","spec_url":"https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-block","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-margin-block-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block-end","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-margin-block-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block-start","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-margin-bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-bottom","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"alternative_name":"scroll-snap-margin-bottom","partial_implementation":true,"added":11}],"safari_ios":[{"added":14.5},{"alternative_name":"scroll-snap-margin-bottom","partial_implementation":true,"added":11}]}},"_aliasOf":"scroll-margin-bottom"},"scroll-margin-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline","spec_url":"https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-inline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-margin-inline-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline-end","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-margin-inline-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline-start","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-margin-left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-left","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"alternative_name":"scroll-snap-margin-left","partial_implementation":true,"added":11}],"safari_ios":[{"added":14.5},{"alternative_name":"scroll-snap-margin-left","partial_implementation":true,"added":11}]}},"_aliasOf":"scroll-margin-left"},"scroll-margin-right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-right","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"alternative_name":"scroll-snap-margin-right","partial_implementation":true,"added":11}],"safari_ios":[{"added":14.5},{"alternative_name":"scroll-snap-margin-right","partial_implementation":true,"added":11}]}},"_aliasOf":"scroll-margin-right"},"scroll-margin-top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-top","spec_url":"https://drafts.csswg.org/css-scroll-snap/#margin-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"alternative_name":"scroll-snap-margin-top","partial_implementation":true,"added":11}],"safari_ios":[{"added":14.5},{"alternative_name":"scroll-snap-margin-top","partial_implementation":true,"added":11}]}},"_aliasOf":"scroll-margin-top"},"scroll-padding":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding","spec_url":"https://drafts.csswg.org/css-scroll-snap/#scroll-padding","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.1}],"safari_ios":[{"added":14.5},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.5}]}}},"scroll-padding-block":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block","spec_url":"https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-block","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-padding-block-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block-end","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-padding-block-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block-start","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-padding-bottom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-bottom","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.1}],"safari_ios":[{"added":14.5},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.5}]}}},"scroll-padding-inline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline","spec_url":"https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-inline","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-padding-inline-end":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline-end","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-padding-inline-start":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline-start","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-logical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-padding-left":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-left","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.1}],"safari_ios":[{"added":14.5},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.5}]}}},"scroll-padding-right":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-right","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.1}],"safari_ios":[{"added":14.5},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.5}]}}},"scroll-padding-top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-top","spec_url":"https://drafts.csswg.org/css-scroll-snap/#padding-longhands-physical","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":14.1},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.1}],"safari_ios":[{"added":14.5},{"partial_implementation":true,"version_last":"14","added":11,"removed":14.5}]}}},"scroll-snap-align":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-align","spec_url":"https://drafts.csswg.org/css-scroll-snap/#scroll-snap-align","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79}],"firefox":[{"added":68}],"firefox_android":[{"added":68}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}}},"scroll-snap-stop":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-stop","spec_url":"https://drafts.csswg.org/css-scroll-snap/#scroll-snap-stop","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":75}],"chrome_android":[{"added":75}],"edge":[{"added":79}],"firefox":[{"added":103}],"firefox_android":[{"added":103}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"scroll-snap-type":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-type","spec_url":"https://drafts.csswg.org/css-scroll-snap/#scroll-snap-type","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":69}],"chrome_android":[{"added":69}],"edge":[{"added":79},{"prefix":"-ms-","version_last":"18","added":12,"removed":79}],"firefox":[{"added":99},{"partial_implementation":true,"version_last":"98","added":68,"removed":99},{"version_last":"67","added":39,"removed":68}],"firefox_android":[{"added":68},{"version_last":"67","added":39,"removed":68}],"ie":[{"prefix":"-ms-","added":10}],"safari":[{"added":11},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":9}]}},"_aliasOf":"scroll-snap-type"},"scroll-timeline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline","spec_url":"https://drafts.csswg.org/scroll-animations/#scroll-timeline-shorthand","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":111}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"scroll-timeline-axis":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline-axis","spec_url":"https://drafts.csswg.org/scroll-animations/#propdef-scroll-timeline-axis","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":111}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"scroll-timeline-name":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline-name","spec_url":"https://drafts.csswg.org/scroll-animations/#scroll-timeline-name","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":111}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"scrollbar-3dlight-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-3dlight-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-3dlight-color"},"scrollbar-arrow-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-arrow-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-arrow-color"},"scrollbar-base-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-base-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-base-color"},"scrollbar-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-color","spec_url":"https://drafts.csswg.org/css-scrollbars/#scrollbar-color","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":121}],"chrome_android":[{"added":121}],"edge":[{"added":121}],"firefox":[{"added":64}],"firefox_android":[{"added":64}],"ie":[{"added":false}],"safari":[{"impl_url":"https://webkit.org/b/231590","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/231590","added":false}]}}},"scrollbar-darkshadow-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-darkshadow-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-darkshadow-color"},"scrollbar-face-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-face-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-face-color"},"scrollbar-gutter":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-gutter","spec_url":"https://drafts.csswg.org/css-overflow/#scrollbar-gutter-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":94}],"chrome_android":[{"added":94}],"edge":[{"added":94}],"firefox":[{"added":97}],"firefox_android":[{"added":97}],"ie":[{"added":false}],"safari":[{"flags":[{"name":"CSS scrollerbar-gutter property","type":"preference","value_to_set":"true"}],"added":17}],"safari_ios":[{"flags":[{"name":"CSS scrollerbar-gutter property","type":"preference","value_to_set":"true"}],"added":17}]}}},"scrollbar-highlight-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-highlight-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-highlight-color"},"scrollbar-shadow-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-shadow-color","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":5},{"prefix":"-ms-","added":8}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"scrollbar-shadow-color"},"scrollbar-width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/scrollbar-width","spec_url":"https://drafts.csswg.org/css-scrollbars/#scrollbar-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":121}],"chrome_android":[{"added":121}],"edge":[{"added":121}],"firefox":[{"added":64}],"firefox_android":[{"added":64}],"ie":[{"added":false}],"safari":[{"impl_url":"https://webkit.org/b/231588","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/231588","added":false}]}}},"shape-image-threshold":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/shape-image-threshold","spec_url":"https://drafts.csswg.org/css-shapes/#shape-image-threshold-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":78}],"chrome_android":[{"added":78}],"edge":[{"added":79}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"shape-margin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/shape-margin","spec_url":"https://drafts.csswg.org/css-shapes/#shape-margin-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1},{"prefix":"-webkit-","added":10.1}],"safari_ios":[{"added":10.3}]}},"_aliasOf":"shape-margin"},"shape-outside":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/shape-outside","spec_url":"https://drafts.csswg.org/css-shapes/#shape-outside-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}},"circle":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/basic-shape#circle()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"gradient":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/gradient","spec_url":"https://drafts.csswg.org/css-images/#gradients","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"image":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/image","spec_url":"https://drafts.csswg.org/css-images/#image-values","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"inset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/basic-shape#inset()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"path":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/path","spec_url":"https://drafts.csswg.org/css-shapes/#funcdef-basic-shape-path","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"polygon":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/basic-shape#polygon()","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":37}],"chrome_android":[{"added":37}],"edge":[{"added":79}],"firefox":[{"added":62}],"firefox_android":[{"added":62}],"ie":[{"added":false}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}}},"shape-rendering":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/painting.html#ShapeRendering","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"speak":{"__compat":{"spec_url":"https://drafts.csswg.org/css-speech-1/#speaking-props-speak","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":80}],"chrome_android":[{"partial_implementation":true,"added":80}],"edge":[{"added":80}],"firefox":[{"impl_url":"https://bugzil.la/1748064","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1748064","added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"speak-as":{"__compat":{"spec_url":"https://drafts.csswg.org/css-speech-1/#speaking-props-speak-as","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"impl_url":"https://bugzil.la/1748068","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1748068","added":false}],"ie":[{"added":false}],"safari":[{"added":11.1}],"safari_ios":[{"added":11.3}]}}},"stop-color":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/pservers.html#StopColorProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stop-opacity":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/pservers.html#StopOpacityProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-shorthand","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-color":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-color","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-dasharray":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-dasharray","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-dashoffset":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-dashoffset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-linecap":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-linecap","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-linejoin":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-linejoin","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-miterlimit":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-miterlimit","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-opacity":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-opacity","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-width":{"__compat":{"spec_url":"https://drafts.fxtf.org/fill-stroke-3/#stroke-width","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"tab-size":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/tab-size","spec_url":"https://drafts.csswg.org/css-text/#tab-size-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":21}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":91},{"prefix":"-moz-","added":4}],"firefox_android":[{"added":91},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}},"length":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/length","spec_url":"https://drafts.csswg.org/css-values/#lengths","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":42}],"chrome_android":[{"added":42}],"edge":[{"added":79}],"firefox":[{"added":53}],"firefox_android":[{"added":53}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"_aliasOf":"tab-size"},"table-layout":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/table-layout","spec_url":"https://drafts.csswg.org/css2/#width-layout","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":14}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5}],"safari":[{"added":1}],"safari_ios":[{"added":3}]}}},"text-align":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-align","spec_url":["https://drafts.csswg.org/css-logical/#text-align","https://drafts.csswg.org/css-text/#text-align-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"block_alignment_values":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":18},{"prefix":"-webkit-","added":18}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":1},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":4},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":4},{"prefix":"-webkit-","added":1.3},{"prefix":"-khtml-","added":1}],"safari_ios":[{"added":3.2},{"prefix":"-webkit-","added":1},{"prefix":"-khtml-","added":1}]}},"_aliasOf":"block_alignment_values"},"flow_relative_values_start_and_end":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":3.1}],"safari_ios":[{"added":2}]}}},"match-parent":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"prefix":"-webkit-","added":16}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":79}],"firefox":[{"added":40}],"firefox_android":[{"added":40}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}},"_aliasOf":"match-parent"},"-webkit-block_alignment_values":{"_aliasOf":"block_alignment_values"},"-moz-block_alignment_values":{"_aliasOf":"block_alignment_values"},"-khtml-block_alignment_values":{"_aliasOf":"block_alignment_values"},"-webkit-match-parent":{"_aliasOf":"match-parent"}},"text-align-last":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-align-last","spec_url":"https://drafts.csswg.org/css-text/#text-align-last-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":47}],"chrome_android":[{"added":47}],"edge":[{"added":12}],"firefox":[{"added":49},{"prefix":"-moz-","version_last":"52","added":12,"removed":53}],"firefox_android":[{"added":49},{"prefix":"-moz-","version_last":"52","added":14,"removed":53}],"ie":[{"partial_implementation":true,"added":5.5}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}},"_aliasOf":"text-align-last"},"text-anchor":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"text-combine-upright":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-combine-upright","spec_url":"https://drafts.csswg.org/css-writing-modes/#text-combine-upright","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48},{"alternative_name":"-webkit-text-combine","partial_implementation":true,"added":9}],"chrome_android":[{"added":48},{"alternative_name":"-webkit-text-combine","partial_implementation":true,"added":18}],"edge":[{"added":79},{"alternative_name":"-ms-text-combine-horizontal","version_last":"18","added":12,"removed":79}],"firefox":[{"added":48}],"firefox_android":[{"added":48}],"ie":[{"alternative_name":"-ms-text-combine-horizontal","added":11}],"safari":[{"added":15.4},{"alternative_name":"-webkit-text-combine","partial_implementation":true,"added":5.1}],"safari_ios":[{"added":15.4},{"alternative_name":"-webkit-text-combine","partial_implementation":true,"added":5}]}},"digits":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"alternative_name":"-ms-text-combine-horizontal","added":11}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"digits"},"_aliasOf":"text-combine-upright","-ms-text-combine-horizontal":{"_aliasOf":"digits"}},"text-decoration":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration","spec_url":"https://drafts.csswg.org/css-text-decor/#text-decoration-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"shorthand":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":6}],"firefox_android":[{"added":6}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":8}],"safari_ios":[{"prefix":"-webkit-","added":8}]}},"_aliasOf":"shorthand"},"text-decoration-thickness":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"-webkit-shorthand":{"_aliasOf":"shorthand"}},"text-decoration-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration-color","spec_url":"https://drafts.csswg.org/css-text-decor/#text-decoration-color-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":36},{"prefix":"-moz-","version_last":"38","added":6,"removed":39}],"firefox_android":[{"added":36},{"prefix":"-moz-","version_last":"38","added":6,"removed":39}],"ie":[{"added":false}],"safari":[{"added":12.1},{"prefix":"-webkit-","added":8}],"safari_ios":[{"added":12.2},{"prefix":"-webkit-","added":8}]}},"_aliasOf":"text-decoration-color"},"text-decoration-line":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration-line","spec_url":"https://drafts.csswg.org/css-text-decor/#text-decoration-line-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":36},{"prefix":"-moz-","version_last":"38","added":6,"removed":39}],"firefox_android":[{"added":36},{"prefix":"-moz-","version_last":"38","added":6,"removed":39}],"ie":[{"added":false}],"safari":[{"added":12.1},{"prefix":"-webkit-","added":8}],"safari_ios":[{"added":12.2},{"prefix":"-webkit-","added":8}]}},"blink":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":26}],"firefox_android":[{"added":26}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"_aliasOf":"text-decoration-line"},"text-decoration-skip":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration-skip","spec_url":"https://drafts.csswg.org/css-text-decor-4/#text-decoration-skipping","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"version_last":"63","added":57,"removed":64}],"chrome_android":[{"version_last":"63","added":57,"removed":64}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":12.1},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":12.2},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"text-decoration-skip"},"text-decoration-skip-ink":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration-skip-ink","spec_url":"https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":64}],"chrome_android":[{"added":64}],"edge":[{"added":79}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}},"all":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":75}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":15.4}],"safari_ios":[{"added":15.4}]}}}},"text-decoration-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration-style","spec_url":"https://drafts.csswg.org/css-text-decor/#text-decoration-style-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":36},{"prefix":"-moz-","version_last":"38","added":6,"removed":39}],"firefox_android":[{"added":36},{"prefix":"-moz-","version_last":"38","added":6,"removed":39}],"ie":[{"added":false}],"safari":[{"added":12.1},{"prefix":"-webkit-","added":8}],"safari_ios":[{"added":12.2},{"prefix":"-webkit-","added":8}]}},"wavy":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":57}],"chrome_android":[{"added":57}],"edge":[{"added":79}],"firefox":[{"added":6}],"firefox_android":[{"added":6}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"_aliasOf":"text-decoration-style"},"text-decoration-thickness":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-decoration-thickness","spec_url":"https://drafts.csswg.org/css-text-decor-4/#text-decoration-width-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":89},{"partial_implementation":true,"version_last":"88","added":87,"removed":89}],"chrome_android":[{"added":89},{"partial_implementation":true,"version_last":"88","added":87,"removed":89}],"edge":[{"added":89},{"partial_implementation":true,"version_last":"88","added":87,"removed":89}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"percentage":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"text-emphasis":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-emphasis","spec_url":"https://drafts.csswg.org/css-text-decor/#text-emphasis-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":99},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":99},{"prefix":"-webkit-","added":25}],"edge":[{"added":99},{"prefix":"-webkit-","added":79}],"firefox":[{"added":46}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":7},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":7},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"text-emphasis"},"text-emphasis-color":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-emphasis-color","spec_url":"https://drafts.csswg.org/css-text-decor/#text-emphasis-color-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":99},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":99},{"prefix":"-webkit-","added":25}],"edge":[{"added":99},{"prefix":"-webkit-","added":79}],"firefox":[{"added":46}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":7},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":7},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"text-emphasis-color"},"text-emphasis-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-emphasis-position","spec_url":"https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":99},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":99},{"prefix":"-webkit-","added":25}],"edge":[{"added":99},{"prefix":"-webkit-","added":79}],"firefox":[{"added":46}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":7},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":7},{"prefix":"-webkit-","added":7}]}},"left_and_right":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":62}],"chrome_android":[{"added":62}],"edge":[{"added":79}],"firefox":[{"added":46}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"over":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":99}],"chrome_android":[{"added":99}],"edge":[{"added":99}],"firefox":[{"added":108}],"firefox_android":[{"added":108}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"under":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":99}],"chrome_android":[{"added":99}],"edge":[{"added":99}],"firefox":[{"added":108}],"firefox_android":[{"added":108}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"_aliasOf":"text-emphasis-position"},"text-emphasis-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-emphasis-style","spec_url":"https://drafts.csswg.org/css-text-decor/#text-emphasis-style-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":99},{"prefix":"-webkit-","added":25}],"chrome_android":[{"added":99},{"prefix":"-webkit-","added":25}],"edge":[{"added":99},{"prefix":"-webkit-","added":79}],"firefox":[{"added":46}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":7},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":7},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"text-emphasis-style"},"text-indent":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-indent","spec_url":"https://drafts.csswg.org/css-text/#text-indent-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":3}],"safari":[{"added":1}],"safari_ios":[{"added":1}]},"tags":["web-features:text-indent"]},"each-line":{"__compat":{"spec_url":"https://drafts.csswg.org/css-text/#valdef-text-indent-each-line","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":121}],"firefox_android":[{"added":121}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}},"hanging":{"__compat":{"spec_url":"https://drafts.csswg.org/css-text/#valdef-text-indent-hanging","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":121}],"firefox_android":[{"added":121}],"ie":[{"added":false}],"safari":[{"added":15}],"safari_ios":[{"added":15}]}}}},"text-justify":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-justify","spec_url":"https://drafts.csswg.org/css-text/#text-justify-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"flags":[{"name":"#enable-experimental-web-platform-features","type":"preference","value_to_set":"true"}],"added":32}],"chrome_android":[{"flags":[{"name":"#enable-experimental-web-platform-features","type":"preference","value_to_set":"true"}],"added":32}],"edge":[{"flags":[{"name":"#enable-experimental-web-platform-features","type":"preference","value_to_set":"true"}],"added":79},{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":55}],"firefox_android":[{"added":55}],"ie":[{"added":11}],"safari":[{"impl_url":"https://webkit.org/b/99945","added":false}],"safari_ios":[{"impl_url":"https://webkit.org/b/99945","added":false}]}}},"text-orientation":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-orientation","spec_url":"https://drafts.csswg.org/css-writing-modes/#text-orientation","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48},{"prefix":"-webkit-","added":11}],"chrome_android":[{"added":48},{"prefix":"-webkit-","added":18}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":false}],"safari":[{"added":14},{"prefix":"-webkit-","added":5.1}],"safari_ios":[{"added":14},{"prefix":"-webkit-","added":5}]}},"sideways":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":25}],"chrome_android":[{"added":25}],"edge":[{"added":79}],"firefox":[{"added":44}],"firefox_android":[{"added":44}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"_aliasOf":"text-orientation"},"text-overflow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-overflow","spec_url":"https://drafts.csswg.org/css-overflow/#text-overflow","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":7}],"firefox_android":[{"added":7}],"ie":[{"added":6},{"prefix":"-ms-","added":8}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}},"string":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"two_value_syntax":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":9}],"firefox_android":[{"added":9}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"text-overflow"},"text-rendering":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-rendering","spec_url":"https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":4}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":5}],"safari_ios":[{"added":4.2}]}}},"geometricPrecision":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":13}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":46}],"ie":[{"added":false}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}}},"text-shadow":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-shadow","spec_url":"https://drafts.csswg.org/css-text-decor/#text-shadow-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":10}],"safari":[{"added":1.1}],"safari_ios":[{"added":1}]}}},"text-size-adjust":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-size-adjust","spec_url":"https://drafts.csswg.org/css-size-adjust/#adjustment-control","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":54}],"chrome_android":[{"added":54}],"edge":[{"added":79},{"prefix":"-webkit-","version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":14}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"prefix":"-webkit-","added":1}]}},"percentages":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":54}],"chrome_android":[{"added":54}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"text-size-adjust"},"text-transform":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-transform","spec_url":"https://drafts.csswg.org/css-text/#text-transform","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"capitalize":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"dutch_ij_digraph":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":14}],"firefox_android":[{"added":14}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"full-size-kana":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":64}],"firefox_android":[{"added":64}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"full-width":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":19}],"firefox_android":[{"added":19}],"ie":[{"added":false}],"safari":[{"added":17}],"safari_ios":[{"added":17}]}}},"greek_accented_characters":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":34}],"chrome_android":[{"added":34}],"edge":[{"added":79}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"lowercase_sigma":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":30}],"chrome_android":[{"added":30}],"edge":[{"added":12}],"firefox":[{"added":14}],"firefox_android":[{"added":14}],"ie":[{"added":4}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"math-auto":{"__compat":{"spec_url":"https://w3c.github.io/mathml-core/#new-text-transform-values","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":109}],"chrome_android":[{"added":109}],"edge":[{"added":109}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"turkic_is":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":31}],"chrome_android":[{"added":31}],"edge":[{"added":12}],"firefox":[{"added":14}],"firefox_android":[{"added":14}],"ie":[{"added":4}],"safari":[{"added":8}],"safari_ios":[{"added":8}]}}},"uppercase_eszett":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":18}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"text-underline-offset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-underline-offset","spec_url":"https://drafts.csswg.org/css-text-decor-4/#underline-offset","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":70}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}},"percentage":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}}},"text-underline-position":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-underline-position","spec_url":"https://drafts.csswg.org/css-text-decor/#text-underline-position-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":33}],"chrome_android":[{"added":33}],"edge":[{"added":12}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":6}],"safari":[{"added":12.1},{"prefix":"-webkit-","added":9}],"safari_ios":[{"added":12.2},{"prefix":"-webkit-","added":9}]}},"from-font":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":87}],"chrome_android":[{"added":87}],"edge":[{"added":87}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"left":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":71}],"chrome_android":[{"added":71}],"edge":[{"added":79}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"right":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":71}],"chrome_android":[{"added":71}],"edge":[{"added":79}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"under":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":33}],"chrome_android":[{"added":33}],"edge":[{"added":79}],"firefox":[{"added":74}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":12.1}],"safari_ios":[{"added":12.2}]}}},"_aliasOf":"text-underline-position"},"text-wrap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-wrap","spec_url":"https://drafts.csswg.org/css-text-4/#text-wrap","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":114}],"chrome_android":[{"added":114}],"edge":[{"added":114}],"firefox":[{"added":121}],"firefox_android":[{"added":121}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}},"balance":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-wrap#balance","spec_url":"https://drafts.csswg.org/css-text-4/#valdef-text-wrap-balance","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":114}],"chrome_android":[{"added":114}],"edge":[{"added":114}],"firefox":[{"added":121}],"firefox_android":[{"added":121}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"nowrap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-wrap#nowrap","spec_url":"https://drafts.csswg.org/css-text-4/#valdef-text-wrap-mode-nowrap","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":114}],"chrome_android":[{"added":114}],"edge":[{"added":114}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"pretty":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-wrap#pretty","spec_url":"https://drafts.csswg.org/css-text-4/#valdef-text-wrap-pretty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"stable":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-wrap#stable","spec_url":"https://drafts.csswg.org/css-text-4/#valdef-text-wrap-stable","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":121}],"firefox_android":[{"added":121}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"wrap":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/text-wrap#wrap","spec_url":"https://drafts.csswg.org/css-text-4/#valdef-text-wrap-mode-wrap","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":114}],"chrome_android":[{"added":114}],"edge":[{"added":114}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}}},"timeline-scope":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/timeline-scope","spec_url":"https://drafts.csswg.org/scroll-animations/#propdef-timeline-scope","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":116}],"chrome_android":[{"added":116}],"edge":[{"added":116}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"top":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/top","spec_url":"https://drafts.csswg.org/css-position/#insets","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"touch-action":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/touch-action","spec_url":["https://compat.spec.whatwg.org/#touch-action","https://w3c.github.io/pointerevents/#the-touch-action-css-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36}],"chrome_android":[{"added":36}],"edge":[{"added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":13}],"safari_ios":[{"added":9.3}]}},"axis-pan":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36}],"chrome_android":[{"added":36}],"edge":[{"added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}},"_aliasOf":"axis-pan"},"double-tap-zoom":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"double-tap-zoom"},"manipulation":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36}],"chrome_android":[{"added":36}],"edge":[{"added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":13}],"safari_ios":[{"added":9.3}]}},"_aliasOf":"manipulation"},"none":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36}],"chrome_android":[{"added":36}],"edge":[{"added":12}],"firefox":[{"added":52}],"firefox_android":[{"added":52}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}},"_aliasOf":"none"},"pinch-zoom":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":56}],"chrome_android":[{"added":56}],"edge":[{"added":12}],"firefox":[{"added":85}],"firefox_android":[{"added":85}],"ie":[{"added":11},{"prefix":"-ms-","added":10}],"safari":[{"added":13}],"safari_ios":[{"added":13}]}},"_aliasOf":"pinch-zoom"},"unidirectional-pan":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":55}],"chrome_android":[{"added":55}],"edge":[{"added":79}],"firefox":[{"impl_url":"https://bugzil.la/1285685","added":false}],"firefox_android":[{"impl_url":"https://bugzil.la/1285685","added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"touch-action","-ms-axis-pan":{"_aliasOf":"axis-pan"},"-ms-double-tap-zoom":{"_aliasOf":"double-tap-zoom"},"-ms-manipulation":{"_aliasOf":"manipulation"},"-ms-none":{"_aliasOf":"none"},"-ms-pinch-zoom":{"_aliasOf":"pinch-zoom"}},"transform":{"3d":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":12}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":16}],"firefox_android":[{"added":16}],"ie":[{"added":10}],"safari":[{"added":4}],"safari_ios":[{"added":3.2}]}}},"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transform","spec_url":["https://drafts.csswg.org/css-transforms-2/#transform-functions","https://drafts.csswg.org/css-transforms/#transform-property"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":36},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":40,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":40,"removed":false}],"ie":[{"added":10},{"prefix":"-webkit-","added":11},{"prefix":"-ms-","added":9}],"safari":[{"added":9},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":3.2}]}},"_aliasOf":"transform"},"transform-box":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transform-box","spec_url":"https://drafts.csswg.org/css-transforms/#transform-box","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":64}],"chrome_android":[{"added":64}],"edge":[{"added":79}],"firefox":[{"added":55}],"firefox_android":[{"added":55}],"ie":[{"added":false}],"safari":[{"added":11}],"safari_ios":[{"added":11}]}},"border-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":118}],"chrome_android":[{"added":118}],"edge":[{"added":false}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"content-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":118}],"chrome_android":[{"added":118}],"edge":[{"added":false}],"firefox":[{"added":null}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"stroke-box":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":118}],"chrome_android":[{"added":118}],"edge":[{"added":false}],"firefox":[{"added":null}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}}},"transform-origin":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transform-origin","spec_url":"https://drafts.csswg.org/css-transforms/#transform-origin-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":36},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":3.5,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4,"removed":false}],"ie":[{"added":10},{"prefix":"-ms-","added":9}],"safari":[{"added":9},{"prefix":"-webkit-","added":2}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":1}]}},"support_in_svg":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":19}],"chrome_android":[{"added":25}],"edge":[{"added":17}],"firefox":[{"added":43}],"firefox_android":[{"added":43}],"ie":[{"added":false}],"safari":[{"added":6}],"safari_ios":[{"added":6}]}}},"three_value_syntax":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":12}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":10}],"firefox_android":[{"added":10}],"ie":[{"added":9}],"safari":[{"added":5}],"safari_ios":[{"added":3.2}]}}},"_aliasOf":"transform-origin"},"transform-style":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transform-style","spec_url":"https://drafts.csswg.org/css-transforms-2/#transform-style-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36},{"prefix":"-webkit-","added":12}],"chrome_android":[{"added":36},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":10,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":10,"removed":false}],"ie":[{"added":false}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"transform-style"},"transition":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transition","spec_url":"https://drafts.csswg.org/css-transitions/#transition-shorthand-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":26},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":4,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4,"removed":false}],"ie":[{"added":10},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"gradients":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":10}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"transition_behavior_value":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"_aliasOf":"transition"},"transition-behavior":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transition-behavior","spec_url":"https://drafts.csswg.org/css-transitions-2/#transition-behavior-property","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":117}],"chrome_android":[{"added":117}],"edge":[{"added":117}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"transition-delay":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transition-delay","spec_url":"https://drafts.csswg.org/css-transitions/#transition-delay-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":26},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":4,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4,"removed":false}],"ie":[{"added":10},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":4}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"transition-delay"},"transition-duration":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transition-duration","spec_url":"https://drafts.csswg.org/css-transitions/#transition-duration-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":26},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":4,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4,"removed":false}],"ie":[{"added":10},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"transition-duration"},"transition-property":{"IDENT_value":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":16}],"firefox_android":[{"added":16}],"ie":[{"added":10}],"safari":[{"added":4}],"safari_ios":[{"added":3}]}}},"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transition-property","spec_url":"https://drafts.csswg.org/css-transitions/#transition-property-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":26},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":4,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4,"removed":false}],"ie":[{"added":10},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"_aliasOf":"transition-property"},"transition-timing-function":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/transition-timing-function","spec_url":"https://drafts.csswg.org/css-transitions/#transition-timing-function-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":26},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","version_last":"preview","added":4,"removed":null}],"firefox_android":[{"added":16},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4,"removed":false}],"ie":[{"added":10},{"prefix":"-ms-","added":10}],"safari":[{"added":9},{"prefix":"-webkit-","added":3.1}],"safari_ios":[{"added":9},{"prefix":"-webkit-","added":2}]}},"jump":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":77}],"chrome_android":[{"added":77}],"edge":[{"added":79}],"firefox":[{"added":65}],"firefox_android":[{"added":65}],"ie":[{"added":false}],"safari":[{"added":14}],"safari_ios":[{"added":14}]}}},"_aliasOf":"transition-timing-function"},"translate":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/translate","spec_url":"https://drafts.csswg.org/css-transforms-2/#individual-transforms","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":104}],"chrome_android":[{"added":104}],"edge":[{"added":104}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":14.1}],"safari_ios":[{"added":14.5}]}}},"unicode-bidi":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/unicode-bidi","spec_url":"https://drafts.csswg.org/css-writing-modes/#unicode-bidi","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":2}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}},"isolate":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48},{"prefix":"-webkit-","added":16}],"chrome_android":[{"added":48}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":50},{"prefix":"-moz-","version_last":"53","added":10,"removed":54}],"firefox_android":[{"added":50},{"prefix":"-moz-","version_last":"53","added":10,"removed":54}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":6}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":6}]}},"_aliasOf":"isolate"},"isolate-override":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48}],"chrome_android":[{"added":48}],"edge":[{"added":79}],"firefox":[{"added":50},{"prefix":"-moz-","version_last":"53","added":17,"removed":54}],"firefox_android":[{"added":50},{"prefix":"-moz-","version_last":"53","added":17,"removed":54}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7}]}},"_aliasOf":"isolate-override"},"plaintext":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48}],"chrome_android":[{"added":48}],"edge":[{"added":79}],"firefox":[{"added":50},{"prefix":"-moz-","version_last":"53","added":10,"removed":54}],"firefox_android":[{"added":50},{"prefix":"-moz-","version_last":"53","added":10,"removed":54}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":6}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":6}]}},"_aliasOf":"plaintext"},"-webkit-isolate":{"_aliasOf":"isolate"},"-moz-isolate":{"_aliasOf":"isolate"},"-moz-isolate-override":{"_aliasOf":"isolate-override"},"-webkit-isolate-override":{"_aliasOf":"isolate-override"},"-moz-plaintext":{"_aliasOf":"plaintext"},"-webkit-plaintext":{"_aliasOf":"plaintext"}},"user-modify":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/user-modify","status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"prefix":"-webkit-","added":1}],"chrome_android":[{"prefix":"-webkit-","added":18}],"edge":[{"prefix":"-webkit-","added":12}],"firefox":[{"partial_implementation":true,"prefix":"-moz-","added":1}],"firefox_android":[{"partial_implementation":true,"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":2,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":5}]}},"read-write-plaintext-only":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":5}]}}},"_aliasOf":"user-modify"},"user-select":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/user-select","spec_url":"https://drafts.csswg.org/css-ui/#content-selection","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":54},{"prefix":"-webkit-","added":1}],"chrome_android":[{"added":54},{"prefix":"-webkit-","added":18}],"edge":[{"added":79},{"prefix":"-webkit-","added":12},{"prefix":"-ms-","version_last":"18","added":12,"removed":79}],"firefox":[{"added":69},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":1}],"firefox_android":[{"added":79},{"prefix":"-webkit-","added":49},{"prefix":"-moz-","added":4}],"ie":[{"prefix":"-ms-","added":10}],"safari":[{"prefix":"-webkit-","added":3},{"prefix":"-khtml-","version_last":"2","added":2,"removed":3}],"safari_ios":[{"prefix":"-webkit-","added":3}]}},"all":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":53}],"chrome_android":[{"added":53}],"edge":[{"added":79}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":false}],"safari":[{"added":16}],"safari_ios":[{"added":16}]}}},"auto":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":10}],"safari":[{"added":2}],"safari_ios":[{"added":3}]}}},"contain":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"alternative_name":"element","version_last":"18","added":12,"removed":79}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"alternative_name":"element","added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}},"_aliasOf":"contain"},"none":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":21},{"prefix":"-moz-","version_last":"64","added":1,"removed":65}],"firefox_android":[{"added":21},{"prefix":"-moz-","version_last":"64","added":4,"removed":65}],"ie":[{"added":10}],"safari":[{"added":2}],"safari_ios":[{"added":3}]}},"_aliasOf":"none"},"text":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":10}],"safari":[{"added":2}],"safari_ios":[{"added":3}]}}},"_aliasOf":"user-select","element":{"_aliasOf":"contain"},"-moz-none":{"_aliasOf":"none"}},"vector-effect":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"vertical-align":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/vertical-align","spec_url":"https://drafts.csswg.org/css2/#propdef-vertical-align","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"view-timeline":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/view-timeline","spec_url":"https://drafts.csswg.org/scroll-animations/#view-timeline-shorthand","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":114}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"view-timeline-axis":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/view-timeline-axis","spec_url":"https://drafts.csswg.org/scroll-animations/#view-timeline-axis","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":114}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"view-timeline-inset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/view-timeline-inset","spec_url":"https://drafts.csswg.org/scroll-animations/#view-timeline-inset","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"view-timeline-name":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/view-timeline-name","spec_url":"https://drafts.csswg.org/scroll-animations/#view-timeline-name","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":115}],"chrome_android":[{"added":115}],"edge":[{"added":115}],"firefox":[{"flags":[{"name":"layout.css.scroll-driven-animations.enabled","type":"preference","value_to_set":"true"}],"added":111}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"view-transition-name":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/view-transition-name","spec_url":"https://drafts.csswg.org/css-view-transitions/#view-transition-name-prop","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":111}],"chrome_android":[{"added":111}],"edge":[{"added":111}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]},"tags":["web-features:view-transitions"]}},"visibility":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/visibility","spec_url":"https://drafts.csswg.org/css-display/#visibility","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"collapse":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":10}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}}},"white-space":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/white-space","spec_url":"https://drafts.csswg.org/css-text/#white-space-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"break-spaces":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":76}],"chrome_android":[{"added":76}],"edge":[{"added":79}],"firefox":[{"added":69}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"nowrap":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"pre":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"pre-line":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3.5}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}}},"pre-wrap":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3},{"prefix":"-moz-","version_last":"3.5","added":1,"removed":3.6}],"firefox_android":[{"added":4}],"ie":[{"added":8}],"safari":[{"added":3}],"safari_ios":[{"added":1}]}},"_aliasOf":"pre-wrap"},"shorthand_values":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":114}],"chrome_android":[{"partial_implementation":true,"added":114}],"edge":[{"partial_implementation":true,"added":114}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"svg_support":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"version_last":"18","added":12,"removed":79}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":10}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"textarea_support":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":5.5}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}},"-moz-pre-wrap":{"_aliasOf":"pre-wrap"}},"white-space-collapse":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/white-space-collapse","spec_url":"https://drafts.csswg.org/css-text-4/#white-space-collapsing","status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"partial_implementation":true,"added":114}],"chrome_android":[{"partial_implementation":true,"added":114}],"edge":[{"partial_implementation":true,"added":114}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":null}],"safari_ios":[{"added":false}]}}},"widows":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/widows","spec_url":["https://drafts.csswg.org/css-break/#widows-orphans","https://drafts.csswg.org/css-multicol/#filling-columns"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":25}],"chrome_android":[{"added":25}],"edge":[{"added":12}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":8}],"safari":[{"added":1.3}],"safari_ios":[{"added":1}]}}},"width":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/width","spec_url":["https://drafts.csswg.org/css-sizing-4/#width-height-keywords","https://drafts.csswg.org/css-sizing-4/#sizing-values"],"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"animatable":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":26}],"chrome_android":[{"added":26}],"edge":[{"added":12}],"firefox":[{"added":16}],"firefox_android":[{"added":16}],"ie":[{"added":11}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"fit-content":{"__compat":{"spec_url":"https://drafts.csswg.org/css-sizing-4/#valdef-width-fit-content","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":22},{"alternative_name":"intrinsic","version_last":"47","added":1,"removed":48}],"chrome_android":[{"added":46},{"prefix":"-webkit-","added":25},{"alternative_name":"intrinsic","version_last":"47","added":18,"removed":48}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":94},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":94},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"prefix":"-webkit-","added":7},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"fit-content"},"fit-content_function":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"flags":[{"name":"layout.css.fit-content-function.enabled","type":"preference"}],"added":91}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"max-content":{"__compat":{"spec_url":"https://drafts.csswg.org/css-sizing-3/#valdef-width-max-content","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"prefix":"-webkit-","added":22}],"chrome_android":[{"added":46}],"edge":[{"added":79},{"prefix":"-webkit-","added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"alternative_name":"intrinsic","added":2}],"safari_ios":[{"added":11},{"alternative_name":"intrinsic","added":1}]}},"_aliasOf":"max-content"},"min-content":{"__compat":{"spec_url":"https://drafts.csswg.org/css-sizing-3/#valdef-width-min-content","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":46},{"alternative_name":"min-intrinsic","version_last":"47","added":1,"removed":48}],"chrome_android":[{"added":46},{"alternative_name":"min-intrinsic","version_last":"47","added":18,"removed":48}],"edge":[{"added":79}],"firefox":[{"added":66},{"prefix":"-moz-","added":3}],"firefox_android":[{"added":66},{"prefix":"-moz-","added":4}],"ie":[{"added":false}],"safari":[{"added":11},{"alternative_name":"min-intrinsic","added":2}],"safari_ios":[{"added":11},{"alternative_name":"min-intrinsic","added":1}]}},"_aliasOf":"min-content"},"stretch":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"alternative_name":"-webkit-fill-available","added":22}],"chrome_android":[{"alternative_name":"-webkit-fill-available","added":25}],"edge":[{"alternative_name":"-webkit-fill-available","added":79}],"firefox":[{"alternative_name":"-moz-available","added":3}],"firefox_android":[{"alternative_name":"-moz-available","added":4}],"ie":[{"added":false}],"safari":[{"alternative_name":"-webkit-fill-available","added":7}],"safari_ios":[{"alternative_name":"-webkit-fill-available","added":7}]}},"_aliasOf":"stretch"},"-webkit-fit-content":{"_aliasOf":"fit-content"},"intrinsic":{"_aliasOf":"max-content"},"-moz-fit-content":{"_aliasOf":"fit-content"},"-webkit-max-content":{"_aliasOf":"max-content"},"-moz-max-content":{"_aliasOf":"max-content"},"min-intrinsic":{"_aliasOf":"min-content"},"-moz-min-content":{"_aliasOf":"min-content"},"-webkit-fill-available":{"_aliasOf":"stretch"},"-moz-available":{"_aliasOf":"stretch"}},"will-change":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/will-change","spec_url":"https://drafts.csswg.org/css-will-change/#will-change","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":36}],"chrome_android":[{"added":36}],"edge":[{"added":79}],"firefox":[{"added":36}],"firefox_android":[{"added":36}],"ie":[{"added":false}],"safari":[{"added":9.1}],"safari_ios":[{"added":9.3}]}}},"word-break":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/word-break","spec_url":"https://drafts.csswg.org/css-text/#word-break-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":5.5},{"prefix":"-ms-","added":8}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}},"auto-phrase":{"__compat":{"status":{"deprecated":false,"experimental":true,"standard_track":true},"support":{"chrome":[{"added":119}],"chrome_android":[{"added":119}],"edge":[{"added":119}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"break-word":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":79}],"firefox":[{"added":67}],"firefox_android":[{"added":67}],"ie":[{"added":false}],"safari":[{"added":3}],"safari_ios":[{"added":2}]}}},"keep-all":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":44}],"chrome_android":[{"added":44}],"edge":[{"added":12}],"firefox":[{"added":15}],"firefox_android":[{"added":15}],"ie":[{"added":5.5}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}}},"_aliasOf":"word-break"},"word-spacing":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/word-spacing","spec_url":"https://drafts.csswg.org/css-text/#word-spacing-property","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":6}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"percentages":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":45}],"firefox_android":[{"added":45}],"ie":[{"added":false}],"safari":[{"added":7}],"safari_ios":[{"added":7}]}}},"svg":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":9}],"safari":[{"added":5.1}],"safari_ios":[{"added":5}]}}}},"word-wrap":{"_aliasOf":"word-wrap"},"writing-mode":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/writing-mode","spec_url":"https://drafts.csswg.org/css-writing-modes/#block-flow","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48},{"prefix":"-webkit-","added":8}],"chrome_android":[{"added":48},{"prefix":"-webkit-","added":18}],"edge":[{"added":12},{"prefix":"-webkit-","added":12}],"firefox":[{"added":41}],"firefox_android":[{"added":41}],"ie":[{"added":9},{"prefix":"-ms-","added":9}],"safari":[{"added":10.1},{"prefix":"-webkit-","added":5.1}],"safari_ios":[{"added":10.3},{"prefix":"-webkit-","added":5}]}},"horizontal_vertical_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48}],"chrome_android":[{"added":48}],"edge":[{"added":79}],"firefox":[{"added":43}],"firefox_android":[{"added":43}],"ie":[{"added":false}],"safari":[{"added":9}],"safari_ios":[{"added":9}]}}},"sideways_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":false}],"chrome_android":[{"added":false}],"edge":[{"added":false}],"firefox":[{"added":43}],"firefox_android":[{"added":43}],"ie":[{"added":false}],"safari":[{"added":false}],"safari_ios":[{"added":false}]}}},"svg_values":{"__compat":{"status":{"deprecated":true,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":48}],"chrome_android":[{"added":48}],"edge":[{"added":12}],"firefox":[{"added":43}],"firefox_android":[{"added":43}],"ie":[{"added":9}],"safari":[{"added":10.1}],"safari_ios":[{"added":10.3}]}}},"_aliasOf":"writing-mode"},"x":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#X","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"y":{"__compat":{"spec_url":"https://svgwg.org/svg2-draft/geometry.html#Y","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":80}],"chrome_android":[{"added":80}],"edge":[{"added":80}],"firefox":[{"added":72}],"firefox_android":[{"added":79}],"ie":[{"added":false}],"safari":[{"added":13.1}],"safari_ios":[{"added":13.4}]}}},"z-index":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/z-index","spec_url":"https://drafts.csswg.org/css2/#z-index","status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":1}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}},"negative_values":{"__compat":{"status":{"deprecated":false,"experimental":false,"standard_track":true},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"added":3}],"firefox_android":[{"added":4}],"ie":[{"added":4}],"safari":[{"added":1}],"safari_ios":[{"added":1}]}}}},"zoom":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/zoom","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"added":1}],"chrome_android":[{"added":18}],"edge":[{"added":12}],"firefox":[{"flags":[{"name":"layout.css.zoom.enabled","type":"preference","value_to_set":"true"}],"impl_url":"https://bugzil.la/390936","added":null}],"firefox_android":[{"added":false}],"ie":[{"added":5.5}],"safari":[{"added":3.1}],"safari_ios":[{"added":3}]}},"reset":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/CSS/zoom#Values","status":{"deprecated":false,"experimental":false,"standard_track":false},"support":{"chrome":[{"version_last":"58","added":1,"removed":59}],"chrome_android":[{"version_last":"58","added":18,"removed":59}],"edge":[{"added":false}],"firefox":[{"added":false}],"firefox_android":[{"added":false}],"ie":[{"added":false}],"safari":[{"added":3.1}],"safari_ios":[{"added":3}]}}}},"-webkit-align-content":{"_aliasOf":"align-content"},"-webkit-align-items":{"_aliasOf":"align-items"},"-webkit-align-self":{"_aliasOf":"align-self"},"-webkit-alt":{"_aliasOf":"alt"},"-webkit-animation":{"_aliasOf":"animation"},"-moz-animation":{"_aliasOf":"animation"},"-o-animation":{"_aliasOf":"animation"},"-webkit-animation-delay":{"_aliasOf":"animation-delay"},"-moz-animation-delay":{"_aliasOf":"animation-delay"},"-o-animation-delay":{"_aliasOf":"animation-delay"},"-webkit-animation-direction":{"_aliasOf":"animation-direction"},"-moz-animation-direction":{"_aliasOf":"animation-direction"},"-o-animation-direction":{"_aliasOf":"animation-direction"},"-webkit-animation-duration":{"_aliasOf":"animation-duration"},"-moz-animation-duration":{"_aliasOf":"animation-duration"},"-o-animation-duration":{"_aliasOf":"animation-duration"},"-webkit-animation-fill-mode":{"_aliasOf":"animation-fill-mode"},"-moz-animation-fill-mode":{"_aliasOf":"animation-fill-mode"},"-o-animation-fill-mode":{"_aliasOf":"animation-fill-mode"},"-webkit-animation-iteration-count":{"_aliasOf":"animation-iteration-count"},"-moz-animation-iteration-count":{"_aliasOf":"animation-iteration-count"},"-o-animation-iteration-count":{"_aliasOf":"animation-iteration-count"},"-webkit-animation-name":{"_aliasOf":"animation-name"},"-moz-animation-name":{"_aliasOf":"animation-name"},"-o-animation-name":{"_aliasOf":"animation-name"},"-webkit-animation-play-state":{"_aliasOf":"animation-play-state"},"-moz-animation-play-state":{"_aliasOf":"animation-play-state"},"-o-animation-play-state":{"_aliasOf":"animation-play-state"},"-webkit-animation-timing-function":{"_aliasOf":"animation-timing-function"},"-moz-animation-timing-function":{"_aliasOf":"animation-timing-function"},"-o-animation-timing-function":{"_aliasOf":"animation-timing-function"},"-webkit-appearance":{"_aliasOf":"appearance"},"-moz-appearance":{"_aliasOf":"appearance"},"-webkit-backdrop-filter":{"_aliasOf":"backdrop-filter"},"-webkit-backface-visibility":{"_aliasOf":"backface-visibility"},"-moz-backface-visibility":{"_aliasOf":"backface-visibility"},"-webkit-background-clip":{"_aliasOf":"background-clip"},"-moz-background-clip":{"_aliasOf":"background-clip"},"-webkit-background-origin":{"_aliasOf":"background-origin"},"-moz-background-origin":{"_aliasOf":"background-origin"},"-webkit-background-size":{"_aliasOf":"background-size"},"-moz-background-size":{"_aliasOf":"background-size"},"-o-background-size":{"_aliasOf":"background-size"},"-webkit-border-bottom-left-radius":{"_aliasOf":"border-bottom-left-radius"},"-moz-border-radius-bottomleft":{"_aliasOf":"border-bottom-left-radius"},"-webkit-border-bottom-right-radius":{"_aliasOf":"border-bottom-right-radius"},"-moz-border-radius-bottomright":{"_aliasOf":"border-bottom-right-radius"},"-webkit-border-image":{"_aliasOf":"border-image"},"-moz-border-image":{"_aliasOf":"border-image"},"-o-border-image":{"_aliasOf":"border-image"},"-webkit-border-image-slice":{"_aliasOf":"border-image-slice"},"-moz-border-end-color":{"_aliasOf":"border-inline-end-color"},"-moz-border-end-style":{"_aliasOf":"border-inline-end-style"},"-moz-border-end-width":{"_aliasOf":"border-inline-end-width"},"-moz-border-start-color":{"_aliasOf":"border-inline-start-color"},"-moz-border-start-style":{"_aliasOf":"border-inline-start-style"},"-webkit-border-radius":{"_aliasOf":"border-radius"},"-moz-border-radius":{"_aliasOf":"border-radius"},"-webkit-border-top-left-radius":{"_aliasOf":"border-top-left-radius"},"-moz-border-radius-topleft":{"_aliasOf":"border-top-left-radius"},"-webkit-border-top-right-radius":{"_aliasOf":"border-top-right-radius"},"-moz-border-radius-topright":{"_aliasOf":"border-top-right-radius"},"-webkit-box-align":{"_aliasOf":"box-align"},"-moz-box-align":{"_aliasOf":"box-align"},"-khtml-box-align":{"_aliasOf":"box-align"},"-webkit-box-decoration-break":{"_aliasOf":"box-decoration-break"},"-moz-background-inline-policy":{"_aliasOf":"box-decoration-break"},"-webkit-box-direction":{"_aliasOf":"box-direction"},"-moz-box-direction":{"_aliasOf":"box-direction"},"-khtml-box-direction":{"_aliasOf":"box-direction"},"-webkit-box-flex":{"_aliasOf":"box-flex"},"-moz-box-flex":{"_aliasOf":"box-flex"},"-khtml-box-flex":{"_aliasOf":"box-flex"},"-webkit-box-flex-group":{"_aliasOf":"box-flex-group"},"-khtml-box-flex-group":{"_aliasOf":"box-flex-group"},"-webkit-box-lines":{"_aliasOf":"box-lines"},"-khtml-box-lines":{"_aliasOf":"box-lines"},"-webkit-box-ordinal-group":{"_aliasOf":"box-ordinal-group"},"-moz-box-ordinal-group":{"_aliasOf":"box-ordinal-group"},"-khtml-box-ordinal-group":{"_aliasOf":"box-ordinal-group"},"-webkit-box-orient":{"_aliasOf":"box-orient"},"-moz-box-orient":{"_aliasOf":"box-orient"},"-khtml-box-orient":{"_aliasOf":"box-orient"},"-webkit-box-pack":{"_aliasOf":"box-pack"},"-moz-box-pack":{"_aliasOf":"box-pack"},"-khtml-box-pack":{"_aliasOf":"box-pack"},"-webkit-box-shadow":{"_aliasOf":"box-shadow"},"-moz-box-shadow":{"_aliasOf":"box-shadow"},"-webkit-box-sizing":{"_aliasOf":"box-sizing"},"-moz-box-sizing":{"_aliasOf":"box-sizing"},"-webkit-clip-path":{"_aliasOf":"clip-path"},"-webkit-column-count":{"_aliasOf":"column-count"},"-moz-column-count":{"_aliasOf":"column-count"},"-moz-column-fill":{"_aliasOf":"column-fill"},"-webkit-column-fill":{"_aliasOf":"column-fill"},"-webkit-column-rule":{"_aliasOf":"column-rule"},"-moz-column-rule":{"_aliasOf":"column-rule"},"-webkit-column-rule-color":{"_aliasOf":"column-rule-color"},"-moz-column-rule-color":{"_aliasOf":"column-rule-color"},"-webkit-column-rule-style":{"_aliasOf":"column-rule-style"},"-moz-column-rule-style":{"_aliasOf":"column-rule-style"},"-webkit-column-rule-width":{"_aliasOf":"column-rule-width"},"-moz-column-rule-width":{"_aliasOf":"column-rule-width"},"-webkit-column-span":{"_aliasOf":"column-span"},"-webkit-column-width":{"_aliasOf":"column-width"},"-moz-column-width":{"_aliasOf":"column-width"},"-webkit-columns":{"_aliasOf":"columns"},"-moz-columns":{"_aliasOf":"columns"},"-webkit-filter":{"_aliasOf":"filter"},"-webkit-flex":{"_aliasOf":"flex"},"-ms-flex":{"_aliasOf":"flex"},"-webkit-flex-basis":{"_aliasOf":"flex-basis"},"-webkit-flex-direction":{"_aliasOf":"flex-direction"},"-ms-flex-direction":{"_aliasOf":"flex-direction"},"-webkit-flex-flow":{"_aliasOf":"flex-flow"},"-webkit-flex-grow":{"_aliasOf":"flex-grow"},"-ms-flex-positive":{"_aliasOf":"flex-grow"},"-webkit-flex-shrink":{"_aliasOf":"flex-shrink"},"-webkit-flex-wrap":{"_aliasOf":"flex-wrap"},"-webkit-font-feature-settings":{"_aliasOf":"font-feature-settings"},"-moz-font-feature-settings":{"_aliasOf":"font-feature-settings"},"-webkit-font-kerning":{"_aliasOf":"font-kerning"},"-moz-font-language-override":{"_aliasOf":"font-language-override"},"-webkit-font-smoothing":{"_aliasOf":"font-smooth"},"-moz-osx-font-smoothing":{"_aliasOf":"font-smooth"},"-webkit-font-variant-ligatures":{"_aliasOf":"font-variant-ligatures"},"-ms-high-contrast-adjust":{"_aliasOf":"forced-color-adjust"},"-ms-grid-columns":{"_aliasOf":"grid-template-columns"},"-ms-grid-rows":{"_aliasOf":"grid-template-rows"},"-webkit-hyphens":{"_aliasOf":"hyphens"},"-ms-hyphens":{"_aliasOf":"hyphens"},"-moz-hyphens":{"_aliasOf":"hyphens"},"-ms-ime-mode":{"_aliasOf":"ime-mode"},"offset-block":{"_aliasOf":"inset-block"},"offset-block-end":{"_aliasOf":"inset-block-end"},"offset-block-start":{"_aliasOf":"inset-block-start"},"offset-inline":{"_aliasOf":"inset-inline"},"offset-inline-end":{"_aliasOf":"inset-inline-end"},"offset-inline-start":{"_aliasOf":"inset-inline-start"},"-webkit-justify-content":{"_aliasOf":"justify-content"},"-webkit-line-break":{"_aliasOf":"line-break"},"-ms-line-break":{"_aliasOf":"line-break"},"-khtml-line-break":{"_aliasOf":"line-break"},"-webkit-margin-end":{"_aliasOf":"margin-inline-end"},"-moz-margin-end":{"_aliasOf":"margin-inline-end"},"-webkit-margin-start":{"_aliasOf":"margin-inline-start"},"-moz-margin-start":{"_aliasOf":"margin-inline-start"},"-webkit-mask":{"_aliasOf":"mask"},"-webkit-mask-clip":{"_aliasOf":"mask-clip"},"-webkit-mask-image":{"_aliasOf":"mask-image"},"-webkit-mask-origin":{"_aliasOf":"mask-origin"},"-webkit-mask-position":{"_aliasOf":"mask-position"},"-webkit-mask-repeat":{"_aliasOf":"mask-repeat"},"-webkit-mask-size":{"_aliasOf":"mask-size"},"-webkit-max-inline-size":{"_aliasOf":"max-inline-size"},"-o-object-fit":{"_aliasOf":"object-fit"},"-o-object-position":{"_aliasOf":"object-position"},"motion":{"_aliasOf":"offset"},"motion-distance":{"_aliasOf":"offset-distance"},"motion-path":{"_aliasOf":"offset-path"},"offset-rotation":{"_aliasOf":"offset-rotate"},"motion-rotation":{"_aliasOf":"offset-rotate"},"-moz-opacity":{"_aliasOf":"opacity"},"-khtml-opacity":{"_aliasOf":"opacity"},"-webkit-order":{"_aliasOf":"order"},"-ms-order":{"_aliasOf":"order"},"-moz-outline":{"_aliasOf":"outline"},"-moz-outline-color":{"_aliasOf":"outline-color"},"-moz-outline-style":{"_aliasOf":"outline-style"},"-moz-outline-width":{"_aliasOf":"outline-width"},"-ms-overflow-x":{"_aliasOf":"overflow-x"},"-ms-overflow-y":{"_aliasOf":"overflow-y"},"-webkit-padding-end":{"_aliasOf":"padding-inline-end"},"-moz-padding-end":{"_aliasOf":"padding-inline-end"},"-webkit-padding-start":{"_aliasOf":"padding-inline-start"},"-moz-padding-start":{"_aliasOf":"padding-inline-start"},"-webkit-perspective":{"_aliasOf":"perspective"},"-moz-perspective":{"_aliasOf":"perspective"},"-webkit-perspective-origin":{"_aliasOf":"perspective-origin"},"-moz-perspective-origin":{"_aliasOf":"perspective-origin"},"-webkit-print-color-adjust":{"_aliasOf":"print-color-adjust"},"-webkit-ruby-position":{"_aliasOf":"ruby-position"},"scroll-snap-margin":{"_aliasOf":"scroll-margin"},"scroll-snap-margin-bottom":{"_aliasOf":"scroll-margin-bottom"},"scroll-snap-margin-left":{"_aliasOf":"scroll-margin-left"},"scroll-snap-margin-right":{"_aliasOf":"scroll-margin-right"},"scroll-snap-margin-top":{"_aliasOf":"scroll-margin-top"},"-ms-scroll-snap-type":{"_aliasOf":"scroll-snap-type"},"-webkit-scroll-snap-type":{"_aliasOf":"scroll-snap-type"},"-ms-scrollbar-3dlight-color":{"_aliasOf":"scrollbar-3dlight-color"},"-ms-scrollbar-arrow-color":{"_aliasOf":"scrollbar-arrow-color"},"-ms-scrollbar-base-color":{"_aliasOf":"scrollbar-base-color"},"-ms-scrollbar-darkshadow-color":{"_aliasOf":"scrollbar-darkshadow-color"},"-ms-scrollbar-face-color":{"_aliasOf":"scrollbar-face-color"},"-ms-scrollbar-highlight-color":{"_aliasOf":"scrollbar-highlight-color"},"-ms-scrollbar-shadow-color":{"_aliasOf":"scrollbar-shadow-color"},"-webkit-shape-margin":{"_aliasOf":"shape-margin"},"-moz-tab-size":{"_aliasOf":"tab-size"},"-o-tab-size":{"_aliasOf":"tab-size"},"-moz-text-align-last":{"_aliasOf":"text-align-last"},"-ms-text-combine-horizontal":{"_aliasOf":"text-combine-upright"},"-moz-text-decoration-color":{"_aliasOf":"text-decoration-color"},"-webkit-text-decoration-color":{"_aliasOf":"text-decoration-color"},"-moz-text-decoration-line":{"_aliasOf":"text-decoration-line"},"-webkit-text-decoration-line":{"_aliasOf":"text-decoration-line"},"-moz-text-decoration-style":{"_aliasOf":"text-decoration-style"},"-webkit-text-decoration-style":{"_aliasOf":"text-decoration-style"},"-webkit-text-emphasis":{"_aliasOf":"text-emphasis"},"-webkit-text-emphasis-color":{"_aliasOf":"text-emphasis-color"},"-webkit-text-emphasis-position":{"_aliasOf":"text-emphasis-position"},"-webkit-text-emphasis-style":{"_aliasOf":"text-emphasis-style"},"-webkit-text-orientation":{"_aliasOf":"text-orientation"},"-ms-text-overflow":{"_aliasOf":"text-overflow"},"-o-text-overflow":{"_aliasOf":"text-overflow"},"-webkit-text-size-adjust":{"_aliasOf":"text-size-adjust"},"-moz-text-size-adjust":{"_aliasOf":"text-size-adjust"},"-webkit-text-underline-position":{"_aliasOf":"text-underline-position"},"-ms-touch-action":{"_aliasOf":"touch-action"},"-webkit-transform":{"_aliasOf":"transform"},"-moz-transform":{"_aliasOf":"transform"},"-ms-transform":{"_aliasOf":"transform"},"-o-transform":{"_aliasOf":"transform"},"-webkit-transform-origin":{"_aliasOf":"transform-origin"},"-moz-transform-origin":{"_aliasOf":"transform-origin"},"-ms-transform-origin":{"_aliasOf":"transform-origin"},"-o-transform-origin":{"_aliasOf":"transform-origin"},"-webkit-transform-style":{"_aliasOf":"transform-style"},"-moz-transform-style":{"_aliasOf":"transform-style"},"-webkit-transition":{"_aliasOf":"transition"},"-moz-transition":{"_aliasOf":"transition"},"-ms-transition":{"_aliasOf":"transition"},"-o-transition":{"_aliasOf":"transition"},"-webkit-transition-delay":{"_aliasOf":"transition-delay"},"-moz-transition-delay":{"_aliasOf":"transition-delay"},"-ms-transition-delay":{"_aliasOf":"transition-delay"},"-o-transition-delay":{"_aliasOf":"transition-delay"},"-webkit-transition-duration":{"_aliasOf":"transition-duration"},"-moz-transition-duration":{"_aliasOf":"transition-duration"},"-ms-transition-duration":{"_aliasOf":"transition-duration"},"-o-transition-duration":{"_aliasOf":"transition-duration"},"-webkit-transition-property":{"_aliasOf":"transition-property"},"-moz-transition-property":{"_aliasOf":"transition-property"},"-ms-transition-property":{"_aliasOf":"transition-property"},"-o-transition-property":{"_aliasOf":"transition-property"},"-webkit-transition-timing-function":{"_aliasOf":"transition-timing-function"},"-moz-transition-timing-function":{"_aliasOf":"transition-timing-function"},"-ms-transition-timing-function":{"_aliasOf":"transition-timing-function"},"-o-transition-timing-function":{"_aliasOf":"transition-timing-function"},"-moz-user-modify":{"_aliasOf":"user-modify"},"-khtml-user-modify":{"_aliasOf":"user-modify"},"-webkit-user-select":{"_aliasOf":"user-select"},"-ms-user-select":{"_aliasOf":"user-select"},"-moz-user-select":{"_aliasOf":"user-select"},"-khtml-user-select":{"_aliasOf":"user-select"},"-ms-word-break":{"_aliasOf":"word-break"},"-webkit-writing-mode":{"_aliasOf":"writing-mode"},"-ms-writing-mode":{"_aliasOf":"writing-mode"}} \ No newline at end of file
diff --git a/devtools/shared/compatibility/dataset/moz.build b/devtools/shared/compatibility/dataset/moz.build
new file mode 100644
index 0000000000..6fb6046216
--- /dev/null
+++ b/devtools/shared/compatibility/dataset/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ "css-properties.json",
+)
diff --git a/devtools/shared/compatibility/helpers.js b/devtools/shared/compatibility/helpers.js
new file mode 100644
index 0000000000..d69e92a929
--- /dev/null
+++ b/devtools/shared/compatibility/helpers.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 might be required from a node script (./bin/update.js), so don't use
+// Chrome API here.
+
+/**
+ * Return the compatibility table from given compatNode and specified terms.
+ * For example, if the terms is ["background-color"],
+ * this function returns compatNode["background-color"].__compat.
+ *
+ * @return {Object} compatibility table
+ * {
+ * description: {String} Description of this compatibility table.
+ * mdn_url: {String} Document in the MDN.
+ * support: {
+ * $browserName: {String} $browserName is such as firefox, firefox_android and so on.
+ * [
+ * {
+ * added: {String}
+ * The version this feature was added.
+ * removed: {String} Optional.
+ * The version this feature was removed. Optional.
+ * prefix: {String} Optional.
+ * The prefix this feature is needed such as "-moz-".
+ * alternative_name: {String} Optional.
+ * The alternative name of this feature such as "-moz-osx-font-smoothing" of "font-smooth".
+ * notes: {String} Optional.
+ * A simple note for this support.
+ * },
+ * ...
+ * ],
+ * },
+ * status: {
+ * experimental: {Boolean} If true, this feature is experimental.
+ * standard_track: {Boolean}, If true, this feature is on the standard track.
+ * deprecated: {Boolean} If true, this feature is deprecated.
+ * }
+ * }
+ */
+function getCompatTable(compatNode, terms) {
+ let targetNode = getCompatNode(compatNode, terms);
+
+ if (!targetNode) {
+ return null;
+ }
+
+ if (!targetNode.__compat) {
+ for (const field in targetNode) {
+ // TODO: We don't have a way to know the context for now.
+ // Thus, use first context node as the compat table.
+ // e.g. flex_context of align-item
+ // https://github.com/mdn/browser-compat-data/blob/master/css/properties/align-items.json#L5
+ if (field.endsWith("_context")) {
+ targetNode = targetNode[field];
+ break;
+ }
+ }
+ }
+
+ return targetNode.__compat;
+}
+
+/**
+ * Return a compatibility node which is target for `terms` parameter from `compatNode`
+ * parameter. For example, when check `background-clip: content-box;`, the `terms` will
+ * be ["background-clip", "content-box"]. Then, follow the name of terms from the
+ * compatNode node, return the target node. Although this function actually do more
+ * complex a bit, if it says simply, returns a node of
+ * compatNode["background-clip"]["content-box""] .
+ */
+function getCompatNode(compatNode, terms) {
+ for (const term of terms) {
+ compatNode = getChildCompatNode(compatNode, term);
+ if (!compatNode) {
+ return null;
+ }
+ }
+
+ return compatNode;
+}
+
+function getChildCompatNode(compatNode, term) {
+ term = term.toLowerCase();
+
+ let child = null;
+ for (const field in compatNode) {
+ if (field.toLowerCase() === term) {
+ child = compatNode[field];
+ break;
+ }
+ }
+
+ if (!child) {
+ return null;
+ }
+
+ if (child._aliasOf) {
+ // If the node is an alias, returns the node the alias points.
+ child = compatNode[child._aliasOf];
+ }
+
+ return child;
+}
+
+module.exports = {
+ getCompatNode,
+ getCompatTable,
+};
diff --git a/devtools/shared/compatibility/moz.build b/devtools/shared/compatibility/moz.build
new file mode 100644
index 0000000000..aa37d28738
--- /dev/null
+++ b/devtools/shared/compatibility/moz.build
@@ -0,0 +1,18 @@
+# -*- 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 += [
+ "dataset",
+]
+
+DevToolsModules(
+ "compatibility-user-settings.js",
+ "constants.js",
+ "helpers.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Inspector: Compatibility")
diff --git a/devtools/shared/compatibility/package.json b/devtools/shared/compatibility/package.json
new file mode 100644
index 0000000000..d4a93d45bf
--- /dev/null
+++ b/devtools/shared/compatibility/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "devtools-mdn-compat-data",
+ "version": "1.0.0",
+ "description": "Handle MDN compatibility data used by the Compatibility",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "update": "node bin/update"
+ },
+ "author": "",
+ "devDependencies": {
+ "@mdn/browser-compat-data": "^"
+ }
+}
diff --git a/devtools/shared/constants.js b/devtools/shared/constants.js
new file mode 100644
index 0000000000..62381fbb35
--- /dev/null
+++ b/devtools/shared/constants.js
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Constants used in various panels, shared between client and the server.
+ */
+
+/* Accessibility Panel ====================================================== */
+
+// List of audit types.
+const AUDIT_TYPE = {
+ CONTRAST: "CONTRAST",
+ KEYBOARD: "KEYBOARD",
+ TEXT_LABEL: "TEXT_LABEL",
+};
+
+// Types of issues grouped by audit types.
+const ISSUE_TYPE = {
+ [AUDIT_TYPE.KEYBOARD]: {
+ // Focusable accessible objects have no semantics.
+ FOCUSABLE_NO_SEMANTICS: "FOCUSABLE_NO_SEMANTICS",
+ // Tab index greater than 0 is provided.
+ FOCUSABLE_POSITIVE_TABINDEX: "FOCUSABLE_POSITIVE_TABINDEX",
+ // Interactive accesible objects do not have an associated action.
+ INTERACTIVE_NO_ACTION: "INTERACTIVE_NO_ACTION",
+ // Interative accessible objcets are not focusable.
+ INTERACTIVE_NOT_FOCUSABLE: "INTERACTIVE_NOT_FOCUSABLE",
+ // Accessible objects can only be interacted with a mouse.
+ MOUSE_INTERACTIVE_ONLY: "MOUSE_INTERACTIVE_ONLY",
+ // Focusable accessible objects have no focus styling.
+ NO_FOCUS_VISIBLE: "NO_FOCUS_VISIBLE",
+ },
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ // <AREA> name is provided via "alt" attribute.
+ AREA_NO_NAME_FROM_ALT: "AREA_NO_NAME_FROM_ALT",
+ // Dialog name is not provided.
+ DIALOG_NO_NAME: "DIALOG_NO_NAME",
+ // Document title is not provided.
+ DOCUMENT_NO_TITLE: "DOCUMENT_NO_TITLE",
+ // <EMBED> name is not provided.
+ EMBED_NO_NAME: "EMBED_NO_NAME",
+ // <FIGURE> name is not provided.
+ FIGURE_NO_NAME: "FIGURE_NO_NAME",
+ // <FIELDSET> name is not provided.
+ FORM_FIELDSET_NO_NAME: "FORM_FIELDSET_NO_NAME",
+ // <FIELDSET> name is not provided via <LEGEND> element.
+ FORM_FIELDSET_NO_NAME_FROM_LEGEND: "FORM_FIELDSET_NO_NAME_FROM_LEGEND",
+ // Form element's name is not provided.
+ FORM_NO_NAME: "FORM_NO_NAME",
+ // Form element's name is not visible.
+ FORM_NO_VISIBLE_NAME: "FORM_NO_VISIBLE_NAME",
+ // <OPTGROUP> name is not provided via "label" attribute.
+ FORM_OPTGROUP_NO_NAME_FROM_LABEL: "FORM_OPTGROUP_NO_NAME_FROM_LABEL",
+ // <FRAME> name is not provided.
+ FRAME_NO_NAME: "FRAME_NO_NAME",
+ // <H{1, 2, ...}> has no content.
+ HEADING_NO_CONTENT: "HEADING_NO_CONTENT",
+ // <H{1, 2, ...}> name is not provided.
+ HEADING_NO_NAME: "HEADING_NO_NAME",
+ // <IFRAME> name is not provided via "title" attribute.
+ IFRAME_NO_NAME_FROM_TITLE: "IFRAME_NO_NAME_FROM_TITLE",
+ // <IMG> name is not provided (including empty name).
+ IMAGE_NO_NAME: "IMAGE_NO_NAME",
+ // Interactive element's name is not provided.
+ INTERACTIVE_NO_NAME: "INTERACTIVE_NO_NAME",
+ // <MGLYPH> name is no provided.
+ MATHML_GLYPH_NO_NAME: "MATHML_GLYPH_NO_NAME",
+ // Toolbar's name is not provided when more than one toolbar is present.
+ TOOLBAR_NO_NAME: "TOOLBAR_NO_NAME",
+ },
+};
+
+// Constants associated with WCAG guidelines score system.
+const SCORES = {
+ // Satisfies WCAG AA guidelines.
+ AA: "AA",
+ // Satisfies WCAG AAA guidelines.
+ AAA: "AAA",
+ // Elevates accessibility experience.
+ BEST_PRACTICES: "BEST_PRACTICES",
+ // Does not satisfy the baseline WCAG guidelines.
+ FAIL: "FAIL",
+ // Partially satisfies the WCAG AA guidelines.
+ WARNING: "WARNING",
+};
+
+// List of simulation types.
+const SIMULATION_TYPE = {
+ // No red color blindness
+ PROTANOPIA: "PROTANOPIA",
+ // No green color blindness
+ DEUTERANOPIA: "DEUTERANOPIA",
+ // No blue color blindness
+ TRITANOPIA: "TRITANOPIA",
+ // Absense of color vision
+ ACHROMATOPSIA: "ACHROMATOPSIA",
+ // Low contrast
+ CONTRAST_LOSS: "CONTRAST_LOSS",
+};
+
+/* Compatibility Panel ====================================================== */
+
+const COMPATIBILITY_ISSUE_TYPE = {
+ CSS_PROPERTY: "CSS_PROPERTY",
+ CSS_PROPERTY_ALIASES: "CSS_PROPERTY_ALIASES",
+};
+
+/* Style Editor ============================================================= */
+
+// The PageStyle actor flattens the DOM CSS objects a little bit, merging
+// Rules and their Styles into one actor. For elements (which have a style
+// but no associated rule) we fake a rule with the following style id.
+// This `id` is intended to be used instead of a regular CSSRule Type constant.
+// See https://developer.mozilla.org/en-US/docs/Web/API/CSSRule#Type_constants
+const ELEMENT_STYLE = 100;
+
+/* WebConsole Panel ========================================================= */
+
+const MESSAGE_CATEGORY = {
+ CSS_PARSER: "CSS Parser",
+};
+
+/* Debugger ============================================================= */
+
+// Map protocol pause "why" reason to a valid L10N key (in devtools/shared/locales/en-US/debugger-paused-reasons.ftl)
+const DEBUGGER_PAUSED_REASONS_L10N_MAPPING = {
+ debuggerStatement: "whypaused-debugger-statement",
+ breakpoint: "whypaused-breakpoint",
+ exception: "whypaused-exception",
+ resumeLimit: "whypaused-resume-limit",
+ breakpointConditionThrown: "whypaused-breakpoint-condition-thrown",
+ eventBreakpoint: "whypaused-event-breakpoint",
+ getWatchpoint: "whypaused-get-watchpoint",
+ setWatchpoint: "whypaused-set-watchpoint",
+ mutationBreakpoint: "whypaused-mutation-breakpoint",
+ interrupted: "whypaused-interrupted",
+
+ // V8
+ DOM: "whypaused-breakpoint",
+ EventListener: "whypaused-pause-on-dom-events",
+ XHR: "whypaused-xhr",
+ promiseRejection: "whypaused-promise-rejection",
+ assert: "whypaused-assert",
+ debugCommand: "whypaused-debug-command",
+ other: "whypaused-other",
+};
+
+/* Exports ============================================================= */
+
+module.exports = {
+ accessibility: {
+ AUDIT_TYPE,
+ ISSUE_TYPE,
+ SCORES,
+ SIMULATION_TYPE,
+ },
+ COMPATIBILITY_ISSUE_TYPE,
+ DEBUGGER_PAUSED_REASONS_L10N_MAPPING,
+ MESSAGE_CATEGORY,
+ style: {
+ ELEMENT_STYLE,
+ },
+};
diff --git a/devtools/shared/content-observer.js b/devtools/shared/content-observer.js
new file mode 100644
index 0000000000..08271e4572
--- /dev/null
+++ b/devtools/shared/content-observer.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";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+/**
+ * Handles adding an observer for the creation of content document globals,
+ * event sent immediately after a web content document window has been set up,
+ * but before any script code has been executed.
+ */
+function ContentObserver(targetActor) {
+ this._contentWindow = targetActor.window;
+ this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
+ this._onInnerWindowDestroyed = this._onInnerWindowDestroyed.bind(this);
+ this.startListening();
+}
+
+module.exports.ContentObserver = ContentObserver;
+
+ContentObserver.prototype = {
+ /**
+ * Starts listening for the required observer messages.
+ */
+ startListening() {
+ Services.obs.addObserver(
+ this._onContentGlobalCreated,
+ "content-document-global-created"
+ );
+ Services.obs.addObserver(
+ this._onInnerWindowDestroyed,
+ "inner-window-destroyed"
+ );
+ },
+
+ /**
+ * Stops listening for the required observer messages.
+ */
+ stopListening() {
+ Services.obs.removeObserver(
+ this._onContentGlobalCreated,
+ "content-document-global-created"
+ );
+ Services.obs.removeObserver(
+ this._onInnerWindowDestroyed,
+ "inner-window-destroyed"
+ );
+ },
+
+ /**
+ * Fired immediately after a web content document window has been set up.
+ */
+ _onContentGlobalCreated(subject, topic, data) {
+ if (subject == this._contentWindow) {
+ EventEmitter.emit(this, "global-created", subject);
+ }
+ },
+
+ /**
+ * Fired when an inner window is removed from the backward/forward cache.
+ */
+ _onInnerWindowDestroyed(subject, topic, data) {
+ const id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ EventEmitter.emit(this, "global-destroyed", id);
+ },
+};
+
+// Utility functions.
+
+ContentObserver.GetInnerWindowID = function (window) {
+ return window.windowGlobalChild.innerWindowId;
+};
diff --git a/devtools/shared/css/color-db.js b/devtools/shared/css/color-db.js
new file mode 100644
index 0000000000..fbac9d9d4b
--- /dev/null
+++ b/devtools/shared/css/color-db.js
@@ -0,0 +1,323 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// /!\ Auto-generated from nsColorNameList.h.
+// This should be kept in sync with that list.
+// test_cssColorDatabase.js tries to enforce this.
+
+const cssColors = {
+ aliceblue: [240, 248, 255, 1],
+ antiquewhite: [250, 235, 215, 1],
+ aqua: [0, 255, 255, 1],
+ aquamarine: [127, 255, 212, 1],
+ azure: [240, 255, 255, 1],
+ beige: [245, 245, 220, 1],
+ bisque: [255, 228, 196, 1],
+ black: [0, 0, 0, 1],
+ blanchedalmond: [255, 235, 205, 1],
+ blue: [0, 0, 255, 1],
+ blueviolet: [138, 43, 226, 1],
+ brown: [165, 42, 42, 1],
+ burlywood: [222, 184, 135, 1],
+ cadetblue: [95, 158, 160, 1],
+ chartreuse: [127, 255, 0, 1],
+ chocolate: [210, 105, 30, 1],
+ coral: [255, 127, 80, 1],
+ cornflowerblue: [100, 149, 237, 1],
+ cornsilk: [255, 248, 220, 1],
+ crimson: [220, 20, 60, 1],
+ cyan: [0, 255, 255, 1],
+ darkblue: [0, 0, 139, 1],
+ darkcyan: [0, 139, 139, 1],
+ darkgoldenrod: [184, 134, 11, 1],
+ darkgray: [169, 169, 169, 1],
+ darkgreen: [0, 100, 0, 1],
+ darkgrey: [169, 169, 169, 1],
+ darkkhaki: [189, 183, 107, 1],
+ darkmagenta: [139, 0, 139, 1],
+ darkolivegreen: [85, 107, 47, 1],
+ darkorange: [255, 140, 0, 1],
+ darkorchid: [153, 50, 204, 1],
+ darkred: [139, 0, 0, 1],
+ darksalmon: [233, 150, 122, 1],
+ darkseagreen: [143, 188, 143, 1],
+ darkslateblue: [72, 61, 139, 1],
+ darkslategray: [47, 79, 79, 1],
+ darkslategrey: [47, 79, 79, 1],
+ darkturquoise: [0, 206, 209, 1],
+ darkviolet: [148, 0, 211, 1],
+ deeppink: [255, 20, 147, 1],
+ deepskyblue: [0, 191, 255, 1],
+ dimgray: [105, 105, 105, 1],
+ dimgrey: [105, 105, 105, 1],
+ dodgerblue: [30, 144, 255, 1],
+ firebrick: [178, 34, 34, 1],
+ floralwhite: [255, 250, 240, 1],
+ forestgreen: [34, 139, 34, 1],
+ fuchsia: [255, 0, 255, 1],
+ gainsboro: [220, 220, 220, 1],
+ ghostwhite: [248, 248, 255, 1],
+ gold: [255, 215, 0, 1],
+ goldenrod: [218, 165, 32, 1],
+ gray: [128, 128, 128, 1],
+ grey: [128, 128, 128, 1],
+ green: [0, 128, 0, 1],
+ greenyellow: [173, 255, 47, 1],
+ honeydew: [240, 255, 240, 1],
+ hotpink: [255, 105, 180, 1],
+ indianred: [205, 92, 92, 1],
+ indigo: [75, 0, 130, 1],
+ ivory: [255, 255, 240, 1],
+ khaki: [240, 230, 140, 1],
+ lavender: [230, 230, 250, 1],
+ lavenderblush: [255, 240, 245, 1],
+ lawngreen: [124, 252, 0, 1],
+ lemonchiffon: [255, 250, 205, 1],
+ lightblue: [173, 216, 230, 1],
+ lightcoral: [240, 128, 128, 1],
+ lightcyan: [224, 255, 255, 1],
+ lightgoldenrodyellow: [250, 250, 210, 1],
+ lightgray: [211, 211, 211, 1],
+ lightgreen: [144, 238, 144, 1],
+ lightgrey: [211, 211, 211, 1],
+ lightpink: [255, 182, 193, 1],
+ lightsalmon: [255, 160, 122, 1],
+ lightseagreen: [32, 178, 170, 1],
+ lightskyblue: [135, 206, 250, 1],
+ lightslategray: [119, 136, 153, 1],
+ lightslategrey: [119, 136, 153, 1],
+ lightsteelblue: [176, 196, 222, 1],
+ lightyellow: [255, 255, 224, 1],
+ lime: [0, 255, 0, 1],
+ limegreen: [50, 205, 50, 1],
+ linen: [250, 240, 230, 1],
+ magenta: [255, 0, 255, 1],
+ maroon: [128, 0, 0, 1],
+ mediumaquamarine: [102, 205, 170, 1],
+ mediumblue: [0, 0, 205, 1],
+ mediumorchid: [186, 85, 211, 1],
+ mediumpurple: [147, 112, 219, 1],
+ mediumseagreen: [60, 179, 113, 1],
+ mediumslateblue: [123, 104, 238, 1],
+ mediumspringgreen: [0, 250, 154, 1],
+ mediumturquoise: [72, 209, 204, 1],
+ mediumvioletred: [199, 21, 133, 1],
+ midnightblue: [25, 25, 112, 1],
+ mintcream: [245, 255, 250, 1],
+ mistyrose: [255, 228, 225, 1],
+ moccasin: [255, 228, 181, 1],
+ navajowhite: [255, 222, 173, 1],
+ navy: [0, 0, 128, 1],
+ oldlace: [253, 245, 230, 1],
+ olive: [128, 128, 0, 1],
+ olivedrab: [107, 142, 35, 1],
+ orange: [255, 165, 0, 1],
+ orangered: [255, 69, 0, 1],
+ orchid: [218, 112, 214, 1],
+ palegoldenrod: [238, 232, 170, 1],
+ palegreen: [152, 251, 152, 1],
+ paleturquoise: [175, 238, 238, 1],
+ palevioletred: [219, 112, 147, 1],
+ papayawhip: [255, 239, 213, 1],
+ peachpuff: [255, 218, 185, 1],
+ peru: [205, 133, 63, 1],
+ pink: [255, 192, 203, 1],
+ plum: [221, 160, 221, 1],
+ powderblue: [176, 224, 230, 1],
+ purple: [128, 0, 128, 1],
+ rebeccapurple: [102, 51, 153, 1],
+ red: [255, 0, 0, 1],
+ rosybrown: [188, 143, 143, 1],
+ royalblue: [65, 105, 225, 1],
+ saddlebrown: [139, 69, 19, 1],
+ salmon: [250, 128, 114, 1],
+ sandybrown: [244, 164, 96, 1],
+ seagreen: [46, 139, 87, 1],
+ seashell: [255, 245, 238, 1],
+ sienna: [160, 82, 45, 1],
+ silver: [192, 192, 192, 1],
+ skyblue: [135, 206, 235, 1],
+ slateblue: [106, 90, 205, 1],
+ slategray: [112, 128, 144, 1],
+ slategrey: [112, 128, 144, 1],
+ snow: [255, 250, 250, 1],
+ springgreen: [0, 255, 127, 1],
+ steelblue: [70, 130, 180, 1],
+ tan: [210, 180, 140, 1],
+ teal: [0, 128, 128, 1],
+ thistle: [216, 191, 216, 1],
+ tomato: [255, 99, 71, 1],
+ turquoise: [64, 224, 208, 1],
+ violet: [238, 130, 238, 1],
+ wheat: [245, 222, 179, 1],
+ white: [255, 255, 255, 1],
+ whitesmoke: [245, 245, 245, 1],
+ yellow: [255, 255, 0, 1],
+ yellowgreen: [154, 205, 50, 1],
+};
+
+// Lab values generated using formula from http://www.easyrgb.com/en/math.php.
+// X_10, Y_10, Z_10 (CIE 1964) reference values for D65 illuminant
+// (Daylight, sRGB, Adobe-RGB) were used.
+const labColors = {
+ aliceblue: [97.17890760827636, -0.9397756746095665, -5.246475627032843],
+ antiquewhite: [93.73077088204487, 2.236373679139203, 10.649604819299775],
+ aqua: [91.11652110946342, -47.73670577664391, -15.108617112376965],
+ aquamarine: [92.03615371984776, -45.172410289053445, 8.849868350925295],
+ azure: [98.93278063011066, -4.468521315692032, -2.6738855376031267],
+ beige: [95.9488798865349, -3.7925176714859177, 11.156542330042662],
+ bisque: [92.0124490871158, 4.82408891282099, 18.18700912515505],
+ black: [0, 0, 0],
+ blanchedalmond: [93.91948969581235, 2.528359387620638, 16.175210959201024],
+ blue: [32.302586667249486, 79.43492388715862, -108.79669359538693],
+ blueviolet: [42.18810476642369, 70.12395877232036, -75.62885995423268],
+ brown: [37.521829744034335, 49.932241625757406, 30.23890786161597],
+ burlywood: [77.01689872654846, 7.390609044482909, 29.376190715516536],
+ cadetblue: [61.15461539950293, -19.415767005661277, -8.112231649104906],
+ chartreuse: [89.87420853068858, -67.74722658691745, 85.31006761317337],
+ chocolate: [55.98605299432204, 37.34619405020917, 56.41475927486582],
+ coral: [67.29048083264607, 45.696749904176436, 47.02073461855407],
+ cornflowerblue: [61.92818670495679, 9.628987440020332, -50.20264083555141],
+ cornsilk: [97.45526614880224, -1.8103271448461355, 13.39924085969022],
+ crimson: [47.02980511087301, 71.21981659922766, 33.22842672949625],
+ cyan: [91.11652110946342, -47.73670577664391, -15.108617112376965],
+ darkblue: [14.757156815274186, 50.58098497783475, -69.27738650827831],
+ darkcyan: [52.207519815998296, -30.3968264791159, -9.620563573304297],
+ darkgoldenrod: [59.2185428516686, 10.142719410772493, 62.4074048759456],
+ darkgray: [69.23779560557699, 0.30845909288851336, -0.725035226776094],
+ darkgreen: [36.20351872497333, -43.22004822584333, 41.62418791981264],
+ darkgrey: [69.23779560557699, 0.30845909288851336, -0.725035226776094],
+ darkkhaki: [73.38127833356914, -8.475096082350364, 38.72716132653318],
+ darkmagenta: [32.59748369188066, 62.78988696261057, -39.34048012552456],
+ darkolivegreen: [42.2340254244788, -18.63594475603103, 30.256085481863913],
+ darkorange: [69.48104411888188, 37.16688167941806, 75.14386507030734],
+ darkorchid: [43.37926695780136, 65.43267508264566, -60.90252187934995],
+ darkred: [28.084732284208997, 51.2102293904998, 41.189776600752445],
+ darksalmon: [69.85346453844176, 28.509213259811705, 27.1173578088314],
+ darkseagreen: [72.08740631745533, -23.522932623842696, 17.379064372791554],
+ darkslateblue: [30.829287209616794, 26.247343305352388, -42.688380574102695],
+ darkslategray: [31.25607829935253, -11.558209436609623, -4.143133638795559],
+ darkslategrey: [31.25607829935253, -11.558209436609623, -4.143133638795559],
+ darkturquoise: [75.29307531314078, -39.74330458218445, -14.353628151972542],
+ darkviolet: [39.57886745488795, 76.59826422688023, -71.18960396554851],
+ deeppink: [55.95428053659428, 84.8834888580915, -6.348160442712891],
+ deepskyblue: [72.54923231908798, -17.34624248545108, -43.501223759829614],
+ dimgray: [44.41356014781601, 0.21862498706087807, -0.513879541005613],
+ dimgrey: [44.41356014781601, 0.21862498706087807, -0.513879541005613],
+ dodgerblue: [59.381725677880596, 10.24722110279519, -64.33873493803226],
+ firebrick: [39.11257274771978, 56.17110340915579, 37.36856201227712],
+ floralwhite: [98.40143252802201, 0.37693631302881947, 4.4296183363891295],
+ forestgreen: [50.59443111773345, -49.389501730047996, 44.675247183324544],
+ fuchsia: [60.319933664076004, 98.60839787805526, -61.78226947982015],
+ gainsboro: [87.76088811005116, 0.3754905813360132, -0.8825932030137817],
+ ghostwhite: [97.7572735023453, 1.6602117625886814, -4.329673676949475],
+ gold: [86.9285847161576, -1.5580031594539245, 86.69625851871493],
+ goldenrod: [70.81571317667877, 8.842268163696321, 68.36969220318736],
+ gray: [53.585013452169036, 0.2518147023348183, -0.5918921958278478],
+ grey: [53.585013452169036, 0.2518147023348183, -0.5918921958278478],
+ green: [46.22881784262658, -51.520138371609654, 49.61780491379323],
+ greenyellow: [91.95763180408608, -52.141510237787294, 81.35705222692138],
+ honeydew: [98.56580137512647, -7.15743440652189, 4.528336799358468],
+ hotpink: [65.48186958181394, 64.59515848582348, -11.397593830097975],
+ indianred: [53.3911485087161, 45.12118404819798, 21.633935524110235],
+ indigo: [20.46961954096429, 51.86802044661362, -53.88700686969674],
+ ivory: [99.63977381109996, -2.1358046127358254, 6.214520241062194],
+ khaki: [90.32729582034386, -8.636557330944761, 44.29884008040395],
+ lavender: [91.82769059829121, 4.102249854337825, -10.62634890559453],
+ lavenderblush: [96.06837944039779, 6.29813340018881, -1.5503003307978824],
+ lawngreen: [88.87798766318886, -67.54051106102659, 84.48686421833247],
+ lemonchiffon: [97.6476983766357, -5.022220605824412, 21.377483194761602],
+ lightblue: [83.81410256512628, -10.538570949192195, -12.381398199756767],
+ lightcoral: [66.15316216626996, 43.146382854120255, 18.95210780303691],
+ lightcyan: [97.86814756512815, -9.540634546450377, -4.3595592568243635],
+ lightgoldenrodyellow: [
+ 97.36879732328356, -6.078213524913445, 18.368452638973686,
+ ],
+ lightgray: [84.5561167363605, 0.36389313370355225, -0.8553333223096704],
+ lightgreen: [86.54957590580997, -45.99957827642365, 36.262226875570526],
+ lightgrey: [84.5561167363605, 0.36389313370355225, -0.8553333223096704],
+ lightpink: [81.05253594361857, 28.338879720153866, 4.233248736979411],
+ lightsalmon: [74.70287844017, 31.832905786707443, 33.947175894873574],
+ lightseagreen: [65.78768757989991, -37.24749389368925, -7.0553625579029555],
+ lightskyblue: [79.72503275959482, -10.490259345458607, -29.455091101479304],
+ lightslategray: [
+ 55.917349372007195, -1.9881568917105574, -11.774175139854615,
+ ],
+ lightslategrey: [
+ 55.917349372007195, -1.9881568917105574, -11.774175139854615,
+ ],
+ lightsteelblue: [78.45233742822654, -0.9392458669100212, -16.08913657631925],
+ lightyellow: [99.28483637693047, -4.695918870143368, 13.930783736002962],
+ lime: [87.73703347354422, -85.88539046866318, 82.71415186197115],
+ limegreen: [72.60854102811317, -66.86592102874306, 60.99160141799125],
+ linen: [95.31120096478845, 2.0811899572744097, 5.104906068897774],
+ magenta: [60.319933664076004, 98.60839787805526, -61.78226947982015],
+ maroon: [25.530784572416174, 48.24348236957932, 37.97194526317749],
+ mediumaquamarine: [75.6931293906138, -38.036553589818666, 7.571354765719218],
+ mediumblue: [24.97615723043893, 67.38641044652589, -92.29465191227868],
+ mediumorchid: [53.64213313573549, 59.370412003217574, -48.23242669563843],
+ mediumpurple: [54.97523587308039, 37.09186250486124, -50.94130373794006],
+ mediumseagreen: [65.27341001550978, -47.96643007935963, 23.721501577817094],
+ mediumslateblue: [52.15750471109439, 41.35727328020744, -66.29828530971085],
+ mediumspringgreen: [87.3411479783456, -70.37364235921007, 31.74790377903216],
+ mediumturquoise: [76.8834686242291, -37.053452849746925, -9.183510559800713],
+ mediumvioletred: [44.76162545138306, 71.28274660137451, -15.767834030225348],
+ midnightblue: [15.859552026439893, 31.860128204191234, -50.08845915164022],
+ mintcream: [99.15659662605985, -3.7499332041522893, 0.27328356915989893],
+ mistyrose: [92.65558901260384, 9.147609792762356, 3.9347415624175097],
+ moccasin: [91.72208952705464, 2.8291906794328248, 25.573953055107125],
+ navajowhite: [90.10007935356519, 4.895862633988224, 27.509525419099212],
+ navy: [12.975311577716514, 47.65069178672376, -65.26396023609063],
+ oldlace: [96.77967120416257, 0.5784959274544033, 7.247216630469544],
+ olive: [51.86833136334822, -12.698958090622824, 56.38276716821539],
+ olivedrab: [54.650773976467136, -27.99433891007591, 49.33893945670905],
+ orange: [74.93219484533535, 24.28083176103374, 78.57643571179258],
+ orangered: [57.57499421872107, 68.11461778155586, 68.68889341848478],
+ orchid: [62.8009876658991, 55.621167174625064, -35.24868649820956],
+ palegoldenrod: [91.14038954621267, -6.970467434592564, 30.213907174487264],
+ palegreen: [90.75103714758299, -47.95503269332063, 37.81303534889619],
+ paleturquoise: [90.06142407788806, -19.270453187271997, -7.332269935129032],
+ palevioletred: [60.564787867661266, 45.838854731426515, -0.25065577032372666],
+ papayawhip: [95.07544110280504, 1.6725420650229306, 13.652324626448543],
+ peachpuff: [89.34875285782702, 8.471965039192453, 20.229897875280955],
+ peru: [61.751663684265665, 21.6941378210313, 47.496113355477334],
+ pink: [83.58479885775868, 24.525540181552508, 2.4934778968259685],
+ plum: [73.37274280928348, 32.885443900333215, -22.856650741159747],
+ powderblue: [86.13359115282357, -13.734412712450183, -8.914802708654769],
+ purple: [29.782100092098077, 59.15229907621816, -37.06137976602428],
+ rebeccapurple: [32.90243720302775, 43.10251060523782, -47.79866704780537],
+ red: [53.23288178584245, 80.42312097443104, 66.96552840294578],
+ osybrown: [63.6060381979233, 17.31557480446677, 5.964013673186241],
+ royalblue: [47.83284750656463, 26.523923842683427, -66.12638984604963],
+ saddlebrown: [37.46692024400534, 26.659518429642898, 40.73548402580912],
+ salmon: [67.25995258561211, 45.569072753010865, 28.527978009408052],
+ sandybrown: [73.95154071958171, 23.37100127546693, 46.25858205082345],
+ seagreen: [51.53535112854968, -39.50584886698982, 19.578964107165852],
+ seashell: [97.12111642889012, 2.5730542225586572, 3.614216901880374],
+ sienna: [43.796139581025685, 29.564844221078523, 35.307185975723435],
+ silver: [77.7043635899527, 0.339097964550894, -0.7970521060836511],
+ skyblue: [79.20897459092869, -14.504258485788124, -22.18979862749155],
+ slateblue: [45.33730589003685, 36.29854743329886, -58.578448229379696],
+ slategray: [52.83625796271889, -1.894387969799849, -11.208348578862527],
+ slategrey: [52.83625796271889, -1.894387969799849, -11.208348578862527],
+ snow: [98.64376394836441, 2.073027231003055, -0.3849225040892579],
+ springgreen: [88.47265520282772, -76.59140183507563, 46.37560569696295],
+ steelblue: [52.46747241512048, -3.828873386915843, -32.93153861676517],
+ tan: [74.97454643298407, 5.353040309039603, 23.775061853275446],
+ teal: [48.25607381337552, -28.635856151982775, -9.063218319038114],
+ thistle: [80.07734471203901, 13.57781031097305, -10.092571203748268],
+ tomato: [62.20136881808274, 58.18874059980511, 45.985101486056514],
+ turquoise: [81.26705459794431, -43.765218870671475, -4.872758326234994],
+ violet: [69.69362286537107, 56.72140058020769, -37.7245806568902],
+ wheat: [89.35068099788126, 1.8921211283112749, 23.23051272657026],
+ white: [100, 0.41978155958710683, -0.9866994530830286],
+ whitesmoke: [96.53748961423615, 0.40725140432995577, -0.9572472366698781],
+ yellow: [97.13824698129729, -21.169488449187533, 93.99151723968109],
+ yellowgreen: [76.5351984302265, -37.69097531845872, 66.13250570541341],
+};
+
+exports.cssColors = cssColors;
+exports.labColors = labColors;
diff --git a/devtools/shared/css/color.js b/devtools/shared/css/color.js
new file mode 100644
index 0000000000..16ea58bc08
--- /dev/null
+++ b/devtools/shared/css/color.js
@@ -0,0 +1,766 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 COLOR_UNIT_PREF = "devtools.defaultColorUnit";
+const SPECIALVALUES = new Set([
+ "currentcolor",
+ "initial",
+ "inherit",
+ "transparent",
+ "unset",
+]);
+
+/**
+ * This module is used to convert between various color types.
+ *
+ * Usage:
+ * let {colorUtils} = require("devtools/shared/css/color");
+ * let color = new colorUtils.CssColor("red");
+ * // In order to support css-color-4 color function, pass true to the
+ * // second argument.
+ * // e.g.
+ * // let color = new colorUtils.CssColor("red", true);
+ *
+ * color.authored === "red"
+ * color.hasAlpha === false
+ * color.valid === true
+ * color.transparent === false // transparent has a special status.
+ * color.name === "red" // returns hex when no name available.
+ * color.hex === "#f00" // returns shortHex when available else returns
+ * longHex. If alpha channel is present then we
+ * return this.alphaHex if available,
+ * or this.longAlphaHex if not.
+ * color.alphaHex === "#f00f" // returns short alpha hex when available
+ * else returns longAlphaHex.
+ * color.longHex === "#ff0000" // If alpha channel is present then we return
+ * this.longAlphaHex.
+ * color.longAlphaHex === "#ff0000ff"
+ * color.rgb === "rgb(255, 0, 0)" // If alpha channel is present
+ * // then we return this.rgba.
+ * color.rgba === "rgba(255, 0, 0, 1)"
+ * color.hsl === "hsl(0, 100%, 50%)"
+ * color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
+ * then we return this.rgba.
+ * color.hwb === "hwb(0, 0%, 0%)"
+ *
+ * color.toString() === "#f00"; // Outputs the color type determined in the
+ * COLOR_UNIT_PREF constant (above).
+ *
+ * Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
+ */
+class CssColor {
+ /**
+ * @param {String} colorValue: Any valid color string
+ */
+ constructor(colorValue) {
+ // Store a lower-cased version of the color to help with format
+ // testing. The original text is kept as well so it can be
+ // returned when needed.
+ this.#lowerCased = colorValue.toLowerCase();
+ this.#authored = colorValue;
+ }
+
+ /**
+ * Values used in COLOR_UNIT_PREF
+ */
+ static COLORUNIT = {
+ authored: "authored",
+ hex: "hex",
+ name: "name",
+ rgb: "rgb",
+ hsl: "hsl",
+ hwb: "hwb",
+ };
+
+ // The value as-authored.
+ #authored = null;
+ #currentFormat;
+ // A lower-cased copy of |authored|.
+ #lowerCased = null;
+
+ get hasAlpha() {
+ if (!this.valid) {
+ return false;
+ }
+ return this.getRGBATuple().a !== 1;
+ }
+
+ /**
+ * Return true if the color is a valid color and we can get rgba tuples from it.
+ */
+ get valid() {
+ // We can't use InspectorUtils.isValidCSSColor as colors can be valid but we can't have
+ // their rgba tuples (e.g. currentColor, accentColor, … whose actual values depends on
+ // additional context we don't have here).
+ return InspectorUtils.colorToRGBA(this.#authored) !== null;
+ }
+
+ /**
+ * Not a real color type but used to preserve accuracy when converting between
+ * e.g. 8 character hex -> rgba -> 8 character hex (hex alpha values are
+ * 0 - 255 but rgba alpha values are only 0.0 to 1.0).
+ */
+ get highResTuple() {
+ const type = classifyColor(this.#authored);
+
+ if (type === CssColor.COLORUNIT.hex) {
+ return hexToRGBA(this.#authored.substring(1), true);
+ }
+
+ // If we reach this point then the alpha value must be in the range
+ // 0.0 - 1.0 so we need to multiply it by 255.
+ const tuple = InspectorUtils.colorToRGBA(this.#authored);
+ tuple.a *= 255;
+ return tuple;
+ }
+
+ /**
+ * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
+ */
+ get transparent() {
+ try {
+ const tuple = this.getRGBATuple();
+ return !(tuple.r || tuple.g || tuple.b || tuple.a);
+ } catch (e) {
+ return false;
+ }
+ }
+
+ get specialValue() {
+ return SPECIALVALUES.has(this.#lowerCased) ? this.#authored : null;
+ }
+
+ get name() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ const tuple = this.getRGBATuple();
+
+ if (tuple.a !== 1) {
+ return this.hex;
+ }
+ const { r, g, b } = tuple;
+ return InspectorUtils.rgbToColorName(r, g, b) || this.hex;
+ }
+
+ get hex() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (this.hasAlpha) {
+ return this.alphaHex;
+ }
+
+ let hex = this.longHex;
+ if (
+ hex.charAt(1) == hex.charAt(2) &&
+ hex.charAt(3) == hex.charAt(4) &&
+ hex.charAt(5) == hex.charAt(6)
+ ) {
+ hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
+ }
+ return hex;
+ }
+
+ get alphaHex() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let alphaHex = this.longAlphaHex;
+ if (
+ alphaHex.charAt(1) == alphaHex.charAt(2) &&
+ alphaHex.charAt(3) == alphaHex.charAt(4) &&
+ alphaHex.charAt(5) == alphaHex.charAt(6) &&
+ alphaHex.charAt(7) == alphaHex.charAt(8)
+ ) {
+ alphaHex =
+ "#" +
+ alphaHex.charAt(1) +
+ alphaHex.charAt(3) +
+ alphaHex.charAt(5) +
+ alphaHex.charAt(7);
+ }
+ return alphaHex;
+ }
+
+ get longHex() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (this.hasAlpha) {
+ return this.longAlphaHex;
+ }
+
+ const tuple = this.getRGBATuple();
+ return (
+ "#" +
+ ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0))
+ .toString(16)
+ .substr(-6)
+ );
+ }
+
+ get longAlphaHex() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ const tuple = this.highResTuple;
+
+ return (
+ "#" +
+ ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0))
+ .toString(16)
+ .substr(-6) +
+ Math.round(tuple.a).toString(16).padStart(2, "0")
+ );
+ }
+
+ get rgb() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (!this.hasAlpha) {
+ if (this.#lowerCased.startsWith("rgb(")) {
+ // The color is valid and begins with rgb(.
+ return this.#authored;
+ }
+ const tuple = this.getRGBATuple();
+ return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
+ }
+ return this.rgba;
+ }
+
+ get rgba() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (this.#lowerCased.startsWith("rgba(")) {
+ // The color is valid and begins with rgba(.
+ return this.#authored;
+ }
+ const components = this.getRGBATuple();
+ return (
+ "rgba(" +
+ components.r +
+ ", " +
+ components.g +
+ ", " +
+ components.b +
+ ", " +
+ components.a +
+ ")"
+ );
+ }
+
+ get hsl() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (this.#lowerCased.startsWith("hsl(")) {
+ // The color is valid and begins with hsl(.
+ return this.#authored;
+ }
+ if (this.hasAlpha) {
+ return this.hsla;
+ }
+ return this.#hsl();
+ }
+
+ get hsla() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (this.#lowerCased.startsWith("hsla(")) {
+ // The color is valid and begins with hsla(.
+ return this.#authored;
+ }
+ if (this.hasAlpha) {
+ const a = this.getRGBATuple().a;
+ return this.#hsl(a);
+ }
+ return this.#hsl(1);
+ }
+
+ get hwb() {
+ const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+ if (this.#lowerCased.startsWith("hwb(")) {
+ // The color is valid and begins with hwb(.
+ return this.#authored;
+ }
+ if (this.hasAlpha) {
+ const a = this.getRGBATuple().a;
+ return this.#hwb(a);
+ }
+ return this.#hwb();
+ }
+
+ /**
+ * Check whether the current color value is in the special list e.g.
+ * transparent or invalid.
+ *
+ * @return {String|Boolean}
+ * - If the current color is a special value e.g. "transparent" then
+ * return the color.
+ * - If the current color is a system value e.g. "accentcolor" then
+ * return the color.
+ * - If the color is invalid or that we can't get rgba components from it
+ * (e.g. "accentcolor"), return an empty string.
+ * - If the color is a regular color e.g. #F06 so we return false
+ * to indicate that the color is neither invalid or special.
+ */
+ #getInvalidOrSpecialValue() {
+ if (this.specialValue) {
+ return this.specialValue;
+ }
+ if (!this.valid) {
+ return "";
+ }
+ return false;
+ }
+
+ nextColorUnit() {
+ // Reorder the formats array to have the current format at the
+ // front so we can cycle through.
+ // Put "name" at the end as that provides a hex value if there's
+ // no name for the color.
+ let formats = ["hex", "hsl", "rgb", "hwb", "name"];
+
+ let currentFormat = this.#currentFormat;
+ // If we don't have determined the current format yet
+ if (!currentFormat) {
+ // If the pref value is COLORUNIT.authored, get the actual unit from the authored color,
+ // otherwise use the pref value.
+ const defaultFormat = Services.prefs.getCharPref(COLOR_UNIT_PREF);
+ currentFormat =
+ defaultFormat === CssColor.COLORUNIT.authored
+ ? classifyColor(this.#authored)
+ : defaultFormat;
+ }
+ const putOnEnd = formats.splice(0, formats.indexOf(currentFormat));
+ formats = [...formats, ...putOnEnd];
+
+ const currentDisplayedColor = this[formats[0]];
+
+ let colorUnit;
+ for (const format of formats) {
+ if (this[format].toLowerCase() !== currentDisplayedColor.toLowerCase()) {
+ colorUnit = CssColor.COLORUNIT[format];
+ break;
+ }
+ }
+
+ this.#currentFormat = colorUnit;
+ return this.toString(colorUnit);
+ }
+
+ /**
+ * Return a string representing a color of type defined in COLOR_UNIT_PREF.
+ */
+ toString(colorUnit, forceUppercase) {
+ let color;
+
+ switch (colorUnit) {
+ case CssColor.COLORUNIT.authored:
+ color = this.#authored;
+ break;
+ case CssColor.COLORUNIT.hex:
+ color = this.hex;
+ break;
+ case CssColor.COLORUNIT.hsl:
+ color = this.hsl;
+ break;
+ case CssColor.COLORUNIT.name:
+ color = this.name;
+ break;
+ case CssColor.COLORUNIT.rgb:
+ color = this.rgb;
+ break;
+ case CssColor.COLORUNIT.hwb:
+ color = this.hwb;
+ break;
+ default:
+ color = this.rgb;
+ }
+
+ if (
+ forceUppercase ||
+ (colorUnit != CssColor.COLORUNIT.authored &&
+ colorIsUppercase(this.#authored))
+ ) {
+ color = color.toUpperCase();
+ }
+
+ return color;
+ }
+
+ /**
+ * Returns a RGBA 4-Tuple representation of a color or transparent as
+ * appropriate.
+ */
+ getRGBATuple() {
+ const tuple = InspectorUtils.colorToRGBA(this.#authored);
+
+ tuple.a = parseFloat(tuple.a.toFixed(2));
+
+ return tuple;
+ }
+
+ #hsl(maybeAlpha) {
+ if (this.#lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
+ // We can use it as-is.
+ return this.#authored;
+ }
+
+ const { r, g, b } = this.getRGBATuple();
+ const [h, s, l] = rgbToHsl([r, g, b]);
+ if (maybeAlpha !== undefined) {
+ return "hsla(" + h + ", " + s + "%, " + l + "%, " + maybeAlpha + ")";
+ }
+ return "hsl(" + h + ", " + s + "%, " + l + "%)";
+ }
+
+ #hwb(maybeAlpha) {
+ if (this.#lowerCased.startsWith("hwb(") && maybeAlpha === undefined) {
+ // We can use it as-is.
+ return this.#authored;
+ }
+
+ const { r, g, b } = this.getRGBATuple();
+ const [hue, white, black] = rgbToHwb([r, g, b]);
+ return `hwb(${hue} ${white}% ${black}%${
+ maybeAlpha !== undefined ? " / " + maybeAlpha : ""
+ })`;
+ }
+
+ /**
+ * This method allows comparison of CssColor objects using ===.
+ */
+ valueOf() {
+ return this.rgba;
+ }
+
+ /**
+ * Check whether the color is fully transparent (alpha === 0).
+ *
+ * @return {Boolean} True if the color is transparent and valid.
+ */
+ isTransparent() {
+ return this.getRGBATuple().a === 0;
+ }
+}
+
+/**
+ * Convert rgb value to hsl
+ *
+ * @param {array} rgb
+ * Array of rgb values
+ * @return {array}
+ * Array of hsl values.
+ */
+function rgbToHsl([r, g, b]) {
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ let h;
+ let s;
+ const l = (max + min) / 2;
+
+ if (max == min) {
+ h = s = 0;
+ } else {
+ const d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+
+ switch (max) {
+ case r:
+ h = ((g - b) / d) % 6;
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+ h *= 60;
+ if (h < 0) {
+ h += 360;
+ }
+ }
+
+ return [roundTo(h, 1), roundTo(s * 100, 1), roundTo(l * 100, 1)];
+}
+
+/**
+ * Convert RGB value to HWB
+ *
+ * @param {array} rgb
+ * Array of RGB values
+ * @return {array}
+ * Array of HWB values.
+ */
+function rgbToHwb([r, g, b]) {
+ const hsl = rgbToHsl([r, g, b]);
+
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ const white = Math.min(r, g, b);
+ const black = 1 - Math.max(r, g, b);
+ return [roundTo(hsl[0], 1), roundTo(white * 100, 1), roundTo(black * 100, 1)];
+}
+
+/**
+ * Convert rgb value to CIE LAB colorspace (https://en.wikipedia.org/wiki/CIELAB_color_space).
+ * Formula from http://www.easyrgb.com/en/math.php.
+ *
+ * @param {array} rgb
+ * Array of rgb values
+ * @return {array}
+ * Array of lab values.
+ */
+function rgbToLab([r, g, b]) {
+ // Convert rgb values to xyz coordinates.
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
+ g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
+ b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
+
+ r = r * 100;
+ g = g * 100;
+ b = b * 100;
+
+ let [x, y, z] = [
+ r * 0.4124 + g * 0.3576 + b * 0.1805,
+ r * 0.2126 + g * 0.7152 + b * 0.0722,
+ r * 0.0193 + g * 0.1192 + b * 0.9505,
+ ];
+
+ // Convert xyz coordinates to lab values.
+ // Divisors used are X_10, Y_10, Z_10 (CIE 1964) reference values for D65
+ // illuminant (Daylight, sRGB, Adobe-RGB) taken from http://www.easyrgb.com/en/math.php
+ x = x / 94.811;
+ y = y / 100;
+ z = z / 107.304;
+
+ x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
+ y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
+ z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;
+
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
+}
+
+/**
+ * Calculates the CIE Delta-E value for two lab values (http://www.colorwiki.com/wiki/Delta_E%3a_The_Color_Difference#Delta-E_1976).
+ * Formula from http://www.easyrgb.com/en/math.php.
+ *
+ * @param {array} lab1
+ * Array of lab values for the first color
+ * @param {array} lab2
+ * Array of lab values for the second color
+ * @return {Number}
+ * DeltaE value between the two colors
+ */
+function calculateDeltaE([l1, a1, b1], [l2, a2, b2]) {
+ return Math.sqrt(
+ Math.pow(l1 - l2, 2) + Math.pow(a1 - a2, 2) + Math.pow(b1 - b2, 2)
+ );
+}
+
+function roundTo(number, digits) {
+ const multiplier = Math.pow(10, digits);
+ return Math.round(number * multiplier) / multiplier;
+}
+
+/**
+ * Given a color, classify its type as one of the possible color
+ * units, as known by |CssColor.COLORUNIT|.
+ *
+ * @param {String} value
+ * The color, in any form accepted by CSS.
+ * @return {String}
+ * The color classification, one of "rgb", "hsl", "hwb",
+ * "hex", "name", or if no format is recognized, "authored".
+ */
+function classifyColor(value) {
+ value = value.toLowerCase();
+ if (value.startsWith("rgb(") || value.startsWith("rgba(")) {
+ return CssColor.COLORUNIT.rgb;
+ } else if (value.startsWith("hsl(") || value.startsWith("hsla(")) {
+ return CssColor.COLORUNIT.hsl;
+ } else if (value.startsWith("hwb(")) {
+ return CssColor.COLORUNIT.hwb;
+ } else if (/^#[0-9a-f]+$/.exec(value)) {
+ return CssColor.COLORUNIT.hex;
+ } else if (/^[a-z\-]+$/.exec(value)) {
+ return CssColor.COLORUNIT.name;
+ }
+ return CssColor.COLORUNIT.authored;
+}
+
+/**
+ * A helper function to convert a hex string like "F0C" or "F0C8" to a color.
+ *
+ * @param {String} name the color string
+ * @param {Boolean} highResolution Forces returned alpha value to be in the
+ * range 0 - 255 as opposed to 0.0 - 1.0.
+ * @return {Object} an object of the form {r, g, b, a}; or null if the
+ * name was not a valid color
+ */
+function hexToRGBA(name, highResolution) {
+ let r,
+ g,
+ b,
+ a = 1;
+
+ if (name.length === 3) {
+ // short hex string (e.g. F0C)
+ r = parseInt(name.charAt(0) + name.charAt(0), 16);
+ g = parseInt(name.charAt(1) + name.charAt(1), 16);
+ b = parseInt(name.charAt(2) + name.charAt(2), 16);
+ } else if (name.length === 4) {
+ // short alpha hex string (e.g. F0CA)
+ r = parseInt(name.charAt(0) + name.charAt(0), 16);
+ g = parseInt(name.charAt(1) + name.charAt(1), 16);
+ b = parseInt(name.charAt(2) + name.charAt(2), 16);
+ a = parseInt(name.charAt(3) + name.charAt(3), 16);
+
+ if (!highResolution) {
+ a /= 255;
+ }
+ } else if (name.length === 6) {
+ // hex string (e.g. FD01CD)
+ r = parseInt(name.charAt(0) + name.charAt(1), 16);
+ g = parseInt(name.charAt(2) + name.charAt(3), 16);
+ b = parseInt(name.charAt(4) + name.charAt(5), 16);
+ } else if (name.length === 8) {
+ // alpha hex string (e.g. FD01CDAB)
+ r = parseInt(name.charAt(0) + name.charAt(1), 16);
+ g = parseInt(name.charAt(2) + name.charAt(3), 16);
+ b = parseInt(name.charAt(4) + name.charAt(5), 16);
+ a = parseInt(name.charAt(6) + name.charAt(7), 16);
+
+ if (!highResolution) {
+ a /= 255;
+ }
+ } else {
+ return null;
+ }
+ if (!highResolution) {
+ a = Math.round(a * 10) / 10;
+ }
+ return { r, g, b, a };
+}
+
+/**
+ * Calculates the luminance of a rgba tuple based on the formula given in
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ *
+ * @param {Array} rgba An array with [r,g,b,a] values.
+ * @return {Number} The calculated luminance.
+ */
+function calculateLuminance(rgba) {
+ for (let i = 0; i < 3; i++) {
+ rgba[i] /= 255;
+ rgba[i] =
+ rgba[i] < 0.03928
+ ? rgba[i] / 12.92
+ : Math.pow((rgba[i] + 0.055) / 1.055, 2.4);
+ }
+ return 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2];
+}
+
+/**
+ * Blend background and foreground colors takign alpha into account.
+ * @param {Array} foregroundColor
+ * An array with [r,g,b,a] values containing the foreground color.
+ * @param {Array} backgroundColor
+ * An array with [r,g,b,a] values containing the background color. Defaults to
+ * [ 255, 255, 255, 1 ].
+ * @return {Array}
+ * An array with combined [r,g,b,a] colors.
+ */
+function blendColors(foregroundColor, backgroundColor = [255, 255, 255, 1]) {
+ const [fgR, fgG, fgB, fgA] = foregroundColor;
+ const [bgR, bgG, bgB, bgA] = backgroundColor;
+ if (fgA === 1) {
+ return foregroundColor;
+ }
+
+ return [
+ (1 - fgA) * bgR + fgA * fgR,
+ (1 - fgA) * bgG + fgA * fgG,
+ (1 - fgA) * bgB + fgA * fgB,
+ fgA + bgA * (1 - fgA),
+ ];
+}
+
+/**
+ * Calculates the contrast ratio of 2 rgba tuples based on the formula in
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
+ *
+ * @param {Array} backgroundColor An array with [r,g,b,a] values containing
+ * the background color.
+ * @param {Array} textColor An array with [r,g,b,a] values containing
+ * the text color.
+ * @return {Number} The calculated luminance.
+ */
+function calculateContrastRatio(backgroundColor, textColor) {
+ // Do not modify given colors.
+ backgroundColor = Array.from(backgroundColor);
+ textColor = Array.from(textColor);
+
+ backgroundColor = blendColors(backgroundColor);
+ textColor = blendColors(textColor, backgroundColor);
+
+ const backgroundLuminance = calculateLuminance(backgroundColor);
+ const textLuminance = calculateLuminance(textColor);
+ const ratio = (textLuminance + 0.05) / (backgroundLuminance + 0.05);
+
+ return ratio > 1.0 ? ratio : 1 / ratio;
+}
+
+function colorIsUppercase(color) {
+ // Specifically exclude the case where the color is
+ // case-insensitive. This makes it so that "#000" isn't
+ // considered "upper case" for the purposes of color cycling.
+ return color === color.toUpperCase() && color !== color.toLowerCase();
+}
+
+module.exports.colorUtils = {
+ CssColor,
+ rgbToHsl,
+ rgbToHwb,
+ rgbToLab,
+ classifyColor,
+ calculateContrastRatio,
+ calculateDeltaE,
+ calculateLuminance,
+ blendColors,
+ colorIsUppercase,
+};
diff --git a/devtools/shared/css/constants.js b/devtools/shared/css/constants.js
new file mode 100644
index 0000000000..cc8748c553
--- /dev/null
+++ b/devtools/shared/css/constants.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";
+
+/**
+ * All CSS <angle> types that properties can support.
+ */
+exports.CSS_ANGLEUNIT = {
+ deg: "deg",
+ rad: "rad",
+ grad: "grad",
+ turn: "turn",
+};
+
+/**
+ * @backward-compat { version 70 } Mapping of InspectorPropertyType to old type ID.
+ */
+exports.CSS_TYPES = {
+ color: 2,
+ gradient: 4,
+ "timing-function": 10,
+};
+
+/**
+ * Supported pseudo-class locks in the order in which they appear in the pseudo-class
+ * panel in the Rules sidebar panel of the Inspector.
+ */
+exports.PSEUDO_CLASSES = [
+ ":hover",
+ ":active",
+ ":focus",
+ ":focus-visible",
+ ":focus-within",
+ ":visited",
+ ":target",
+];
diff --git a/devtools/shared/css/lexer.js b/devtools/shared/css/lexer.js
new file mode 100644
index 0000000000..18e78717d1
--- /dev/null
+++ b/devtools/shared/css/lexer.js
@@ -0,0 +1,1522 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A CSS Lexer. This file is a bit unusual -- it is a more or less
+// direct translation of layout/style/nsCSSScanner.cpp and
+// layout/style/CSSLexer.cpp into JS. This implemented the
+// CSSLexer.webidl interface, and the intent is to try to keep it in
+// sync with changes to the platform CSS lexer. Due to this goal,
+// this file violates some naming conventions and consequently locally
+// disables some eslint rules.
+
+/* eslint-disable camelcase, mozilla/no-aArgs, no-else-return, complexity */
+
+"use strict";
+
+// White space of any kind. No value fields are used. Note that
+// comments do *not* count as white space; comments separate tokens
+// but are not themselves tokens.
+const eCSSToken_Whitespace = "whitespace"; //
+// A comment.
+const eCSSToken_Comment = "comment"; // /*...*/
+
+// Identifier-like tokens. mIdent is the text of the identifier.
+// The difference between ID and Hash is: if the text after the #
+// would have been a valid Ident if the # hadn't been there, the
+// scanner produces an ID token. Otherwise it produces a Hash token.
+// (This distinction is required by css3-selectors.)
+const eCSSToken_Ident = "ident"; // word
+const eCSSToken_Function = "function"; // word(
+const eCSSToken_AtKeyword = "at"; // @word
+const eCSSToken_ID = "id"; // #word
+const eCSSToken_Hash = "hash"; // #0word
+
+// Numeric tokens. mNumber is the floating-point value of the
+// number, and mHasSign indicates whether there was an explicit sign
+// (+ or -) in front of the number. If mIntegerValid is true, the
+// number had the lexical form of an integer, and mInteger is its
+// integer value. Lexically integer values outside the range of a
+// 32-bit signed number are clamped to the maximum values; mNumber
+// will indicate a 'truer' value in that case. Percentage tokens
+// are always considered not to be integers, even if their numeric
+// value is integral (100% => mNumber = 1.0). For Dimension
+// tokens, mIdent holds the text of the unit.
+const eCSSToken_Number = "number"; // 1 -5 +2e3 3.14159 7.297352e-3
+const eCSSToken_Dimension = "dimension"; // 24px 8.5in
+const eCSSToken_Percentage = "percentage"; // 85% 1280.4%
+
+// String-like tokens. In all cases, mIdent holds the text
+// belonging to the string, and mSymbol holds the delimiter
+// character, which may be ', ", or zero (only for unquoted URLs).
+// Bad_String and Bad_URL tokens are emitted when the closing
+// delimiter or parenthesis was missing.
+const eCSSToken_String = "string"; // 'foo bar' "foo bar"
+const eCSSToken_Bad_String = "bad_string"; // 'foo bar
+const eCSSToken_URL = "url"; // url(foobar) url("foo bar")
+const eCSSToken_Bad_URL = "bad_url"; // url(foo
+
+// Any one-character symbol. mSymbol holds the character.
+const eCSSToken_Symbol = "symbol"; // . ; { } ! *
+
+// Match operators. These are single tokens rather than pairs of
+// Symbol tokens because css3-selectors forbids the presence of
+// comments between the two characters. No value fields are used;
+// the token type indicates which operator.
+const eCSSToken_Includes = "includes"; // ~=
+const eCSSToken_Dashmatch = "dashmatch"; // |=
+const eCSSToken_Beginsmatch = "beginsmatch"; // ^=
+const eCSSToken_Endsmatch = "endsmatch"; // $=
+const eCSSToken_Containsmatch = "containsmatch"; // *=
+
+// Unicode-range token: currently used only in @font-face.
+// The lexical rule for this token includes several forms that are
+// semantically invalid. Therefore, mIdent always holds the
+// complete original text of the token (so we can print it
+// accurately in diagnostics), and mIntegerValid is true iff the
+// token is semantically valid. In that case, mInteger holds the
+// lowest value included in the range, and mInteger2 holds the
+// highest value included in the range.
+const eCSSToken_URange = "urange"; // U+007e U+01?? U+2000-206F
+
+// HTML comment delimiters, ignored as a unit when they appear at
+// the top level of a style sheet, for compatibility with websites
+// written for compatibility with pre-CSS browsers. This token type
+// subsumes the css2.1 CDO and CDC tokens, which are always treated
+// the same by the parser. mIdent holds the text of the token, for
+// diagnostics.
+const eCSSToken_HTMLComment = "htmlcomment"; // <!-- -->
+
+const eEOFCharacters_None = 0x0000;
+
+// to handle \<EOF> inside strings
+const eEOFCharacters_DropBackslash = 0x0001;
+
+// to handle \<EOF> outside strings
+const eEOFCharacters_ReplacementChar = 0x0002;
+
+// to close comments
+const eEOFCharacters_Asterisk = 0x0004;
+const eEOFCharacters_Slash = 0x0008;
+
+// to close double-quoted strings
+const eEOFCharacters_DoubleQuote = 0x0010;
+
+// to close single-quoted strings
+const eEOFCharacters_SingleQuote = 0x0020;
+
+// to close URLs
+const eEOFCharacters_CloseParen = 0x0040;
+
+// Bridge the char/string divide.
+const APOSTROPHE = "'".charCodeAt(0);
+const ASTERISK = "*".charCodeAt(0);
+const CARRIAGE_RETURN = "\r".charCodeAt(0);
+const CIRCUMFLEX_ACCENT = "^".charCodeAt(0);
+const COMMERCIAL_AT = "@".charCodeAt(0);
+const DIGIT_NINE = "9".charCodeAt(0);
+const DIGIT_ZERO = "0".charCodeAt(0);
+const DOLLAR_SIGN = "$".charCodeAt(0);
+const EQUALS_SIGN = "=".charCodeAt(0);
+const EXCLAMATION_MARK = "!".charCodeAt(0);
+const FULL_STOP = ".".charCodeAt(0);
+const GREATER_THAN_SIGN = ">".charCodeAt(0);
+const HYPHEN_MINUS = "-".charCodeAt(0);
+const LATIN_CAPITAL_LETTER_E = "E".charCodeAt(0);
+const LATIN_CAPITAL_LETTER_U = "U".charCodeAt(0);
+const LATIN_SMALL_LETTER_E = "e".charCodeAt(0);
+const LATIN_SMALL_LETTER_U = "u".charCodeAt(0);
+const LEFT_PARENTHESIS = "(".charCodeAt(0);
+const LESS_THAN_SIGN = "<".charCodeAt(0);
+const LINE_FEED = "\n".charCodeAt(0);
+const NUMBER_SIGN = "#".charCodeAt(0);
+const PERCENT_SIGN = "%".charCodeAt(0);
+const PLUS_SIGN = "+".charCodeAt(0);
+const QUESTION_MARK = "?".charCodeAt(0);
+const QUOTATION_MARK = '"'.charCodeAt(0);
+const REVERSE_SOLIDUS = "\\".charCodeAt(0);
+const RIGHT_PARENTHESIS = ")".charCodeAt(0);
+const SOLIDUS = "/".charCodeAt(0);
+const TILDE = "~".charCodeAt(0);
+const VERTICAL_LINE = "|".charCodeAt(0);
+
+const UCS2_REPLACEMENT_CHAR = 0xfffd;
+
+const kImpliedEOFCharacters = [
+ UCS2_REPLACEMENT_CHAR,
+ ASTERISK,
+ SOLIDUS,
+ QUOTATION_MARK,
+ APOSTROPHE,
+ RIGHT_PARENTHESIS,
+ 0,
+];
+
+//
+const ARGS_LENGTH_MAX = 500 * 1000;
+
+/**
+ * Several methods in this helper can reach the 500000 limit for arguments in
+ * Firefox, see Bug 1414361.
+ *
+ * This will apply the provided method, on the provided scope with an array of
+ * arguments which can exceed the 500k limit supported by Firefox.
+ *
+ * In practice, the arguments array will be split in several chunks of 500k
+ * items maximum and each chunk will be applied separately.
+ *
+ * !! Note that if you are expecting to use the return value of the method, here
+ * we will return an array of each return value for each chunk. It will be up to
+ * the consumer to decide how to combine the results into a meaningful final
+ * result !!
+ *
+ * @param {Function} method
+ * The method to apply.
+ * @param {*} scope
+ * The scope ("this") to use when applying the method.
+ * @param {Array} args
+ * The array of arguments to apply.
+ *
+ * @returns {Array}
+ * The array of return values, one item for each chunk that had to be
+ * created.
+ */
+function safeApply(method, scope, args) {
+ let i = 0;
+ const res = [];
+ const length = args.length;
+ while (i < length) {
+ const _start = i;
+ i += ARGS_LENGTH_MAX;
+ res.push(method.apply(scope, args.slice(_start, i)));
+ }
+ return res;
+}
+
+/**
+ * Ensure that the character is valid. If it is valid, return it;
+ * otherwise, return the replacement character.
+ *
+ * @param {Number} c the character to check
+ * @return {Number} the character or its replacement
+ */
+function ensureValidChar(c) {
+ if (c >= 0x00110000 || (c & 0xfff800) == 0xd800) {
+ // Out of range or a surrogate.
+ return UCS2_REPLACEMENT_CHAR;
+ }
+ return c;
+}
+
+/**
+ * Turn a string into an array of character codes.
+ *
+ * @param {String} str the input string
+ * @return {Array} an array of character codes, one per character in
+ * the input string.
+ */
+function stringToCodes(str) {
+ // This is a hot path, and using a simple for loop is faster than any other mean (e.g.
+ // Array#map ).
+ const charCodes = [];
+ for (let i = 0; i < str.length; i++) {
+ charCodes.push(str.charCodeAt(i));
+ }
+ return charCodes;
+}
+
+const IS_HEX_DIGIT = 0x01;
+const IS_IDSTART = 0x02;
+const IS_IDCHAR = 0x04;
+const IS_URL_CHAR = 0x08;
+const IS_HSPACE = 0x10;
+const IS_VSPACE = 0x20;
+const IS_SPACE = IS_HSPACE | IS_VSPACE;
+const IS_STRING = 0x40;
+
+const H = IS_HSPACE;
+const V = IS_VSPACE;
+const I = IS_IDCHAR;
+const J = IS_IDSTART;
+const U = IS_URL_CHAR;
+const S = IS_STRING;
+const X = IS_HEX_DIGIT;
+
+const SH = S | H;
+const SU = S | U;
+const SUI = S | U | I;
+const SUIJ = S | U | I | J;
+const SUIX = S | U | I | X;
+const SUIJX = S | U | I | J | X;
+
+/* eslint-disable indent, indent-legacy, no-multi-spaces, comma-spacing, spaced-comment */
+const gLexTable = [
+ // 00 01 02 03 04 05 06 07
+ 0,
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ // 08 TAB LF 0B FF CR 0E 0F
+ S,
+ SH,
+ V,
+ S,
+ V,
+ V,
+ S,
+ S,
+ // 10 11 12 13 14 15 16 17
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ // 18 19 1A 1B 1C 1D 1E 1F
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ S,
+ //SPC ! " # $ % & '
+ SH,
+ SU,
+ 0,
+ SU,
+ SU,
+ SU,
+ SU,
+ 0,
+ // ( ) * + , - . /
+ S,
+ S,
+ SU,
+ SU,
+ SU,
+ SUI,
+ SU,
+ SU,
+ // 0 1 2 3 4 5 6 7
+ SUIX,
+ SUIX,
+ SUIX,
+ SUIX,
+ SUIX,
+ SUIX,
+ SUIX,
+ SUIX,
+ // 8 9 : ; < = > ?
+ SUIX,
+ SUIX,
+ SU,
+ SU,
+ SU,
+ SU,
+ SU,
+ SU,
+ // @ A B C D E F G
+ SU,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJ,
+ // H I J K L M N O
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ // P Q R S T U V W
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ // X Y Z [ \ ] ^ _
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SU,
+ J,
+ SU,
+ SU,
+ SUIJ,
+ // ` a b c d e f g
+ SU,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJX,
+ SUIJ,
+ // h i j k l m n o
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ // p q r s t u v w
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ // x y z { | } ~ 7F
+ SUIJ,
+ SUIJ,
+ SUIJ,
+ SU,
+ SU,
+ SU,
+ SU,
+ S,
+];
+/* eslint-enable indent, indent-legacy, no-multi-spaces, comma-spacing, spaced-comment */
+
+/**
+ * True if 'ch' is in character class 'cls', which should be one of
+ * the constants above or some combination of them. All characters
+ * above U+007F are considered to be in 'cls'. EOF is never in 'cls'.
+ */
+function IsOpenCharClass(ch, cls) {
+ return ch >= 0 && (ch >= 128 || (gLexTable[ch] & cls) != 0);
+}
+
+/**
+ * True if 'ch' is in character class 'cls', which should be one of
+ * the constants above or some combination of them. No characters
+ * above U+007F are considered to be in 'cls'. EOF is never in 'cls'.
+ */
+function IsClosedCharClass(ch, cls) {
+ return ch >= 0 && ch < 128 && (gLexTable[ch] & cls) != 0;
+}
+
+/**
+ * True if 'ch' is CSS whitespace, i.e. any of the ASCII characters
+ * TAB, LF, FF, CR, or SPC.
+ */
+function IsWhitespace(ch) {
+ return IsClosedCharClass(ch, IS_SPACE);
+}
+
+/**
+ * True if 'ch' is horizontal whitespace, i.e. TAB or SPC.
+ */
+function IsHorzSpace(ch) {
+ return IsClosedCharClass(ch, IS_HSPACE);
+}
+
+/**
+ * True if 'ch' is vertical whitespace, i.e. LF, FF, or CR. Vertical
+ * whitespace requires special handling when consumed, see AdvanceLine.
+ */
+function IsVertSpace(ch) {
+ return IsClosedCharClass(ch, IS_VSPACE);
+}
+
+/**
+ * True if 'ch' is a character that can appear in the middle of an identifier.
+ * This includes U+0000 since it is handled as U+FFFD, but for purposes of
+ * GatherText it should not be included in IsOpenCharClass.
+ */
+function IsIdentChar(ch) {
+ return IsOpenCharClass(ch, IS_IDCHAR) || ch == 0;
+}
+
+/**
+ * True if 'ch' is a character that by itself begins an identifier.
+ * This includes U+0000 since it is handled as U+FFFD, but for purposes of
+ * GatherText it should not be included in IsOpenCharClass.
+ * (This is a subset of IsIdentChar.)
+ */
+function IsIdentStart(ch) {
+ return IsOpenCharClass(ch, IS_IDSTART) || ch == 0;
+}
+
+/**
+ * True if the two-character sequence aFirstChar+aSecondChar begins an
+ * identifier.
+ */
+function StartsIdent(aFirstChar, aSecondChar) {
+ return (
+ IsIdentStart(aFirstChar) ||
+ (aFirstChar == HYPHEN_MINUS &&
+ (aSecondChar == HYPHEN_MINUS || IsIdentStart(aSecondChar)))
+ );
+}
+
+/**
+ * True if 'ch' is a decimal digit.
+ */
+function IsDigit(ch) {
+ return ch >= DIGIT_ZERO && ch <= DIGIT_NINE;
+}
+
+/**
+ * True if 'ch' is a hexadecimal digit.
+ */
+function IsHexDigit(ch) {
+ return IsClosedCharClass(ch, IS_HEX_DIGIT);
+}
+
+/**
+ * Assuming that 'ch' is a decimal digit, return its numeric value.
+ */
+function DecimalDigitValue(ch) {
+ return ch - DIGIT_ZERO;
+}
+
+/**
+ * Assuming that 'ch' is a hexadecimal digit, return its numeric value.
+ */
+function HexDigitValue(ch) {
+ if (IsDigit(ch)) {
+ return DecimalDigitValue(ch);
+ } else {
+ // Note: c&7 just keeps the low three bits which causes
+ // upper and lower case alphabetics to both yield their
+ // "relative to 10" value for computing the hex value.
+ return (ch & 0x7) + 9;
+ }
+}
+
+/**
+ * If 'ch' can be the first character of a two-character match operator
+ * token, return the token type code for that token, otherwise return
+ * eCSSToken_Symbol to indicate that it can't.
+ */
+function MatchOperatorType(ch) {
+ switch (ch) {
+ case TILDE:
+ return eCSSToken_Includes;
+ case VERTICAL_LINE:
+ return eCSSToken_Dashmatch;
+ case CIRCUMFLEX_ACCENT:
+ return eCSSToken_Beginsmatch;
+ case DOLLAR_SIGN:
+ return eCSSToken_Endsmatch;
+ case ASTERISK:
+ return eCSSToken_Containsmatch;
+ default:
+ return eCSSToken_Symbol;
+ }
+}
+
+function Scanner(buffer) {
+ this.mBuffer = buffer || "";
+ this.mOffset = 0;
+ this.mCount = this.mBuffer.length;
+ this.mLineNumber = 1;
+ this.mLineOffset = 0;
+ this.mTokenLineOffset = 0;
+ this.mTokenOffset = 0;
+ this.mTokenLineNumber = 1;
+ this.mEOFCharacters = eEOFCharacters_None;
+}
+
+Scanner.prototype = {
+ /**
+ * The line number of the most recently returned token. Line
+ * numbers are 0-based.
+ */
+ get lineNumber() {
+ return this.mTokenLineNumber - 1;
+ },
+
+ /**
+ * The column number of the most recently returned token. Column
+ * numbers are 0-based.
+ */
+ get columnNumber() {
+ return this.mTokenOffset - this.mTokenLineOffset;
+ },
+
+ /**
+ * When EOF is reached, the last token might be unterminated in some
+ * ways. This method takes an input string and appends the needed
+ * terminators. In particular:
+ *
+ * 1. If EOF occurs mid-string, this will append the correct quote.
+ * 2. If EOF occurs in a url token, this will append the close paren.
+ * 3. If EOF occurs in a comment this will append the comment closer.
+ *
+ * A trailing backslash might also have been present in the input
+ * string. This is handled in different ways, depending on the
+ * context and arguments.
+ *
+ * If preserveBackslash is true, then the existing backslash at the
+ * end of inputString is preserved, and a new backslash is appended.
+ * That is, the input |\| is transformed to |\\|, and the
+ * input |'\| is transformed to |'\\'|.
+ *
+ * Otherwise, preserveBackslash is false:
+ * If the backslash appears in a string context, then the trailing
+ * backslash is dropped from inputString. That is, |"\| is
+ * transformed to |""|.
+ * If the backslash appears outside of a string context, then
+ * U+FFFD is appended. That is, |\| is transformed to a string
+ * with two characters: backslash followed by U+FFFD.
+ *
+ * Passing false for preserveBackslash makes the result conform to
+ * the CSS Syntax specification. However, passing true may give
+ * somewhat more intuitive behavior.
+ *
+ * @param inputString the input string
+ * @param preserveBackslash how to handle trailing backslashes
+ * @return the input string with the termination characters appended
+ */
+ performEOFFixup(aInputString, aPreserveBackslash) {
+ let result = aInputString;
+
+ let eofChars = this.mEOFCharacters;
+
+ if (
+ aPreserveBackslash &&
+ (eofChars &
+ (eEOFCharacters_DropBackslash | eEOFCharacters_ReplacementChar)) !=
+ 0
+ ) {
+ eofChars &= ~(
+ eEOFCharacters_DropBackslash | eEOFCharacters_ReplacementChar
+ );
+ result += "\\";
+ }
+
+ if (
+ (eofChars & eEOFCharacters_DropBackslash) != 0 &&
+ !!result.length &&
+ result.endsWith("\\")
+ ) {
+ result = result.slice(0, -1);
+ }
+
+ const extra = [];
+ this.AppendImpliedEOFCharacters(eofChars, extra);
+ const asString = String.fromCharCode.apply(null, extra);
+
+ return result + asString;
+ },
+
+ /**
+ * Return the next token, or null at EOF.
+ *
+ * The token object is described by the following WebIDL definition:
+ *
+ * dictionary CSSToken {
+ * // The token type.
+ * CSSTokenType tokenType = "whitespace";
+ *
+ * // Offset of the first character of the token.
+ * unsigned long startOffset = 0;
+ * // Offset of the character after the final character of the token.
+ * // This is chosen so that the offsets can be passed to |substring|
+ * // to yield the exact contents of the token.
+ * unsigned long endOffset = 0;
+ *
+ * // If the token is a number, percentage, or dimension, this holds
+ * // the value. This is not present for other token types.
+ * double number;
+ * // If the token is a number, percentage, or dimension, this is true
+ * // iff the number had an explicit sign. This is not present for
+ * // other token types.
+ * boolean hasSign;
+ * // If the token is a number, percentage, or dimension, this is true
+ * // iff the number was specified as an integer. This is not present
+ * // for other token types.
+ * boolean isInteger;
+ *
+ * // Text associated with the token. This is not present for all
+ * // token types. In particular it is:
+ * //
+ * // Token type Meaning
+ * // ===============================
+ * // ident The identifier.
+ * // function The function name. Note that the "(" is part
+ * // of the token but is not present in |text|.
+ * // at The word.
+ * // id The word.
+ * // hash The word.
+ * // dimension The dimension.
+ * // string The string contents after escape processing.
+ * // bad_string Ditto.
+ * // url The URL after escape processing.
+ * // bad_url Ditto.
+ * // symbol The symbol text.
+ * DOMString text;
+ * };
+ */
+ nextToken() {
+ const token = {};
+ if (!this.Next(token)) {
+ return null;
+ }
+
+ const resultToken = {};
+ resultToken.tokenType = token.mType;
+ resultToken.startOffset = this.mTokenOffset;
+ resultToken.endOffset = this.mOffset;
+ const constructText = () => {
+ return safeApply(String.fromCharCode, null, token.mIdent).join("");
+ };
+
+ switch (token.mType) {
+ case eCSSToken_Whitespace:
+ break;
+
+ case eCSSToken_Ident:
+ case eCSSToken_Function:
+ case eCSSToken_AtKeyword:
+ case eCSSToken_ID:
+ case eCSSToken_Hash:
+ resultToken.text = constructText();
+ break;
+
+ case eCSSToken_Dimension:
+ resultToken.text = constructText();
+ /* Fall through. */
+ case eCSSToken_Number:
+ case eCSSToken_Percentage:
+ resultToken.number = token.mNumber;
+ resultToken.hasSign = token.mHasSign;
+ resultToken.isInteger = token.mIntegerValid;
+ break;
+
+ case eCSSToken_String:
+ case eCSSToken_Bad_String:
+ case eCSSToken_URL:
+ case eCSSToken_Bad_URL:
+ resultToken.text = constructText();
+ /* Don't bother emitting the delimiter, as it is readily extracted
+ from the source string when needed. */
+ break;
+
+ case eCSSToken_Symbol:
+ resultToken.text = String.fromCharCode(token.mSymbol);
+ break;
+
+ case eCSSToken_Includes:
+ case eCSSToken_Dashmatch:
+ case eCSSToken_Beginsmatch:
+ case eCSSToken_Endsmatch:
+ case eCSSToken_Containsmatch:
+ case eCSSToken_URange:
+ break;
+
+ case eCSSToken_Comment:
+ case eCSSToken_HTMLComment:
+ /* The comment text is easily extracted from the source string,
+ and is rarely useful. */
+ break;
+ }
+
+ return resultToken;
+ },
+
+ /**
+ * Return the raw UTF-16 code unit at position |this.mOffset + n| within
+ * the read buffer. If that is beyond the end of the buffer, returns
+ * -1 to indicate end of input.
+ */
+ Peek(n = 0) {
+ if (this.mOffset + n >= this.mCount) {
+ return -1;
+ }
+ return this.mBuffer.charCodeAt(this.mOffset + n);
+ },
+
+ /**
+ * Advance |this.mOffset| over |n| code units. Advance(0) is a no-op.
+ * If |n| is greater than the distance to end of input, will silently
+ * stop at the end. May not be used to advance over a line boundary;
+ * AdvanceLine() must be used instead.
+ */
+ Advance(n = 1) {
+ if (this.mOffset + n >= this.mCount || this.mOffset + n < this.mOffset) {
+ this.mOffset = this.mCount;
+ } else {
+ this.mOffset += n;
+ }
+ },
+
+ /**
+ * Advance |this.mOffset| over a line boundary.
+ */
+ AdvanceLine() {
+ // Advance over \r\n as a unit.
+ if (
+ this.mBuffer.charCodeAt(this.mOffset) == CARRIAGE_RETURN &&
+ this.mOffset + 1 < this.mCount &&
+ this.mBuffer.charCodeAt(this.mOffset + 1) == LINE_FEED
+ ) {
+ this.mOffset += 2;
+ } else {
+ this.mOffset += 1;
+ }
+ // 0 is a magical line number meaning that we don't know (i.e., script)
+ if (this.mLineNumber != 0) {
+ this.mLineNumber++;
+ }
+ this.mLineOffset = this.mOffset;
+ },
+
+ /**
+ * Skip over a sequence of whitespace characters (vertical or
+ * horizontal) starting at the current read position.
+ */
+ SkipWhitespace() {
+ for (;;) {
+ const ch = this.Peek();
+ if (!IsWhitespace(ch)) {
+ // EOF counts as non-whitespace
+ break;
+ }
+ if (IsVertSpace(ch)) {
+ this.AdvanceLine();
+ } else {
+ this.Advance();
+ }
+ }
+ },
+
+ /**
+ * Skip over one CSS comment starting at the current read position.
+ */
+ SkipComment() {
+ this.Advance(2);
+ for (;;) {
+ let ch = this.Peek();
+ if (ch < 0) {
+ this.SetEOFCharacters(eEOFCharacters_Asterisk | eEOFCharacters_Slash);
+ return;
+ }
+ if (ch == ASTERISK) {
+ this.Advance();
+ ch = this.Peek();
+ if (ch < 0) {
+ this.SetEOFCharacters(eEOFCharacters_Slash);
+ return;
+ }
+ if (ch == SOLIDUS) {
+ this.Advance();
+ return;
+ }
+ } else if (IsVertSpace(ch)) {
+ this.AdvanceLine();
+ } else {
+ this.Advance();
+ }
+ }
+ },
+
+ /**
+ * If there is a valid escape sequence starting at the current read
+ * position, consume it, decode it, append the result to |aOutput|,
+ * and return true. Otherwise, consume nothing, leave |aOutput|
+ * unmodified, and return false. If |aInString| is true, accept the
+ * additional form of escape sequence allowed within string-like tokens.
+ */
+ GatherEscape(aOutput, aInString) {
+ let ch = this.Peek(1);
+ if (ch < 0) {
+ // If we are in a string (or a url() containing a string), we want to drop
+ // the backslash on the floor. Otherwise, we want to treat it as a U+FFFD
+ // character.
+ this.Advance();
+ if (aInString) {
+ this.SetEOFCharacters(eEOFCharacters_DropBackslash);
+ } else {
+ aOutput.push(UCS2_REPLACEMENT_CHAR);
+ this.SetEOFCharacters(eEOFCharacters_ReplacementChar);
+ }
+ return true;
+ }
+ if (IsVertSpace(ch)) {
+ if (aInString) {
+ // In strings (and in url() containing a string), escaped
+ // newlines are completely removed, to allow splitting over
+ // multiple lines.
+ this.Advance();
+ this.AdvanceLine();
+ return true;
+ }
+ // Outside of strings, backslash followed by a newline is not an escape.
+ return false;
+ }
+
+ if (!IsHexDigit(ch)) {
+ // "Any character (except a hexadecimal digit, linefeed, carriage
+ // return, or form feed) can be escaped with a backslash to remove
+ // its special meaning." -- CSS2.1 section 4.1.3
+ this.Advance(2);
+ if (ch == 0) {
+ aOutput.push(UCS2_REPLACEMENT_CHAR);
+ } else {
+ aOutput.push(ch);
+ }
+ return true;
+ }
+
+ // "[at most six hexadecimal digits following a backslash] stand
+ // for the ISO 10646 character with that number, which must not be
+ // zero. (It is undefined in CSS 2.1 what happens if a style sheet
+ // does contain a character with Unicode codepoint zero.)"
+ // -- CSS2.1 section 4.1.3
+
+ // At this point we know we have \ followed by at least one
+ // hexadecimal digit, therefore the escape sequence is valid and we
+ // can go ahead and consume the backslash.
+ this.Advance();
+ let val = 0;
+ let i = 0;
+ do {
+ val = val * 16 + HexDigitValue(ch);
+ i++;
+ this.Advance();
+ ch = this.Peek();
+ } while (i < 6 && IsHexDigit(ch));
+
+ // "Interpret the hex digits as a hexadecimal number. If this
+ // number is zero, or is greater than the maximum allowed
+ // codepoint, return U+FFFD REPLACEMENT CHARACTER" -- CSS Syntax
+ // Level 3
+ if (val == 0) {
+ aOutput.push(UCS2_REPLACEMENT_CHAR);
+ } else {
+ aOutput.push(ensureValidChar(val));
+ }
+
+ // Consume exactly one whitespace character after a
+ // hexadecimal escape sequence.
+ if (IsVertSpace(ch)) {
+ this.AdvanceLine();
+ } else if (IsHorzSpace(ch)) {
+ this.Advance();
+ }
+ return true;
+ },
+
+ /**
+ * Consume a run of "text" beginning with the current read position,
+ * consisting of characters in the class |aClass| (which must be a
+ * suitable argument to IsOpenCharClass) plus escape sequences.
+ * Append the text to |aText|, after decoding escape sequences.
+ *
+ * Returns true if at least one character was appended to |aText|,
+ * false otherwise.
+ */
+ GatherText(aClass, aText) {
+ const start = this.mOffset;
+ const inString = aClass == IS_STRING;
+
+ for (;;) {
+ // Consume runs of unescaped characters in one go.
+ let n = this.mOffset;
+ while (
+ n < this.mCount &&
+ IsOpenCharClass(this.mBuffer.charCodeAt(n), aClass)
+ ) {
+ n++;
+ }
+ if (n > this.mOffset) {
+ const codes = stringToCodes(this.mBuffer.slice(this.mOffset, n));
+ safeApply(Array.prototype.push, aText, codes);
+ this.mOffset = n;
+ }
+ if (n == this.mCount) {
+ break;
+ }
+
+ const ch = this.Peek();
+ if (ch == 0) {
+ this.Advance();
+ aText.push(UCS2_REPLACEMENT_CHAR);
+ continue;
+ }
+
+ if (ch != REVERSE_SOLIDUS) {
+ break;
+ }
+ if (!this.GatherEscape(aText, inString)) {
+ break;
+ }
+ }
+
+ return this.mOffset > start;
+ },
+
+ /**
+ * Scan an Ident token. This also handles Function and URL tokens,
+ * both of which begin indistinguishably from an identifier. It can
+ * produce a Symbol token when an apparent identifier actually led
+ * into an invalid escape sequence.
+ */
+ ScanIdent(aToken) {
+ if (!this.GatherText(IS_IDCHAR, aToken.mIdent)) {
+ aToken.mSymbol = this.Peek();
+ this.Advance();
+ return true;
+ }
+
+ if (this.Peek() != LEFT_PARENTHESIS) {
+ aToken.mType = eCSSToken_Ident;
+ return true;
+ }
+
+ this.Advance();
+ aToken.mType = eCSSToken_Function;
+
+ const asString = String.fromCharCode.apply(null, aToken.mIdent);
+ if (asString.toLowerCase() === "url") {
+ this.NextURL(aToken);
+ }
+ return true;
+ },
+
+ /**
+ * Scan an AtKeyword token. Also handles production of Symbol when
+ * an '@' is not followed by an identifier.
+ */
+ ScanAtKeyword(aToken) {
+ // Fall back for when '@' isn't followed by an identifier.
+ aToken.mSymbol = COMMERCIAL_AT;
+ this.Advance();
+
+ const ch = this.Peek();
+ if (StartsIdent(ch, this.Peek(1))) {
+ if (this.GatherText(IS_IDCHAR, aToken.mIdent)) {
+ aToken.mType = eCSSToken_AtKeyword;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Scan a Hash token. Handles the distinction between eCSSToken_ID
+ * and eCSSToken_Hash, and handles production of Symbol when a '#'
+ * is not followed by identifier characters.
+ */
+ ScanHash(aToken) {
+ // Fall back for when '#' isn't followed by identifier characters.
+ aToken.mSymbol = NUMBER_SIGN;
+ this.Advance();
+
+ const ch = this.Peek();
+ if (IsIdentChar(ch) || ch == REVERSE_SOLIDUS) {
+ const type = StartsIdent(ch, this.Peek(1))
+ ? eCSSToken_ID
+ : eCSSToken_Hash;
+ aToken.mIdent.length = 0;
+ if (this.GatherText(IS_IDCHAR, aToken.mIdent)) {
+ aToken.mType = type;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Scan a Number, Percentage, or Dimension token (all of which begin
+ * like a Number). Can produce a Symbol when a '.' is not followed by
+ * digits, or when '+' or '-' are not followed by either a digit or a
+ * '.' and then a digit. Can also produce a HTMLComment when it
+ * encounters '-->'.
+ */
+ ScanNumber(aToken) {
+ let c = this.Peek();
+
+ // Sign of the mantissa (-1 or 1).
+ const sign = c == HYPHEN_MINUS ? -1 : 1;
+ // Absolute value of the integer part of the mantissa. This is a double so
+ // we don't run into overflow issues for consumers that only care about our
+ // floating-point value while still being able to express the full int32_t
+ // range for consumers who want integers.
+ let intPart = 0;
+ // Fractional part of the mantissa. This is a double so that when
+ // we convert to float at the end we'll end up rounding to nearest
+ // float instead of truncating down (as we would if fracPart were
+ // a float and we just effectively lost the last several digits).
+ let fracPart = 0;
+ // Absolute value of the power of 10 that we should multiply by
+ // (only relevant for numbers in scientific notation). Has to be
+ // a signed integer, because multiplication of signed by unsigned
+ // converts the unsigned to signed, so if we plan to actually
+ // multiply by expSign...
+ let exponent = 0;
+ // Sign of the exponent.
+ let expSign = 1;
+
+ aToken.mHasSign = c == PLUS_SIGN || c == HYPHEN_MINUS;
+ if (aToken.mHasSign) {
+ this.Advance();
+ c = this.Peek();
+ }
+
+ let gotDot = c == FULL_STOP;
+
+ if (!gotDot) {
+ // Scan the integer part of the mantissa.
+ do {
+ intPart = 10 * intPart + DecimalDigitValue(c);
+ this.Advance();
+ c = this.Peek();
+ } while (IsDigit(c));
+
+ gotDot = c == FULL_STOP && IsDigit(this.Peek(1));
+ }
+
+ if (gotDot) {
+ // Scan the fractional part of the mantissa.
+ this.Advance();
+ c = this.Peek();
+ // Power of ten by which we need to divide our next digit
+ let divisor = 10;
+ do {
+ fracPart += DecimalDigitValue(c) / divisor;
+ divisor *= 10;
+ this.Advance();
+ c = this.Peek();
+ } while (IsDigit(c));
+ }
+
+ let gotE = false;
+ if (c == LATIN_SMALL_LETTER_E || c == LATIN_CAPITAL_LETTER_E) {
+ const expSignChar = this.Peek(1);
+ const nextChar = this.Peek(2);
+ if (
+ IsDigit(expSignChar) ||
+ ((expSignChar == HYPHEN_MINUS || expSignChar == PLUS_SIGN) &&
+ IsDigit(nextChar))
+ ) {
+ gotE = true;
+ if (expSignChar == HYPHEN_MINUS) {
+ expSign = -1;
+ }
+ this.Advance(); // consumes the E
+ if (expSignChar == HYPHEN_MINUS || expSignChar == PLUS_SIGN) {
+ this.Advance();
+ c = nextChar;
+ } else {
+ c = expSignChar;
+ }
+ do {
+ exponent = 10 * exponent + DecimalDigitValue(c);
+ this.Advance();
+ c = this.Peek();
+ } while (IsDigit(c));
+ }
+ }
+
+ let type = eCSSToken_Number;
+
+ // Set mIntegerValid for all cases (except %, below) because we need
+ // it for the "2n" in :nth-child(2n).
+ aToken.mIntegerValid = false;
+
+ // Time to reassemble our number.
+ // Do all the math in double precision so it's truncated only once.
+ let value = sign * (intPart + fracPart);
+ if (gotE) {
+ // Explicitly cast expSign*exponent to double to avoid issues with
+ // overloaded pow() on Windows.
+ value *= Math.pow(10.0, expSign * exponent);
+ } else if (!gotDot) {
+ // Clamp values outside of integer range.
+ if (sign > 0) {
+ aToken.mInteger = Math.min(intPart, Number.MAX_SAFE_INTEGER);
+ } else {
+ aToken.mInteger = Math.max(-intPart, Number.MIN_SAFE_INTEGER);
+ }
+ aToken.mIntegerValid = true;
+ }
+
+ const ident = aToken.mIdent;
+
+ // Check for Dimension and Percentage tokens.
+ if (c >= 0) {
+ if (StartsIdent(c, this.Peek(1))) {
+ if (this.GatherText(IS_IDCHAR, ident)) {
+ type = eCSSToken_Dimension;
+ }
+ } else if (c == PERCENT_SIGN) {
+ this.Advance();
+ type = eCSSToken_Percentage;
+ value = value / 100.0;
+ aToken.mIntegerValid = false;
+ }
+ }
+ aToken.mNumber = value;
+ aToken.mType = type;
+ return true;
+ },
+
+ /**
+ * Scan a string constant ('foo' or "foo"). Will always produce
+ * either a String or a Bad_String token; the latter occurs when the
+ * close quote is missing. Always returns true (for convenience in Next()).
+ */
+ ScanString(aToken) {
+ const aStop = this.Peek();
+ aToken.mType = eCSSToken_String;
+ aToken.mSymbol = aStop; // Remember how it's quoted.
+ this.Advance();
+
+ for (;;) {
+ this.GatherText(IS_STRING, aToken.mIdent);
+
+ const ch = this.Peek();
+ if (ch == -1) {
+ this.AddEOFCharacters(
+ aStop == QUOTATION_MARK
+ ? eEOFCharacters_DoubleQuote
+ : eEOFCharacters_SingleQuote
+ );
+ break; // EOF ends a string token with no error.
+ }
+ if (ch == aStop) {
+ this.Advance();
+ break;
+ }
+ // Both " and ' are excluded from IS_STRING.
+ if (ch == QUOTATION_MARK || ch == APOSTROPHE) {
+ aToken.mIdent.push(ch);
+ this.Advance();
+ continue;
+ }
+
+ aToken.mType = eCSSToken_Bad_String;
+ break;
+ }
+ return true;
+ },
+
+ /**
+ * Scan a unicode-range token. These match the regular expression
+ *
+ * u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?
+ *
+ * However, some such tokens are "invalid". There are three valid forms:
+ *
+ * u+[0-9a-f]{x} 1 <= x <= 6
+ * u+[0-9a-f]{x}\?{y} 1 <= x+y <= 6
+ * u+[0-9a-f]{x}-[0-9a-f]{y} 1 <= x <= 6, 1 <= y <= 6
+ *
+ * All unicode-range tokens have their text recorded in mIdent; valid ones
+ * are also decoded into mInteger and mInteger2, and mIntegerValid is set.
+ * Note that this does not validate the numeric range, only the syntactic
+ * form.
+ */
+ ScanURange(aResult) {
+ const intro1 = this.Peek();
+ const intro2 = this.Peek(1);
+ let ch = this.Peek(2);
+
+ aResult.mIdent.push(intro1);
+ aResult.mIdent.push(intro2);
+ this.Advance(2);
+
+ let valid = true;
+ let haveQues = false;
+ let low = 0;
+ let high = 0;
+ let i = 0;
+
+ do {
+ aResult.mIdent.push(ch);
+ if (IsHexDigit(ch)) {
+ if (haveQues) {
+ valid = false; // All question marks should be at the end.
+ }
+ low = low * 16 + HexDigitValue(ch);
+ high = high * 16 + HexDigitValue(ch);
+ } else {
+ haveQues = true;
+ low = low * 16 + 0x0;
+ high = high * 16 + 0xf;
+ }
+
+ i++;
+ this.Advance();
+ ch = this.Peek();
+ } while (i < 6 && (IsHexDigit(ch) || ch == QUESTION_MARK));
+
+ if (ch == HYPHEN_MINUS && IsHexDigit(this.Peek(1))) {
+ if (haveQues) {
+ valid = false;
+ }
+
+ aResult.mIdent.push(ch);
+ this.Advance();
+ ch = this.Peek();
+ high = 0;
+ i = 0;
+ do {
+ aResult.mIdent.push(ch);
+ high = high * 16 + HexDigitValue(ch);
+
+ i++;
+ this.Advance();
+ ch = this.Peek();
+ } while (i < 6 && IsHexDigit(ch));
+ }
+
+ aResult.mInteger = low;
+ aResult.mInteger2 = high;
+ aResult.mIntegerValid = valid;
+ aResult.mType = eCSSToken_URange;
+ return true;
+ },
+
+ SetEOFCharacters(aEOFCharacters) {
+ this.mEOFCharacters = aEOFCharacters;
+ },
+
+ AddEOFCharacters(aEOFCharacters) {
+ this.mEOFCharacters = this.mEOFCharacters | aEOFCharacters;
+ },
+
+ AppendImpliedEOFCharacters(aEOFCharacters, aResult) {
+ // First, ignore eEOFCharacters_DropBackslash.
+ let c = aEOFCharacters >> 1;
+
+ // All of the remaining EOFCharacters bits represent appended characters,
+ // and the bits are in the order that they need appending.
+ for (const p of kImpliedEOFCharacters) {
+ if (c & 1) {
+ aResult.push(p);
+ }
+ c >>= 1;
+ }
+ },
+
+ /**
+ * Consume the part of an URL token after the initial 'url('. Caller
+ * is assumed to have consumed 'url(' already. Will always produce
+ * either an URL or a Bad_URL token.
+ *
+ * Exposed for use by nsCSSParser::ParseMozDocumentRule, which applies
+ * the special lexical rules for URL tokens in a nonstandard context.
+ */
+ NextURL(aToken) {
+ this.SkipWhitespace();
+
+ // aToken.mIdent may be "url" at this point; clear that out
+ aToken.mIdent.length = 0;
+
+ let hasString = false;
+ let ch = this.Peek();
+ // Do we have a string?
+ if (ch == QUOTATION_MARK || ch == APOSTROPHE) {
+ this.ScanString(aToken);
+ if (aToken.mType == eCSSToken_Bad_String) {
+ aToken.mType = eCSSToken_Bad_URL;
+ return;
+ }
+ hasString = true;
+ } else {
+ // Otherwise, this is the start of a non-quoted url (which may be empty).
+ aToken.mSymbol = 0;
+ this.GatherText(IS_URL_CHAR, aToken.mIdent);
+ }
+
+ // Consume trailing whitespace and then look for a close parenthesis.
+ this.SkipWhitespace();
+ ch = this.Peek();
+ // ch can be less than zero indicating EOF
+ if (ch < 0 || ch == RIGHT_PARENTHESIS) {
+ this.Advance();
+ aToken.mType = eCSSToken_URL;
+ if (ch < 0) {
+ this.AddEOFCharacters(eEOFCharacters_CloseParen);
+ }
+ } else {
+ aToken.mType = eCSSToken_Bad_URL;
+ if (!hasString) {
+ // Consume until before the next right parenthesis, which follows
+ // how <bad-url-token> is consumed in CSS Syntax 3 spec.
+ // Note that, we only do this when "url(" is not followed by a
+ // string, because in the spec, "url(" followed by a string is
+ // handled as a url function rather than a <url-token>, so the
+ // rest of content before ")" should be consumed in balance,
+ // which will be done by the parser.
+ // The closing ")" is not consumed here. It is left to the parser
+ // so that the parser can handle both cases.
+ do {
+ if (IsVertSpace(ch)) {
+ this.AdvanceLine();
+ } else {
+ this.Advance();
+ }
+ ch = this.Peek();
+ } while (ch >= 0 && ch != RIGHT_PARENTHESIS);
+ }
+ }
+ },
+
+ /**
+ * Primary scanner entry point. Consume one token and fill in
+ * |aToken| accordingly. Will skip over any number of comments first,
+ * and will also skip over rather than return whitespace and comment
+ * tokens, depending on the value of |aSkip|.
+ *
+ * Returns true if it successfully consumed a token, false if EOF has
+ * been reached. Will always advance the current read position by at
+ * least one character unless called when already at EOF.
+ */
+ Next(aToken, aSkip) {
+ // do this here so we don't have to do it in dozens of other places
+ aToken.mIdent = [];
+ aToken.mType = eCSSToken_Symbol;
+
+ this.mTokenOffset = this.mOffset;
+ this.mTokenLineOffset = this.mLineOffset;
+ this.mTokenLineNumber = this.mLineNumber;
+
+ const ch = this.Peek();
+ if (IsWhitespace(ch)) {
+ this.SkipWhitespace();
+ aToken.mType = eCSSToken_Whitespace;
+ return true;
+ }
+ if (
+ ch == SOLIDUS && // !IsSVGMode() &&
+ this.Peek(1) == ASTERISK
+ ) {
+ this.SkipComment();
+ aToken.mType = eCSSToken_Comment;
+ return true;
+ }
+
+ // EOF
+ if (ch < 0) {
+ return false;
+ }
+
+ // 'u' could be UNICODE-RANGE or an identifier-family token
+ if (ch == LATIN_SMALL_LETTER_U || ch == LATIN_CAPITAL_LETTER_U) {
+ const c2 = this.Peek(1);
+ const c3 = this.Peek(2);
+ if (c2 == PLUS_SIGN && (IsHexDigit(c3) || c3 == QUESTION_MARK)) {
+ return this.ScanURange(aToken);
+ }
+ return this.ScanIdent(aToken);
+ }
+
+ // identifier family
+ if (IsIdentStart(ch)) {
+ return this.ScanIdent(aToken);
+ }
+
+ // number family
+ if (IsDigit(ch)) {
+ return this.ScanNumber(aToken);
+ }
+
+ if (ch == FULL_STOP && IsDigit(this.Peek(1))) {
+ return this.ScanNumber(aToken);
+ }
+
+ if (ch == PLUS_SIGN) {
+ const c2 = this.Peek(1);
+ if (IsDigit(c2) || (c2 == FULL_STOP && IsDigit(this.Peek(2)))) {
+ return this.ScanNumber(aToken);
+ }
+ }
+
+ // HYPHEN_MINUS can start an identifier-family token, a number-family token,
+ // or an HTML-comment
+ if (ch == HYPHEN_MINUS) {
+ const c2 = this.Peek(1);
+ const c3 = this.Peek(2);
+ if (IsIdentStart(c2) || (c2 == HYPHEN_MINUS && c3 != GREATER_THAN_SIGN)) {
+ return this.ScanIdent(aToken);
+ }
+ if (IsDigit(c2) || (c2 == FULL_STOP && IsDigit(c3))) {
+ return this.ScanNumber(aToken);
+ }
+ if (c2 == HYPHEN_MINUS && c3 == GREATER_THAN_SIGN) {
+ this.Advance(3);
+ aToken.mType = eCSSToken_HTMLComment;
+ aToken.mIdent = stringToCodes("-->");
+ return true;
+ }
+ }
+
+ // the other HTML-comment token
+ if (
+ ch == LESS_THAN_SIGN &&
+ this.Peek(1) == EXCLAMATION_MARK &&
+ this.Peek(2) == HYPHEN_MINUS &&
+ this.Peek(3) == HYPHEN_MINUS
+ ) {
+ this.Advance(4);
+ aToken.mType = eCSSToken_HTMLComment;
+ aToken.mIdent = stringToCodes("<!--");
+ return true;
+ }
+
+ // AT_KEYWORD
+ if (ch == COMMERCIAL_AT) {
+ return this.ScanAtKeyword(aToken);
+ }
+
+ // HASH
+ if (ch == NUMBER_SIGN) {
+ return this.ScanHash(aToken);
+ }
+
+ // STRING
+ if (ch == QUOTATION_MARK || ch == APOSTROPHE) {
+ return this.ScanString(aToken);
+ }
+
+ // Match operators: ~= |= ^= $= *=
+ const opType = MatchOperatorType(ch);
+ if (opType != eCSSToken_Symbol && this.Peek(1) == EQUALS_SIGN) {
+ aToken.mType = opType;
+ this.Advance(2);
+ return true;
+ }
+
+ // Otherwise, a symbol (DELIM).
+ aToken.mSymbol = ch;
+ this.Advance();
+ return true;
+ },
+};
+
+/**
+ * Create and return a new CSS lexer.
+ *
+ * @param {String} input the CSS text to lex
+ * @return {CSSLexer} the new lexer
+ */
+function getCSSLexer(input) {
+ return new Scanner(input);
+}
+
+exports.getCSSLexer = getCSSLexer;
diff --git a/devtools/shared/css/moz.build b/devtools/shared/css/moz.build
new file mode 100644
index 0000000000..97231df661
--- /dev/null
+++ b/devtools/shared/css/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(
+ "color-db.js",
+ "color.js",
+ "constants.js",
+ "lexer.js",
+ "parsing-utils.js",
+)
diff --git a/devtools/shared/css/parsing-utils.js b/devtools/shared/css/parsing-utils.js
new file mode 100644
index 0000000000..6234eb3255
--- /dev/null
+++ b/devtools/shared/css/parsing-utils.js
@@ -0,0 +1,783 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file holds various CSS parsing and rewriting utilities.
+// Some entry points of note are:
+// parseDeclarations - parse a CSS rule into declarations
+// parsePseudoClassesAndAttributes - parse selector and extract
+// pseudo-classes
+// parseSingleValue - parse a single CSS property value
+
+"use strict";
+
+const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
+
+loader.lazyRequireGetter(
+ this,
+ "CSS_ANGLEUNIT",
+ "resource://devtools/shared/css/constants.js",
+ true
+);
+
+const SELECTOR_ATTRIBUTE = (exports.SELECTOR_ATTRIBUTE = 1);
+const SELECTOR_ELEMENT = (exports.SELECTOR_ELEMENT = 2);
+const SELECTOR_PSEUDO_CLASS = (exports.SELECTOR_PSEUDO_CLASS = 3);
+const CSS_BLOCKS = { "(": ")", "[": "]" };
+
+// When commenting out a declaration, we put this character into the
+// comment opener so that future parses of the commented text know to
+// bypass the property name validity heuristic.
+const COMMENT_PARSING_HEURISTIC_BYPASS_CHAR =
+ (exports.COMMENT_PARSING_HEURISTIC_BYPASS_CHAR = "!");
+
+/**
+ * A generator function that lexes a CSS source string, yielding the
+ * CSS tokens. Comment tokens are dropped.
+ *
+ * @param {String} CSS source string
+ * @yield {CSSToken} The next CSSToken that is lexed
+ * @see CSSToken for details about the returned tokens
+ */
+function* cssTokenizer(string) {
+ const lexer = getCSSLexer(string);
+ while (true) {
+ const token = lexer.nextToken();
+ if (!token) {
+ break;
+ }
+ // None of the existing consumers want comments.
+ if (token.tokenType !== "comment") {
+ yield token;
+ }
+ }
+}
+
+/**
+ * Pass |string| to the CSS lexer and return an array of all the
+ * returned tokens. Comment tokens are not included. In addition to
+ * the usual information, each token will have starting and ending
+ * line and column information attached. Specifically, each token
+ * has an additional "loc" attribute. This attribute is an object
+ * of the form {line: L, column: C}. Lines and columns are both zero
+ * based.
+ *
+ * It's best not to add new uses of this function. In general it is
+ * simpler and better to use the CSSToken offsets, rather than line
+ * and column. Also, this function lexes the entire input string at
+ * once, rather than lazily yielding a token stream. Use
+ * |cssTokenizer| or |getCSSLexer| instead.
+ *
+ * @param{String} string The input string.
+ * @return {Array} An array of tokens (@see CSSToken) that have
+ * line and column information.
+ */
+function cssTokenizerWithLineColumn(string) {
+ const lexer = getCSSLexer(string);
+ const result = [];
+ let prevToken = undefined;
+ while (true) {
+ const token = lexer.nextToken();
+ const lineNumber = lexer.lineNumber;
+ const columnNumber = lexer.columnNumber;
+
+ if (prevToken) {
+ prevToken.loc.end = {
+ line: lineNumber,
+ column: columnNumber,
+ };
+ }
+
+ if (!token) {
+ break;
+ }
+
+ if (token.tokenType === "comment") {
+ // We've already dealt with the previous token's location.
+ prevToken = undefined;
+ } else {
+ const startLoc = {
+ line: lineNumber,
+ column: columnNumber,
+ };
+ token.loc = { start: startLoc };
+
+ result.push(token);
+ prevToken = token;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Escape a comment body. Find the comment start and end strings in a
+ * string and inserts backslashes so that the resulting text can
+ * itself be put inside a comment.
+ *
+ * @param {String} inputString
+ * input string
+ * @return {String} the escaped result
+ */
+function escapeCSSComment(inputString) {
+ const result = inputString.replace(/\/(\\*)\*/g, "/\\$1*");
+ return result.replace(/\*(\\*)\//g, "*\\$1/");
+}
+
+/**
+ * Un-escape a comment body. This undoes any comment escaping that
+ * was done by escapeCSSComment. That is, given input like "/\*
+ * comment *\/", it will strip the backslashes.
+ *
+ * @param {String} inputString
+ * input string
+ * @return {String} the un-escaped result
+ */
+function unescapeCSSComment(inputString) {
+ const result = inputString.replace(/\/\\(\\*)\*/g, "/$1*");
+ return result.replace(/\*\\(\\*)\//g, "*$1/");
+}
+
+/**
+ * A helper function for @see parseDeclarations that handles parsing
+ * of comment text. This wraps a recursive call to parseDeclarations
+ * with the processing needed to ensure that offsets in the result
+ * refer back to the original, unescaped, input string.
+ *
+ * @param {Function} isCssPropertyKnown
+ * A function to check if the CSS property is known. This is either an
+ * internal server function or from the CssPropertiesFront.
+ * @param {String} commentText The text of the comment, without the
+ * delimiters.
+ * @param {Number} startOffset The offset of the comment opener
+ * in the original text.
+ * @param {Number} endOffset The offset of the comment closer
+ * in the original text.
+ * @return {array} Array of declarations of the same form as returned
+ * by parseDeclarations.
+ */
+function parseCommentDeclarations(
+ isCssPropertyKnown,
+ commentText,
+ startOffset,
+ endOffset
+) {
+ let commentOverride = false;
+ if (commentText === "") {
+ return [];
+ } else if (commentText[0] === COMMENT_PARSING_HEURISTIC_BYPASS_CHAR) {
+ // This is the special sign that the comment was written by
+ // rewriteDeclarations and so we should bypass the usual
+ // heuristic.
+ commentOverride = true;
+ commentText = commentText.substring(1);
+ }
+
+ const rewrittenText = unescapeCSSComment(commentText);
+
+ // We might have rewritten an embedded comment. For example
+ // /\* ... *\/ would turn into /* ... */.
+ // This rewriting is necessary for proper lexing, but it means
+ // that the offsets we get back can be off. So now we compute
+ // a map so that we can rewrite offsets later. The map is the same
+ // length as |rewrittenText| and tells us how to map an index
+ // into |rewrittenText| to an index into |commentText|.
+ //
+ // First, we find the location of each comment starter or closer in
+ // |rewrittenText|. At these spots we put a 1 into |rewrites|.
+ // Then we walk the array again, using the elements to compute a
+ // delta, which we use to make the final mapping.
+ //
+ // Note we allocate one extra entry because we can see an ending
+ // offset that is equal to the length.
+ const rewrites = new Array(rewrittenText.length + 1).fill(0);
+
+ const commentRe = /\/\\*\*|\*\\*\//g;
+ while (true) {
+ const matchData = commentRe.exec(rewrittenText);
+ if (!matchData) {
+ break;
+ }
+ rewrites[matchData.index] = 1;
+ }
+
+ let delta = 0;
+ for (let i = 0; i <= rewrittenText.length; ++i) {
+ delta += rewrites[i];
+ // |startOffset| to add the offset from the comment starter, |+2|
+ // for the length of the "/*", then |i| and |delta| as described
+ // above.
+ rewrites[i] = startOffset + 2 + i + delta;
+ if (commentOverride) {
+ ++rewrites[i];
+ }
+ }
+
+ // Note that we pass "false" for parseComments here. It doesn't
+ // seem worthwhile to support declarations in comments-in-comments
+ // here, as there's no way to generate those using the tools, and
+ // users would be crazy to write such things.
+ const newDecls = parseDeclarationsInternal(
+ isCssPropertyKnown,
+ rewrittenText,
+ false,
+ true,
+ commentOverride
+ );
+ for (const decl of newDecls) {
+ decl.offsets[0] = rewrites[decl.offsets[0]];
+ decl.offsets[1] = rewrites[decl.offsets[1]];
+ decl.colonOffsets[0] = rewrites[decl.colonOffsets[0]];
+ decl.colonOffsets[1] = rewrites[decl.colonOffsets[1]];
+ decl.commentOffsets = [startOffset, endOffset];
+ }
+ return newDecls;
+}
+
+/**
+ * A helper function for parseDeclarationsInternal that creates a new
+ * empty declaration.
+ *
+ * @return {object} an empty declaration of the form returned by
+ * parseDeclarations
+ */
+function getEmptyDeclaration() {
+ return {
+ name: "",
+ value: "",
+ priority: "",
+ terminator: "",
+ offsets: [undefined, undefined],
+ colonOffsets: false,
+ };
+}
+
+/**
+ * Like trim, but only trims CSS-allowed whitespace.
+ */
+function cssTrim(str) {
+ const match = /^[ \t\r\n\f]*(.*?)[ \t\r\n\f]*$/.exec(str);
+ if (match) {
+ return match[1];
+ }
+ return str;
+}
+
+/**
+ * A helper function that does all the parsing work for
+ * parseDeclarations. This is separate because it has some arguments
+ * that don't make sense in isolation.
+ *
+ * The return value and arguments are like parseDeclarations, with
+ * these additional arguments.
+ *
+ * @param {Function} isCssPropertyKnown
+ * Function to check if the CSS property is known.
+ * @param {Boolean} inComment
+ * If true, assume that this call is parsing some text
+ * which came from a comment in another declaration.
+ * In this case some heuristics are used to avoid parsing
+ * text which isn't obviously a series of declarations.
+ * @param {Boolean} commentOverride
+ * This only makes sense when inComment=true.
+ * When true, assume that the comment was generated by
+ * rewriteDeclarations, and skip the usual name-checking
+ * heuristic.
+ */
+// eslint-disable-next-line complexity
+function parseDeclarationsInternal(
+ isCssPropertyKnown,
+ inputString,
+ parseComments,
+ inComment,
+ commentOverride
+) {
+ if (inputString === null || inputString === undefined) {
+ throw new Error("empty input string");
+ }
+
+ const lexer = getCSSLexer(inputString);
+
+ let declarations = [getEmptyDeclaration()];
+ let lastProp = declarations[0];
+
+ // This tracks the various CSS blocks the current token is in currently.
+ // This is a stack we push to when a block is opened, and we pop from when a block is
+ // closed. Within a block, colons and semicolons don't advance the way they do outside
+ // of blocks.
+ let currentBlocks = [];
+
+ // This tracks the "!important" parsing state. The states are:
+ // 0 - haven't seen anything
+ // 1 - have seen "!", looking for "important" next (possibly after
+ // whitespace).
+ // 2 - have seen "!important"
+ let importantState = 0;
+ // This is true if we saw whitespace or comments between the "!" and
+ // the "important".
+ let importantWS = false;
+
+ // This tracks the nesting parsing state
+ let isInNested = false;
+ let nestingLevel = 0;
+
+ let current = "";
+
+ const resetStateForNextDeclaration = () => {
+ current = "";
+ currentBlocks = [];
+ importantState = 0;
+ importantWS = false;
+ declarations.push(getEmptyDeclaration());
+ lastProp = declarations.at(-1);
+ };
+
+ while (true) {
+ const token = lexer.nextToken();
+ if (!token) {
+ break;
+ }
+
+ // Update the start and end offsets of the declaration, but only
+ // when we see a significant token.
+ if (token.tokenType !== "whitespace" && token.tokenType !== "comment") {
+ if (lastProp.offsets[0] === undefined) {
+ lastProp.offsets[0] = token.startOffset;
+ }
+ lastProp.offsets[1] = token.endOffset;
+ } else if (
+ lastProp.name &&
+ !current &&
+ !importantState &&
+ !lastProp.priority &&
+ lastProp.colonOffsets[1]
+ ) {
+ // Whitespace appearing after the ":" is attributed to it.
+ lastProp.colonOffsets[1] = token.endOffset;
+ } else if (importantState === 1) {
+ importantWS = true;
+ }
+
+ if (
+ // If we're not already in a nested rule
+ !isInNested &&
+ token.tokenType === "symbol" &&
+ // and there's an opening curly bracket
+ token.text == "{" &&
+ // and we're not inside a function or an attribute
+ !currentBlocks.length
+ ) {
+ // Assume we're encountering a nested rule.
+ isInNested = true;
+ nestingLevel = 1;
+
+ continue;
+ } else if (isInNested) {
+ if (token.tokenType === "symbol") {
+ if (token.text == "{") {
+ nestingLevel++;
+ }
+ if (token.text == "}") {
+ nestingLevel--;
+ }
+ }
+
+ // If we were in a nested rule, and we saw the last closing curly bracket,
+ // reset the state to parse possible declarations declared after the nested rule.
+ if (nestingLevel === 0) {
+ isInNested = false;
+ // We need to remove the previous pending declaration and reset the state
+ declarations.pop();
+ resetStateForNextDeclaration();
+ }
+ continue;
+ } else if (
+ token.tokenType === "symbol" &&
+ CSS_BLOCKS[currentBlocks.at(-1)] === token.text
+ ) {
+ // Closing the last block that was opened.
+ currentBlocks.pop();
+ current += token.text;
+ } else if (token.tokenType === "symbol" && CSS_BLOCKS[token.text]) {
+ // Opening a new block.
+ currentBlocks.push(token.text);
+ current += token.text;
+ } else if (token.tokenType === "function") {
+ // Opening a function is like opening a new block, so push one to the stack.
+ currentBlocks.push("(");
+ current += token.text + "(";
+ } else if (token.tokenType === "symbol" && token.text === ":") {
+ // Either way, a "!important" we've seen is no longer valid now.
+ importantState = 0;
+ importantWS = false;
+ if (!lastProp.name) {
+ // Set the current declaration name if there's no name yet
+ lastProp.name = cssTrim(current);
+ lastProp.colonOffsets = [token.startOffset, token.endOffset];
+ current = "";
+ currentBlocks = [];
+
+ // When parsing a comment body, if the left-hand-side is not a
+ // valid property name, then drop it and stop parsing.
+ if (
+ inComment &&
+ !commentOverride &&
+ !isCssPropertyKnown(lastProp.name)
+ ) {
+ lastProp.name = null;
+ break;
+ }
+ } else {
+ // Otherwise, just append ':' to the current value (declaration value
+ // with colons)
+ current += ":";
+ }
+ } else if (
+ token.tokenType === "symbol" &&
+ token.text === ";" &&
+ !currentBlocks.length
+ ) {
+ lastProp.terminator = "";
+ // When parsing a comment, if the name hasn't been set, then we
+ // have probably just seen an ordinary semicolon used in text,
+ // so drop this and stop parsing.
+ if (inComment && !lastProp.name) {
+ current = "";
+ currentBlocks = [];
+ break;
+ }
+ if (importantState === 2) {
+ lastProp.priority = "important";
+ } else if (importantState === 1) {
+ current += "!";
+ if (importantWS) {
+ current += " ";
+ }
+ }
+ lastProp.value = cssTrim(current);
+ resetStateForNextDeclaration();
+ } else if (token.tokenType === "ident") {
+ if (token.text === "important" && importantState === 1) {
+ importantState = 2;
+ } else {
+ if (importantState > 0) {
+ current += "!";
+ if (importantWS) {
+ current += " ";
+ }
+ if (importantState === 2) {
+ current += "important ";
+ }
+ importantState = 0;
+ importantWS = false;
+ }
+ // Re-escape the token to avoid dequoting problems.
+ // See bug 1287620.
+ current += CSS.escape(token.text);
+ }
+ } else if (token.tokenType === "symbol" && token.text === "!") {
+ importantState = 1;
+ } else if (token.tokenType === "whitespace") {
+ if (current !== "") {
+ current = current.trimEnd() + " ";
+ }
+ } else if (token.tokenType === "comment") {
+ if (parseComments && !lastProp.name && !lastProp.value) {
+ const commentText = inputString.substring(
+ token.startOffset + 2,
+ token.endOffset - 2
+ );
+ const newDecls = parseCommentDeclarations(
+ isCssPropertyKnown,
+ commentText,
+ token.startOffset,
+ token.endOffset
+ );
+
+ // Insert the new declarations just before the final element.
+ const lastDecl = declarations.pop();
+ declarations = [...declarations, ...newDecls, lastDecl];
+ } else {
+ current = current.trimEnd() + " ";
+ }
+ } else {
+ if (importantState > 0) {
+ current += "!";
+ if (importantWS) {
+ current += " ";
+ }
+ if (importantState === 2) {
+ current += "important ";
+ }
+ importantState = 0;
+ importantWS = false;
+ }
+ current += inputString.substring(token.startOffset, token.endOffset);
+ }
+ }
+
+ // Handle whatever trailing properties or values might still be there
+ if (current) {
+ // If nested rule doesn't have closing bracket
+ if (isInNested && nestingLevel > 0) {
+ // We need to remove the previous (nested) pending declaration
+ declarations.pop();
+ } else if (!lastProp.name) {
+ // Ignore this case in comments.
+ if (!inComment) {
+ // Trailing property found, e.g. p1:v1;p2:v2;p3
+ lastProp.name = cssTrim(current);
+ }
+ } else {
+ // Trailing value found, i.e. value without an ending ;
+ if (importantState === 2) {
+ lastProp.priority = "important";
+ } else if (importantState === 1) {
+ current += "!";
+ }
+ lastProp.value = cssTrim(current);
+ const terminator = lexer.performEOFFixup("", true);
+ lastProp.terminator = terminator + ";";
+ // If the input was unterminated, attribute the remainder to
+ // this property. This avoids some bad behavior when rewriting
+ // an unterminated comment.
+ if (terminator) {
+ lastProp.offsets[1] = inputString.length;
+ }
+ }
+ }
+
+ // Remove declarations that have neither a name nor a value
+ declarations = declarations.filter(prop => prop.name || prop.value);
+
+ return declarations;
+}
+
+/**
+ * Returns an array of CSS declarations given a string.
+ * For example, parseDeclarations(isCssPropertyKnown, "width: 1px; height: 1px")
+ * would return:
+ * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}]
+ *
+ * The input string is assumed to only contain declarations so { and }
+ * characters will be treated as part of either the property or value,
+ * depending where it's found.
+ *
+ * @param {Function} isCssPropertyKnown
+ * A function to check if the CSS property is known. This is either an
+ * internal server function or from the CssPropertiesFront.
+ * that are supported by the server.
+ * @param {String} inputString
+ * An input string of CSS
+ * @param {Boolean} parseComments
+ * If true, try to parse the contents of comments as well.
+ * A comment will only be parsed if it occurs outside of
+ * the body of some other declaration.
+ * @return {Array} an array of objects with the following signature:
+ * [{"name": string, "value": string, "priority": string,
+ * "terminator": string,
+ * "offsets": [start, end], "colonOffsets": [start, end]},
+ * ...]
+ * Here, "offsets" holds the offsets of the start and end
+ * of the declaration text, in a form suitable for use with
+ * String.substring.
+ * "terminator" is a string to use to terminate the declaration,
+ * usually "" to mean no additional termination is needed.
+ * "colonOffsets" holds the start and end locations of the
+ * ":" that separates the property name from the value.
+ * If the declaration appears in a comment, then there will
+ * be an additional {"commentOffsets": [start, end] property
+ * on the object, which will hold the offsets of the start
+ * and end of the enclosing comment.
+ */
+function parseDeclarations(
+ isCssPropertyKnown,
+ inputString,
+ parseComments = false
+) {
+ return parseDeclarationsInternal(
+ isCssPropertyKnown,
+ inputString,
+ parseComments,
+ false,
+ false
+ );
+}
+
+/**
+ * Like @see parseDeclarations, but removes properties that do not
+ * have a name.
+ */
+function parseNamedDeclarations(
+ isCssPropertyKnown,
+ inputString,
+ parseComments = false
+) {
+ return parseDeclarations(
+ isCssPropertyKnown,
+ inputString,
+ parseComments
+ ).filter(item => !!item.name);
+}
+
+/**
+ * Returns an array of the parsed CSS selector value and type given a string.
+ *
+ * The components making up the CSS selector can be extracted into 3 different
+ * types: element, attribute and pseudoclass. The object that is appended to
+ * the returned array contains the value related to one of the 3 types described
+ * along with the actual type.
+ *
+ * The following are the 3 types that can be returned in the object signature:
+ * (1) SELECTOR_ATTRIBUTE
+ * (2) SELECTOR_ELEMENT
+ * (3) SELECTOR_PSEUDO_CLASS
+ *
+ * @param {String} value
+ * The CSS selector text.
+ * @return {Array} an array of objects with the following signature:
+ * [{ "value": string, "type": integer }, ...]
+ */
+// eslint-disable-next-line complexity
+function parsePseudoClassesAndAttributes(value) {
+ if (!value) {
+ throw new Error("empty input string");
+ }
+
+ const tokens = cssTokenizer(value);
+ const result = [];
+ let current = "";
+ let functionCount = 0;
+ let hasAttribute = false;
+ let hasColon = false;
+
+ for (const token of tokens) {
+ if (token.tokenType === "ident") {
+ current += value.substring(token.startOffset, token.endOffset);
+
+ if (hasColon && !functionCount) {
+ if (current) {
+ result.push({ value: current, type: SELECTOR_PSEUDO_CLASS });
+ }
+
+ current = "";
+ hasColon = false;
+ }
+ } else if (token.tokenType === "symbol" && token.text === ":") {
+ if (!hasColon) {
+ if (current) {
+ result.push({ value: current, type: SELECTOR_ELEMENT });
+ }
+
+ current = "";
+ hasColon = true;
+ }
+
+ current += token.text;
+ } else if (token.tokenType === "function") {
+ current += value.substring(token.startOffset, token.endOffset);
+ functionCount++;
+ } else if (token.tokenType === "symbol" && token.text === ")") {
+ current += token.text;
+
+ if (hasColon && functionCount == 1) {
+ if (current) {
+ result.push({ value: current, type: SELECTOR_PSEUDO_CLASS });
+ }
+
+ current = "";
+ functionCount--;
+ hasColon = false;
+ } else {
+ functionCount--;
+ }
+ } else if (token.tokenType === "symbol" && token.text === "[") {
+ if (!hasAttribute && !functionCount) {
+ if (current) {
+ result.push({ value: current, type: SELECTOR_ELEMENT });
+ }
+
+ current = "";
+ hasAttribute = true;
+ }
+
+ current += token.text;
+ } else if (token.tokenType === "symbol" && token.text === "]") {
+ current += token.text;
+
+ if (hasAttribute && !functionCount) {
+ if (current) {
+ result.push({ value: current, type: SELECTOR_ATTRIBUTE });
+ }
+
+ current = "";
+ hasAttribute = false;
+ }
+ } else {
+ current += value.substring(token.startOffset, token.endOffset);
+ }
+ }
+
+ if (current) {
+ result.push({ value: current, type: SELECTOR_ELEMENT });
+ }
+
+ return result;
+}
+
+/**
+ * Expects a single CSS value to be passed as the input and parses the value
+ * and priority.
+ *
+ * @param {Function} isCssPropertyKnown
+ * A function to check if the CSS property is known. This is either an
+ * internal server function or from the CssPropertiesFront.
+ * that are supported by the server.
+ * @param {String} value
+ * The value from the text editor.
+ * @return {Object} an object with 'value' and 'priority' properties.
+ */
+function parseSingleValue(isCssPropertyKnown, value) {
+ const declaration = parseDeclarations(
+ isCssPropertyKnown,
+ "a: " + value + ";"
+ )[0];
+ return {
+ value: declaration ? declaration.value : "",
+ priority: declaration ? declaration.priority : "",
+ };
+}
+
+/**
+ * Convert an angle value to degree.
+ *
+ * @param {Number} angleValue The angle value.
+ * @param {CSS_ANGLEUNIT} angleUnit The angleValue's angle unit.
+ * @return {Number} An angle value in degree.
+ */
+function getAngleValueInDegrees(angleValue, angleUnit) {
+ switch (angleUnit) {
+ case CSS_ANGLEUNIT.deg:
+ return angleValue;
+ case CSS_ANGLEUNIT.grad:
+ return angleValue * 0.9;
+ case CSS_ANGLEUNIT.rad:
+ return (angleValue * 180) / Math.PI;
+ case CSS_ANGLEUNIT.turn:
+ return angleValue * 360;
+ default:
+ throw new Error("No matched angle unit.");
+ }
+}
+
+exports.cssTokenizer = cssTokenizer;
+exports.cssTokenizerWithLineColumn = cssTokenizerWithLineColumn;
+exports.escapeCSSComment = escapeCSSComment;
+exports.unescapeCSSComment = unescapeCSSComment;
+exports.parseDeclarations = parseDeclarations;
+exports.parseNamedDeclarations = parseNamedDeclarations;
+// parseCommentDeclarations is exported for testing.
+exports._parseCommentDeclarations = parseCommentDeclarations;
+exports.parsePseudoClassesAndAttributes = parsePseudoClassesAndAttributes;
+exports.parseSingleValue = parseSingleValue;
+exports.getAngleValueInDegrees = getAngleValueInDegrees;
diff --git a/devtools/shared/debounce.js b/devtools/shared/debounce.js
new file mode 100644
index 0000000000..d43dea48d5
--- /dev/null
+++ b/devtools/shared/debounce.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";
+
+/**
+ * Create a debouncing function wrapper to only call the target function after a certain
+ * amount of time has passed without it being called.
+ *
+ * @param {Function} func
+ * The function to debounce
+ * @param {number} wait
+ * The wait period
+ * @param {Object} scope
+ * The scope to use for func
+ * @return {Function} The debounced function, which has a `cancel` method that the
+ * consumer can call to cancel any pending setTimeout callback.
+ */
+exports.debounce = function (func, wait, scope) {
+ let timer = null;
+
+ function clearTimer(resetTimer = false) {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ if (resetTimer) {
+ timer = null;
+ }
+ }
+
+ const debouncedFunction = function () {
+ clearTimer();
+
+ const args = arguments;
+ timer = setTimeout(function () {
+ timer = null;
+ func.apply(scope, args);
+ }, wait);
+ };
+
+ debouncedFunction.cancel = clearTimer.bind(null, true);
+
+ return debouncedFunction;
+};
diff --git a/devtools/shared/discovery/discovery.js b/devtools/shared/discovery/discovery.js
new file mode 100644
index 0000000000..f7ec5a6860
--- /dev/null
+++ b/devtools/shared/discovery/discovery.js
@@ -0,0 +1,427 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 implements a UDP mulitcast device discovery protocol that:
+ * * Is optimized for mobile devices
+ * * Doesn't require any special schema for service info
+ *
+ * To ensure it works well on mobile devices, there is no heartbeat or other
+ * recurring transmission.
+ *
+ * Devices are typically in one of two groups: scanning for services or
+ * providing services (though they may be in both groups as well).
+ *
+ * Scanning devices listen on UPDATE_PORT for UDP multicast traffic. When the
+ * scanning device wants to force an update of the services available, it sends
+ * a status packet to SCAN_PORT.
+ *
+ * Service provider devices listen on SCAN_PORT for any packets from scanning
+ * devices. If one is recevied, the provider device sends a status packet
+ * (listing the services it offers) to UPDATE_PORT.
+ *
+ * Scanning devices purge any previously known devices after REPLY_TIMEOUT ms
+ * from that start of a scan if no reply is received during the most recent
+ * scan.
+ *
+ * When a service is registered, is supplies a regular object with any details
+ * about itself (a port number, for example) in a service-defined format, which
+ * is then available to scanning devices.
+ */
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const UDPSocket = Components.Constructor(
+ "@mozilla.org/network/udp-socket;1",
+ "nsIUDPSocket",
+ "init"
+);
+
+const SCAN_PORT = 50624;
+const UPDATE_PORT = 50625;
+const ADDRESS = "224.0.0.115";
+const REPLY_TIMEOUT = 5000;
+
+var logging = Services.prefs.getBoolPref("devtools.discovery.log");
+function log(msg) {
+ if (logging) {
+ console.log("DISCOVERY: " + msg);
+ }
+}
+
+/**
+ * Each Transport instance owns a single UDPSocket.
+ * @param port integer
+ * The port to listen on for incoming UDP multicast packets.
+ */
+function Transport(port) {
+ EventEmitter.decorate(this);
+ try {
+ this.socket = new UDPSocket(
+ port,
+ false,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ this.socket.joinMulticast(ADDRESS);
+ this.socket.asyncListen(this);
+ } catch (e) {
+ log("Failed to start new socket: " + e);
+ }
+}
+
+Transport.prototype = {
+ /**
+ * Send a object to some UDP port.
+ * @param object object
+ * Object which is the message to send
+ * @param port integer
+ * UDP port to send the message to
+ */
+ send(object, port) {
+ if (logging) {
+ log("Send to " + port + ":\n" + JSON.stringify(object, null, 2));
+ }
+ const message = JSON.stringify(object);
+ const rawMessage = Uint8Array.from(message, x => x.charCodeAt(0));
+ try {
+ this.socket.send(ADDRESS, port, rawMessage, rawMessage.length);
+ } catch (e) {
+ log("Failed to send message: " + e);
+ }
+ },
+
+ destroy() {
+ this.socket.close();
+ },
+
+ // nsIUDPSocketListener
+
+ onPacketReceived(socket, message) {
+ const messageData = message.data;
+ const object = JSON.parse(messageData);
+ object.from = message.fromAddr.address;
+ const port = message.fromAddr.port;
+ if (port == this.socket.port) {
+ log("Ignoring looped message");
+ return;
+ }
+ if (logging) {
+ log(
+ "Recv on " + this.socket.port + ":\n" + JSON.stringify(object, null, 2)
+ );
+ }
+ this.emit("message", object);
+ },
+
+ onStopListening() {},
+};
+
+/**
+ * Manages the local device's name. The name can be generated in serveral
+ * platform-specific ways (see |_generate|). The aim is for each device on the
+ * same local network to have a unique name.
+ */
+function LocalDevice() {
+ this._name = LocalDevice.UNKNOWN;
+ // Trigger |_get| to load name eagerly
+ this._get();
+}
+
+LocalDevice.UNKNOWN = "unknown";
+
+LocalDevice.prototype = {
+ _get() {
+ // Without Settings API, just generate a name and stop, since the value
+ // can't be persisted.
+ this._generate();
+ },
+
+ /**
+ * Generate a new device name from various platform-specific properties.
+ * Triggers the |name| setter to persist if needed.
+ */
+ _generate() {
+ if (Services.appinfo.widgetToolkit == "android") {
+ // For Firefox for Android, use the device's model name.
+ // TODO: Bug 1180997: Find the right way to expose an editable name
+ this.name = Services.sysinfo.get("device");
+ } else {
+ this.name = Services.dns.myHostName;
+ }
+ },
+
+ get name() {
+ return this._name;
+ },
+
+ set name(name) {
+ this._name = name;
+ log("Device: " + this._name);
+ },
+};
+
+function Discovery() {
+ EventEmitter.decorate(this);
+
+ this.localServices = {};
+ this.remoteServices = {};
+ this.device = new LocalDevice();
+ this.replyTimeout = REPLY_TIMEOUT;
+
+ // Defaulted to Transport, but can be altered by tests
+ this._factories = { Transport };
+
+ this._transports = {
+ scan: null,
+ update: null,
+ };
+ this._expectingReplies = {
+ from: new Set(),
+ };
+
+ this._onRemoteScan = this._onRemoteScan.bind(this);
+ this._onRemoteUpdate = this._onRemoteUpdate.bind(this);
+ this._purgeMissingDevices = this._purgeMissingDevices.bind(this);
+}
+
+Discovery.prototype = {
+ /**
+ * Add a new service offered by this device.
+ * @param service string
+ * Name of the service
+ * @param info object
+ * Arbitrary data about the service to announce to scanning devices
+ */
+ addService(service, info) {
+ log("ADDING LOCAL SERVICE");
+ if (Object.keys(this.localServices).length === 0) {
+ this._startListeningForScan();
+ }
+ this.localServices[service] = info;
+ },
+
+ /**
+ * Remove a service offered by this device.
+ * @param service string
+ * Name of the service
+ */
+ removeService(service) {
+ delete this.localServices[service];
+ if (Object.keys(this.localServices).length === 0) {
+ this._stopListeningForScan();
+ }
+ },
+
+ /**
+ * Scan for service updates from other devices.
+ */
+ scan() {
+ this._startListeningForUpdate();
+ this._waitForReplies();
+ // TODO Bug 1027457: Use timer to debounce
+ this._sendStatusTo(SCAN_PORT);
+ },
+
+ /**
+ * Get a list of all remote devices currently offering some service.:w
+ */
+ getRemoteDevices() {
+ const devices = new Set();
+ for (const service in this.remoteServices) {
+ for (const device in this.remoteServices[service]) {
+ devices.add(device);
+ }
+ }
+ return [...devices];
+ },
+
+ /**
+ * Get a list of all remote devices currently offering a particular service.
+ */
+ getRemoteDevicesWithService(service) {
+ const devicesWithService = this.remoteServices[service] || {};
+ return Object.keys(devicesWithService);
+ },
+
+ /**
+ * Get service info (any details registered by the remote device) for a given
+ * service on a device.
+ */
+ getRemoteService(service, device) {
+ const devicesWithService = this.remoteServices[service] || {};
+ return devicesWithService[device];
+ },
+
+ _waitForReplies() {
+ clearTimeout(this._expectingReplies.timer);
+ this._expectingReplies.from = new Set(this.getRemoteDevices());
+ this._expectingReplies.timer = setTimeout(
+ this._purgeMissingDevices,
+ this.replyTimeout
+ );
+ },
+
+ get Transport() {
+ return this._factories.Transport;
+ },
+
+ _startListeningForScan() {
+ if (this._transports.scan) {
+ // Already listening
+ return;
+ }
+ log("LISTEN FOR SCAN");
+ this._transports.scan = new this.Transport(SCAN_PORT);
+ this._transports.scan.on("message", this._onRemoteScan);
+ },
+
+ _stopListeningForScan() {
+ if (!this._transports.scan) {
+ // Not listening
+ return;
+ }
+ this._transports.scan.off("message", this._onRemoteScan);
+ this._transports.scan.destroy();
+ this._transports.scan = null;
+ },
+
+ _startListeningForUpdate() {
+ if (this._transports.update) {
+ // Already listening
+ return;
+ }
+ log("LISTEN FOR UPDATE");
+ this._transports.update = new this.Transport(UPDATE_PORT);
+ this._transports.update.on("message", this._onRemoteUpdate);
+ },
+
+ _stopListeningForUpdate() {
+ if (!this._transports.update) {
+ // Not listening
+ return;
+ }
+ this._transports.update.off("message", this._onRemoteUpdate);
+ this._transports.update.destroy();
+ this._transports.update = null;
+ },
+
+ _restartListening() {
+ if (this._transports.scan) {
+ this._stopListeningForScan();
+ this._startListeningForScan();
+ }
+ if (this._transports.update) {
+ this._stopListeningForUpdate();
+ this._startListeningForUpdate();
+ }
+ },
+
+ /**
+ * When sending message, we can use either transport, so just pick the first
+ * one currently alive.
+ */
+ get _outgoingTransport() {
+ if (this._transports.scan) {
+ return this._transports.scan;
+ }
+ if (this._transports.update) {
+ return this._transports.update;
+ }
+ return null;
+ },
+
+ _sendStatusTo(port) {
+ const status = {
+ device: this.device.name,
+ services: this.localServices,
+ };
+ this._outgoingTransport.send(status, port);
+ },
+
+ _onRemoteScan() {
+ // Send my own status in response
+ log("GOT SCAN REQUEST");
+ this._sendStatusTo(UPDATE_PORT);
+ },
+
+ _onRemoteUpdate(update) {
+ log("GOT REMOTE UPDATE");
+
+ const remoteDevice = update.device;
+ const remoteHost = update.from;
+
+ // Record the reply as received so it won't be purged as missing
+ this._expectingReplies.from.delete(remoteDevice);
+
+ // First, loop over the known services
+ for (const service in this.remoteServices) {
+ const devicesWithService = this.remoteServices[service];
+ const hadServiceForDevice = !!devicesWithService[remoteDevice];
+ const haveServiceForDevice = service in update.services;
+ // If the remote device used to have service, but doesn't any longer, then
+ // it was deleted, so we remove it here.
+ if (hadServiceForDevice && !haveServiceForDevice) {
+ delete devicesWithService[remoteDevice];
+ log("REMOVED " + service + ", DEVICE " + remoteDevice);
+ this.emit(service + "-device-removed", remoteDevice);
+ }
+ }
+
+ // Second, loop over the services in the received update
+ for (const service in update.services) {
+ // Detect if this is a new device for this service
+ const newDevice =
+ !this.remoteServices[service] ||
+ !this.remoteServices[service][remoteDevice];
+
+ // Look up the service info we may have received previously from the same
+ // remote device
+ const devicesWithService = this.remoteServices[service] || {};
+ const oldDeviceInfo = devicesWithService[remoteDevice];
+
+ // Store the service info from the remote device
+ const newDeviceInfo = Cu.cloneInto(update.services[service], {});
+ newDeviceInfo.host = remoteHost;
+ devicesWithService[remoteDevice] = newDeviceInfo;
+ this.remoteServices[service] = devicesWithService;
+
+ // If this is a new service for the remote device, announce the addition
+ if (newDevice) {
+ log("ADDED " + service + ", DEVICE " + remoteDevice);
+ this.emit(service + "-device-added", remoteDevice, newDeviceInfo);
+ }
+
+ // If we've seen this service from the remote device, but the details have
+ // changed, announce the update
+ if (
+ !newDevice &&
+ JSON.stringify(oldDeviceInfo) != JSON.stringify(newDeviceInfo)
+ ) {
+ log("UPDATED " + service + ", DEVICE " + remoteDevice);
+ this.emit(service + "-device-updated", remoteDevice, newDeviceInfo);
+ }
+ }
+ },
+
+ _purgeMissingDevices() {
+ log("PURGING MISSING DEVICES");
+ for (const service in this.remoteServices) {
+ const devicesWithService = this.remoteServices[service];
+ for (const remoteDevice in devicesWithService) {
+ // If we're still expecting a reply from a remote device when it's time
+ // to purge, then the service is removed.
+ if (this._expectingReplies.from.has(remoteDevice)) {
+ delete devicesWithService[remoteDevice];
+ log("REMOVED " + service + ", DEVICE " + remoteDevice);
+ this.emit(service + "-device-removed", remoteDevice);
+ }
+ }
+ }
+ },
+};
+
+var discovery = new Discovery();
+
+module.exports = discovery;
diff --git a/devtools/shared/discovery/moz.build b/devtools/shared/discovery/moz.build
new file mode 100644
index 0000000000..6e415ff558
--- /dev/null
+++ b/devtools/shared/discovery/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+DevToolsModules(
+ "discovery.js",
+)
diff --git a/devtools/shared/discovery/tests/xpcshell/.eslintrc.js b/devtools/shared/discovery/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..7f6b62a9e5
--- /dev/null
+++ b/devtools/shared/discovery/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/discovery/tests/xpcshell/test_discovery.js b/devtools/shared/discovery/tests/xpcshell/test_discovery.js
new file mode 100644
index 0000000000..cf14d7416c
--- /dev/null
+++ b/devtools/shared/discovery/tests/xpcshell/test_discovery.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const discovery = require("resource://devtools/shared/discovery/discovery.js");
+const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+Services.prefs.setBoolPref("devtools.discovery.log", true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.discovery.log");
+});
+
+function log(msg) {
+ info("DISCOVERY: " + msg);
+}
+
+// Global map of actively listening ports to TestTransport instances
+var gTestTransports = {};
+
+/**
+ * Implements the same API as Transport in discovery.js. Here, no UDP sockets
+ * are used. Instead, messages are delivered immediately.
+ */
+function TestTransport(port) {
+ EventEmitter.decorate(this);
+ this.port = port;
+ gTestTransports[this.port] = this;
+}
+
+TestTransport.prototype = {
+ send(object, port) {
+ log("Send to " + port + ":\n" + JSON.stringify(object, null, 2));
+ if (!gTestTransports[port]) {
+ log("No listener on port " + port);
+ return;
+ }
+ const message = JSON.stringify(object);
+ gTestTransports[port].onPacketReceived(null, message);
+ },
+
+ destroy() {
+ delete gTestTransports[this.port];
+ },
+
+ // nsIUDPSocketListener
+
+ onPacketReceived(socket, message) {
+ const object = JSON.parse(message);
+ object.from = "localhost";
+ log("Recv on " + this.port + ":\n" + JSON.stringify(object, null, 2));
+ this.emit("message", object);
+ },
+
+ onStopListening(socket, status) {},
+};
+
+// Use TestTransport instead of the usual Transport
+discovery._factories.Transport = TestTransport;
+
+// Ignore name generation on b2g and force a fixed value
+Object.defineProperty(discovery.device, "name", {
+ get() {
+ return "test-device";
+ },
+});
+
+add_task(async function () {
+ // At startup, no remote devices are known
+ deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
+ deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
+
+ discovery.scan();
+
+ // No services added yet, still empty
+ deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
+ deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
+
+ discovery.addService("devtools", { port: 1234 });
+
+ // Changes not visible until next scan
+ deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
+ deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
+
+ await scanForChange("devtools", "added");
+
+ // Now we see the new service
+ deepEqual(discovery.getRemoteDevicesWithService("devtools"), ["test-device"]);
+ deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
+
+ discovery.addService("penguins", { tux: true });
+ await scanForChange("penguins", "added");
+
+ deepEqual(discovery.getRemoteDevicesWithService("devtools"), ["test-device"]);
+ deepEqual(discovery.getRemoteDevicesWithService("penguins"), ["test-device"]);
+ deepEqual(discovery.getRemoteDevices(), ["test-device"]);
+
+ deepEqual(discovery.getRemoteService("devtools", "test-device"), {
+ port: 1234,
+ host: "localhost",
+ });
+ deepEqual(discovery.getRemoteService("penguins", "test-device"), {
+ tux: true,
+ host: "localhost",
+ });
+
+ discovery.removeService("devtools");
+ await scanForChange("devtools", "removed");
+
+ discovery.addService("penguins", { tux: false });
+ await scanForChange("penguins", "updated");
+
+ // Scan again, but nothing should be removed
+ await scanForNoChange("penguins", "removed");
+
+ // Split the scanning side from the service side to simulate the machine with
+ // the service becoming unreachable
+ gTestTransports = {};
+
+ discovery.removeService("penguins");
+ await scanForChange("penguins", "removed");
+});
+
+function scanForChange(service, changeType) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ reject(new Error("Reply never arrived"));
+ }, discovery.replyTimeout + 500);
+ discovery.on(service + "-device-" + changeType, function onChange() {
+ discovery.off(service + "-device-" + changeType, onChange);
+ clearTimeout(timer);
+ resolve();
+ });
+ discovery.scan();
+ });
+}
+
+function scanForNoChange(service, changeType) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ resolve();
+ }, discovery.replyTimeout + 500);
+ discovery.on(service + "-device-" + changeType, function onChange() {
+ discovery.off(service + "-device-" + changeType, onChange);
+ clearTimeout(timer);
+ reject(new Error("Unexpected change occurred"));
+ });
+ discovery.scan();
+ });
+}
diff --git a/devtools/shared/discovery/tests/xpcshell/xpcshell.toml b/devtools/shared/discovery/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..e259b7704c
--- /dev/null
+++ b/devtools/shared/discovery/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = "devtools"
+head = ""
+firefox-appdir = "browser"
+
+["test_discovery.js"]
diff --git a/devtools/shared/dom-helpers.js b/devtools/shared/dom-helpers.js
new file mode 100644
index 0000000000..3646855560
--- /dev/null
+++ b/devtools/shared/dom-helpers.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";
+
+exports.DOMHelpers = {
+ /**
+ * A simple way to be notified (once) when a window becomes
+ * interactive (DOMContentLoaded).
+ *
+ * It is based on the chromeEventHandler. This is useful when
+ * chrome iframes are loaded in content docshells (in Firefox
+ * tabs for example).
+ *
+ * @param nsIDOMWindow win
+ * The content window, owning the document to traverse.
+ * @param Function callback
+ * The method to call when the frame is loaded.
+ * @param String targetURL
+ * (optional) Check that the frame URL corresponds to the provided URL
+ * before calling the callback.
+ */
+ onceDOMReady(win, callback, targetURL) {
+ if (!win) {
+ throw new Error("window can't be null or undefined");
+ }
+ const docShell = win.docShell;
+ const onReady = function (event) {
+ if (event.target == win.document) {
+ docShell.chromeEventHandler.removeEventListener(
+ "DOMContentLoaded",
+ onReady
+ );
+ // If in `callback` the URL of the window is changed and a listener to DOMContentLoaded
+ // is attached, the event we just received will be also be caught by the new listener.
+ // We want to avoid that so we execute the callback in the next queue.
+ Services.tm.dispatchToMainThread(callback);
+ }
+ };
+ if (
+ (win.document.readyState == "complete" ||
+ win.document.readyState == "interactive") &&
+ win.location.href == targetURL
+ ) {
+ Services.tm.dispatchToMainThread(callback);
+ } else {
+ docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady);
+ }
+ },
+};
diff --git a/devtools/shared/dom-node-constants.js b/devtools/shared/dom-node-constants.js
new file mode 100644
index 0000000000..66276294a6
--- /dev/null
+++ b/devtools/shared/dom-node-constants.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = {
+ 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/shared/dom-node-filter-constants.js b/devtools/shared/dom-node-filter-constants.js
new file mode 100644
index 0000000000..840f8686c4
--- /dev/null
+++ b/devtools/shared/dom-node-filter-constants.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = {
+ FILTER_ACCEPT: 1,
+ FILTER_REJECT: 2,
+ FILTER_SKIP: 3,
+
+ SHOW_ALL: 0xffffffff,
+ SHOW_ELEMENT: 0x00000001,
+ SHOW_ATTRIBUTE: 0x00000002,
+ SHOW_TEXT: 0x00000004,
+ SHOW_CDATA_SECTION: 0x00000008,
+ SHOW_ENTITY_REFERENCE: 0x00000010,
+ SHOW_ENTITY: 0x00000020,
+ SHOW_PROCESSING_INSTRUCTION: 0x00000040,
+ SHOW_COMMENT: 0x00000080,
+ SHOW_DOCUMENT: 0x00000100,
+ SHOW_DOCUMENT_TYPE: 0x00000200,
+ SHOW_DOCUMENT_FRAGMENT: 0x00000400,
+ SHOW_NOTATION: 0x00000800,
+};
diff --git a/devtools/shared/event-emitter.js b/devtools/shared/event-emitter.js
new file mode 100644
index 0000000000..2ba4ae927b
--- /dev/null
+++ b/devtools/shared/event-emitter.js
@@ -0,0 +1,470 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 BAD_LISTENER =
+ "The event listener must be a function, or an object that has " +
+ "`EventEmitter.handler` Symbol.";
+
+const eventListeners = Symbol("EventEmitter/listeners");
+const onceOriginalListener = Symbol("EventEmitter/once-original-listener");
+const handler = Symbol("EventEmitter/event-handler");
+loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
+
+class EventEmitter {
+ /**
+ * Registers an event `listener` that is called every time events of
+ * specified `type` is emitted on the given event `target`.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of event.
+ * @param {Function|Object} listener
+ * The listener that processes the event.
+ * @param {Object} options
+ * @param {AbortSignal} options.signal
+ * The listener will be removed when linked AbortController’s abort() method is called
+ * @returns {Function}
+ * A function that removes the listener when called.
+ */
+ static on(target, type, listener, { signal } = {}) {
+ if (typeof listener !== "function" && !isEventHandler(listener)) {
+ throw new Error(BAD_LISTENER);
+ }
+
+ if (signal?.aborted === true) {
+ // The signal is already aborted so don't setup the listener.
+ // We return an empty function as it's the expected returned value.
+ return () => {};
+ }
+
+ if (!(eventListeners in target)) {
+ target[eventListeners] = new Map();
+ }
+
+ const events = target[eventListeners];
+
+ if (events.has(type)) {
+ events.get(type).add(listener);
+ } else {
+ events.set(type, new Set([listener]));
+ }
+
+ const offFn = () => EventEmitter.off(target, type, listener);
+
+ if (signal) {
+ signal.addEventListener("abort", offFn, { once: true });
+ }
+
+ return offFn;
+ }
+
+ /**
+ * Removes an event `listener` for the given event `type` on the given event
+ * `target`. If no `listener` is passed removes all listeners of the given
+ * `type`. If `type` is not passed removes all the listeners of the given
+ * event `target`.
+ * @param {Object} target
+ * The event target object.
+ * @param {String} [type]
+ * The type of event.
+ * @param {Function|Object} [listener]
+ * The listener that processes the event.
+ */
+ static off(target, type, listener) {
+ const length = arguments.length;
+ const events = target[eventListeners];
+
+ if (!events) {
+ return;
+ }
+
+ if (length >= 3) {
+ // Trying to remove from the `target` the `listener` specified for the
+ // event's `type` given.
+ const listenersForType = events.get(type);
+
+ // If we don't have listeners for the event's type, we bail out.
+ if (!listenersForType) {
+ return;
+ }
+
+ // If the listeners list contains the listener given, we just remove it.
+ if (listenersForType.has(listener)) {
+ listenersForType.delete(listener);
+ } else {
+ // If it's not present, there is still the possibility that the listener
+ // have been added using `once`, since the method wraps the original listener
+ // in another function.
+ // So we iterate all the listeners to check if any of them is a wrapper to
+ // the `listener` given.
+ for (const value of listenersForType.values()) {
+ if (
+ onceOriginalListener in value &&
+ value[onceOriginalListener] === listener
+ ) {
+ listenersForType.delete(value);
+ break;
+ }
+ }
+ }
+ } else if (length === 2) {
+ // No listener was given, it means we're removing all the listeners from
+ // the given event's `type`.
+ if (events.has(type)) {
+ events.delete(type);
+ }
+ } else if (length === 1) {
+ // With only the `target` given, we're removing all the listeners from the object.
+ events.clear();
+ }
+ }
+
+ static clearEvents(target) {
+ const events = target[eventListeners];
+ if (!events) {
+ return;
+ }
+ events.clear();
+ }
+
+ /**
+ * Registers an event `listener` that is called only the next time an event
+ * of the specified `type` is emitted on the given event `target`.
+ * It returns a Promise resolved once the specified event `type` is emitted.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of the event.
+ * @param {Function|Object} [listener]
+ * The listener that processes the event.
+ * @param {Object} options
+ * @param {AbortSignal} options.signal
+ * The listener will be removed when linked AbortController’s abort() method is called
+ * @return {Promise}
+ * The promise resolved once the event `type` is emitted.
+ */
+ static once(target, type, listener, options) {
+ return new Promise(resolve => {
+ // This is the actual listener that will be added to the target's listener, it wraps
+ // the call to the original `listener` given.
+ const newListener = (first, ...rest) => {
+ // To prevent side effects we're removing the listener upfront.
+ EventEmitter.off(target, type, newListener);
+
+ let rv;
+ if (listener) {
+ if (isEventHandler(listener)) {
+ // if the `listener` given is actually an object that handles the events
+ // using `EventEmitter.handler`, we want to call that function, passing also
+ // the event's type as first argument, and the `listener` (the object) as
+ // contextual object.
+ rv = listener[handler](type, first, ...rest);
+ } else {
+ // Otherwise we'll just call it
+ rv = listener.call(target, first, ...rest);
+ }
+ }
+
+ // We resolve the promise once the listener is called.
+ resolve(first);
+
+ // Listeners may return a promise, so pass it along
+ return rv;
+ };
+
+ newListener[onceOriginalListener] = listener;
+ EventEmitter.on(target, type, newListener, options);
+ });
+ }
+
+ static emit(target, type, ...rest) {
+ EventEmitter._emit(target, type, false, rest);
+ }
+
+ static emitAsync(target, type, ...rest) {
+ return EventEmitter._emit(target, type, true, rest);
+ }
+
+ /**
+ * Emit an event of a given `type` on a given `target` object.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of the event.
+ * @param {Boolean} async
+ * If true, this function will wait for each listener completion.
+ * Each listener has to return a promise, which will be awaited for.
+ * @param {Array} args
+ * The arguments to pass to each listener function.
+ * @return {Promise|undefined}
+ * If `async` argument is true, returns the promise resolved once all listeners have resolved.
+ * Otherwise, this function returns undefined;
+ */
+ static _emit(target, type, async, args) {
+ if (loggingEnabled) {
+ logEvent(type, args);
+ }
+
+ const targetEventListeners = target[eventListeners];
+ if (!targetEventListeners) {
+ return undefined;
+ }
+
+ const listeners = targetEventListeners.get(type);
+ if (!listeners?.size) {
+ return undefined;
+ }
+
+ const promises = async ? [] : null;
+
+ // Creating a temporary Set with the original listeners, to avoiding side effects
+ // in emit.
+ for (const listener of new Set(listeners)) {
+ // If the object was destroyed during event emission, stop emitting.
+ if (!(eventListeners in target)) {
+ break;
+ }
+
+ // If listeners were removed during emission, make sure the
+ // event handler we're going to fire wasn't removed.
+ if (listeners && listeners.has(listener)) {
+ try {
+ let promise;
+ if (isEventHandler(listener)) {
+ promise = listener[handler](type, ...args);
+ } else {
+ promise = listener.apply(target, args);
+ }
+ if (async) {
+ // Assert the name instead of `constructor != Promise` in order
+ // to avoid cross compartment issues where Promise can be multiple.
+ if (!promise || promise.constructor.name != "Promise") {
+ console.warn(
+ `Listener for event '${type}' did not return a promise.`
+ );
+ } else {
+ promises.push(promise);
+ }
+ }
+ } catch (ex) {
+ // Prevent a bad listener from interfering with the others.
+ console.error(ex);
+ const msg = ex + ": " + ex.stack;
+ dump(msg + "\n");
+ }
+ }
+ }
+
+ if (async) {
+ return Promise.all(promises);
+ }
+
+ return undefined;
+ }
+
+ /**
+ * Returns a number of event listeners registered for the given event `type`
+ * on the given event `target`.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of event.
+ * @return {Number}
+ * The number of event listeners.
+ */
+ static count(target, type) {
+ if (eventListeners in target) {
+ const listenersForType = target[eventListeners].get(type);
+
+ if (listenersForType) {
+ return listenersForType.size;
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Decorate an object with event emitter functionality; basically using the
+ * class' prototype as mixin.
+ *
+ * @param Object target
+ * The object to decorate.
+ * @return Object
+ * The object given, mixed.
+ */
+ static decorate(target) {
+ const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
+ delete descriptors.constructor;
+ return Object.defineProperties(target, descriptors);
+ }
+
+ static get handler() {
+ return handler;
+ }
+
+ on(...args) {
+ return EventEmitter.on(this, ...args);
+ }
+
+ off(...args) {
+ EventEmitter.off(this, ...args);
+ }
+
+ clearEvents() {
+ EventEmitter.clearEvents(this);
+ }
+
+ once(...args) {
+ return EventEmitter.once(this, ...args);
+ }
+
+ emit(...args) {
+ EventEmitter.emit(this, ...args);
+ }
+
+ emitAsync(...args) {
+ return EventEmitter.emitAsync(this, ...args);
+ }
+
+ emitForTests(...args) {
+ if (flags.testing) {
+ EventEmitter.emit(this, ...args);
+ }
+ }
+
+ count(...args) {
+ return EventEmitter.count(this, ...args);
+ }
+}
+
+module.exports = EventEmitter;
+
+const isEventHandler = listener =>
+ listener && handler in listener && typeof listener[handler] === "function";
+
+const {
+ getNthPathExcluding,
+} = require("resource://devtools/shared/platform/stack.js");
+let loggingEnabled = false;
+
+if (!isWorker) {
+ loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false);
+ const observer = {
+ observe: () => {
+ loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
+ },
+ };
+ Services.prefs.addObserver("devtools.dump.emit", observer);
+
+ // Also listen for Loader unload to unregister the pref observer and
+ // prevent leaking
+ const unloadObserver = function (subject) {
+ if (subject.wrappedJSObject == require("@loader/unload")) {
+ Services.prefs.removeObserver("devtools.dump.emit", observer);
+ Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
+ }
+ };
+ Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
+}
+
+function serialize(target) {
+ const MAXLEN = 60;
+
+ // Undefined
+ if (typeof target === "undefined") {
+ return "undefined";
+ }
+
+ if (target === null) {
+ return "null";
+ }
+
+ // Number / String
+ if (typeof target === "string" || typeof target === "number") {
+ return truncate(target, MAXLEN);
+ }
+
+ // HTML Node
+ if (target.nodeName) {
+ let out = target.nodeName;
+
+ if (target.id) {
+ out += "#" + target.id;
+ }
+ if (target.className) {
+ out += "." + target.className;
+ }
+
+ return out;
+ }
+
+ // Array
+ if (Array.isArray(target)) {
+ return truncate(target.toSource(), MAXLEN);
+ }
+
+ // Function
+ if (typeof target === "function") {
+ return `function ${target.name ? target.name : "anonymous"}()`;
+ }
+
+ // Window
+ if (target?.constructor?.name === "Window") {
+ return `window (${target.location.origin})`;
+ }
+
+ // Object
+ if (typeof target === "object") {
+ let out = "{";
+
+ const entries = Object.entries(target);
+ for (let i = 0; i < Math.min(10, entries.length); i++) {
+ const [name, value] = entries[i];
+
+ if (i > 0) {
+ out += ", ";
+ }
+
+ out += `${name}: ${truncate(value, MAXLEN)}`;
+ }
+
+ return out + "}";
+ }
+
+ // Other
+ return truncate(target.toSource(), MAXLEN);
+}
+
+function truncate(value, maxLen) {
+ // We don't use value.toString() because it can throw.
+ const str = String(value);
+ return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
+}
+
+function logEvent(type, args) {
+ let argsOut = "";
+
+ // We need this try / catch to prevent any dead object errors.
+ try {
+ argsOut = `${args.map(serialize).join(", ")}`;
+ } catch (e) {
+ // Object is dead so the toolbox is most likely shutting down,
+ // do nothing.
+ }
+
+ const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
+
+ if (args.length) {
+ dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
+ } else {
+ dump(`EMITTING: emit(${type}) from ${path}\n`);
+ }
+}
diff --git a/devtools/shared/extend.js b/devtools/shared/extend.js
new file mode 100644
index 0000000000..020af5ea28
--- /dev/null
+++ b/devtools/shared/extend.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";
+
+/**
+ * Utility function, that is useful for creating objects that inherit from other
+ * objects, without associated classes.
+ *
+ * Replacement for `extends` API from "sdk/core/heritage".
+ */
+exports.extend = function (prototype, properties) {
+ return Object.create(prototype, Object.getOwnPropertyDescriptors(properties));
+};
diff --git a/devtools/shared/flags.js b/devtools/shared/flags.js
new file mode 100644
index 0000000000..1b0ec26472
--- /dev/null
+++ b/devtools/shared/flags.js
@@ -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/. */
+
+"use strict";
+
+/**
+ * This module controls various global flags that can be toggled on and off.
+ * These flags are generally used to change the behavior of the code during
+ * testing. They are tracked by preferences so that they are propagated
+ * between the parent and content processes. The flags are exposed via a module
+ * as a conveniene and to stop from littering preference names throughout the
+ * code ase.
+ *
+ * Each of the flags is documented where it is defined.
+ */
+
+/**
+ * We cannot make a normal property writeable on `exports` because
+ * the module system freezes it. This function observes a preference
+ * and provides the latest value through a getter.
+ */
+function makePrefTrackedFlag(exports, name, pref) {
+ let flag;
+ // We don't have access to pref in worker, so disable all logs by default
+ if (isWorker) {
+ flag = false;
+ } else {
+ flag = Services.prefs.getBoolPref(pref, false);
+ const prefObserver = () => {
+ flag = Services.prefs.getBoolPref(pref, false);
+ };
+ Services.prefs.addObserver(pref, prefObserver);
+
+ // Also listen for Loader unload to unregister the pref observer and prevent leaking
+ const unloadObserver = function (subject) {
+ if (subject.wrappedJSObject == require("@loader/unload")) {
+ Services.prefs.removeObserver(pref, prefObserver);
+ Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
+ }
+ };
+ Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
+ }
+ Object.defineProperty(exports, name, {
+ get() {
+ return flag;
+ },
+ });
+}
+
+/**
+ * Setting the "devtools.debugger.log" preference to true will enable logging of
+ * the RDP calls to the devtools server.
+ */
+makePrefTrackedFlag(exports, "wantLogging", "devtools.debugger.log");
+
+/**
+ * Setting the "devtools.debugger.log.verbose" preference to true will enable a
+ * more verbose logging of the the RDP. The "devtools.debugger.log" preference
+ * must be set to true as well for this to have any effect.
+ */
+makePrefTrackedFlag(exports, "wantVerbose", "devtools.debugger.log.verbose");
+
+/**
+ * Setting the "devtools.testing" preference to true will toggle on certain
+ * behaviors that can differ from the production version of the code. These
+ * behaviors typically enable easier testing or enhanced debugging features.
+ */
+makePrefTrackedFlag(exports, "testing", "devtools.testing");
diff --git a/devtools/shared/generate-uuid.js b/devtools/shared/generate-uuid.js
new file mode 100644
index 0000000000..bb54bd6c79
--- /dev/null
+++ b/devtools/shared/generate-uuid.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";
+
+const { generateUUID } = Services.uuid;
+
+/**
+ * Returns a new `uuid`.
+ *
+ */
+
+module.exports = { generateUUID };
diff --git a/devtools/shared/heapsnapshot/AutoMemMap.cpp b/devtools/shared/heapsnapshot/AutoMemMap.cpp
new file mode 100644
index 0000000000..8a7c89b4bf
--- /dev/null
+++ b/devtools/shared/heapsnapshot/AutoMemMap.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/AutoMemMap.h"
+
+#include "mozilla/Unused.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+namespace devtools {
+
+AutoMemMap::~AutoMemMap() {
+ if (addr) {
+ Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS);
+ addr = nullptr;
+ }
+
+ if (fileMap) {
+ Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS);
+ fileMap = nullptr;
+ }
+
+ if (fd) {
+ Unused << NS_WARN_IF(PR_Close(fd) != PR_SUCCESS);
+ fd = nullptr;
+ }
+}
+
+nsresult AutoMemMap::init(nsIFile* file, int flags, int mode,
+ PRFileMapProtect prot) {
+ MOZ_ASSERT(!fd);
+ MOZ_ASSERT(!fileMap);
+ MOZ_ASSERT(!addr);
+
+ nsresult rv;
+ int64_t inputFileSize;
+ rv = file->GetFileSize(&inputFileSize);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Check if the file is too big to memmap.
+ if (inputFileSize > int64_t(UINT32_MAX)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ fileSize = uint32_t(inputFileSize);
+
+ rv = file->OpenNSPRFileDesc(flags, mode, &fd);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!fd) return NS_ERROR_UNEXPECTED;
+
+ fileMap = PR_CreateFileMap(fd, inputFileSize, prot);
+ if (!fileMap) return NS_ERROR_UNEXPECTED;
+
+ addr = PR_MemMap(fileMap, 0, fileSize);
+ if (!addr) return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/AutoMemMap.h b/devtools/shared/heapsnapshot/AutoMemMap.h
new file mode 100644
index 0000000000..f67dacf0df
--- /dev/null
+++ b/devtools/shared/heapsnapshot/AutoMemMap.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_AutoMemMap_h
+#define mozilla_devtools_AutoMemMap_h
+
+#include <prio.h>
+#include "nsIFile.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "ErrorList.h"
+
+namespace mozilla {
+namespace devtools {
+
+// # AutoMemMap
+//
+// AutoMemMap is an RAII class to manage mapping a file to memory. It is a
+// wrapper around managing opening and closing a file and calling PR_MemMap and
+// PR_MemUnmap.
+//
+// Example usage:
+//
+// {
+// AutoMemMap mm;
+// if (NS_FAILED(mm.init("/path/to/desired/file"))) {
+// // Handle the error however you see fit.
+// return false;
+// }
+//
+// doStuffWithMappedMemory(mm.address());
+// }
+// // The memory is automatically unmapped when the AutoMemMap leaves scope.
+class MOZ_RAII AutoMemMap {
+ // At the time of this writing, this class supports file imports up to
+ // UINT32_MAX bytes due to limitations in the underlying function PR_MemMap.
+ uint32_t fileSize;
+ PRFileDesc* fd;
+ PRFileMap* fileMap;
+ void* addr;
+
+ AutoMemMap(const AutoMemMap& aOther) = delete;
+ void operator=(const AutoMemMap& aOther) = delete;
+
+ public:
+ explicit AutoMemMap()
+ : fileSize(0), fd(nullptr), fileMap(nullptr), addr(nullptr){};
+ ~AutoMemMap();
+
+ // Initialize this AutoMemMap.
+ nsresult init(nsIFile* file, int flags = PR_RDONLY, int mode = 0,
+ PRFileMapProtect prot = PR_PROT_READONLY);
+
+ // Get the size of the memory mapped file.
+ uint32_t size() const {
+ MOZ_ASSERT(fd, "Should only call size() if init() succeeded.");
+ return fileSize;
+ }
+
+ // Get the mapped memory.
+ void* address() {
+ MOZ_ASSERT(addr);
+ return addr;
+ }
+ const void* address() const {
+ MOZ_ASSERT(addr);
+ return addr;
+ }
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_AutoMemMap_h
diff --git a/devtools/shared/heapsnapshot/CensusUtils.js b/devtools/shared/heapsnapshot/CensusUtils.js
new file mode 100644
index 0000000000..3af68d3b57
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CensusUtils.js
@@ -0,0 +1,502 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ flatten,
+} = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
+
+/** * Visitor ****************************************************************/
+
+/**
+ * A Visitor visits each node and edge of a census report tree as the census
+ * report is being traversed by `walk`.
+ */
+function Visitor() {}
+exports.Visitor = Visitor;
+
+/**
+ * The `enter` method is called when a new sub-report is entered in traversal.
+ *
+ * @param {Object} breakdown
+ * The breakdown for the sub-report that is being entered by traversal.
+ *
+ * @param {Object} report
+ * The report generated by the given breakdown.
+ *
+ * @param {any} edge
+ * The edge leading to this sub-report. The edge is null if (but not iff!
+ * eg, null allocation stack edges) we are entering the root report.
+ */
+Visitor.prototype.enter = function (breakdown, report, edge) {};
+
+/**
+ * The `exit` method is called when traversal of a sub-report has finished.
+ *
+ * @param {Object} breakdown
+ * The breakdown for the sub-report whose traversal has finished.
+ *
+ * @param {Object} report
+ * The report generated by the given breakdown.
+ *
+ * @param {any} edge
+ * The edge leading to this sub-report. The edge is null if (but not iff!
+ * eg, null allocation stack edges) we are entering the root report.
+ */
+Visitor.prototype.exit = function (breakdown, report, edge) {};
+
+/**
+ * The `count` method is called when leaf nodes (reports whose breakdown is
+ * by: "count") in the report tree are encountered.
+ *
+ * @param {Object} breakdown
+ * The count breakdown for this report.
+ *
+ * @param {Object} report
+ * The report generated by a breakdown by "count".
+ *
+ * @param {any|null} edge
+ * The edge leading to this count report. The edge is null if we are
+ * entering the root report.
+ */
+Visitor.prototype.count = function (breakdown, report, edge) {};
+
+/** * getReportEdges *********************************************************/
+
+const EDGES = Object.create(null);
+
+EDGES.count = function (breakdown, report) {
+ return [];
+};
+
+EDGES.bucket = function (breakdown, report) {
+ return [];
+};
+
+EDGES.internalType = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: breakdown.then,
+ }));
+};
+
+EDGES.descriptiveType = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: breakdown.then,
+ }));
+};
+
+EDGES.objectClass = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: key === "other" ? breakdown.other : breakdown.then,
+ }));
+};
+
+EDGES.coarseType = function (breakdown, report) {
+ return [
+ { edge: "objects", referent: report.objects, breakdown: breakdown.objects },
+ { edge: "scripts", referent: report.scripts, breakdown: breakdown.scripts },
+ { edge: "strings", referent: report.strings, breakdown: breakdown.strings },
+ { edge: "other", referent: report.other, breakdown: breakdown.other },
+ { edge: "domNode", referent: report.domNode, breakdown: breakdown.domNode },
+ ];
+};
+
+EDGES.allocationStack = function (breakdown, report) {
+ const edges = [];
+ report.forEach((value, key) => {
+ edges.push({
+ edge: key,
+ referent: value,
+ breakdown: key === "noStack" ? breakdown.noStack : breakdown.then,
+ });
+ });
+ return edges;
+};
+
+EDGES.filename = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: key === "noFilename" ? breakdown.noFilename : breakdown.then,
+ }));
+};
+
+/**
+ * Get the set of outgoing edges from `report` as specified by the given
+ * breakdown.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ */
+function getReportEdges(breakdown, report) {
+ return EDGES[breakdown.by](breakdown, report);
+}
+exports.getReportEdges = getReportEdges;
+
+/** * walk *******************************************************************/
+
+function recursiveWalk(breakdown, edge, report, visitor) {
+ if (breakdown.by === "count") {
+ visitor.enter(breakdown, report, edge);
+ visitor.count(breakdown, report, edge);
+ visitor.exit(breakdown, report, edge);
+ } else {
+ visitor.enter(breakdown, report, edge);
+ for (const {
+ edge: ed,
+ referent,
+ breakdown: subBreakdown,
+ } of getReportEdges(breakdown, report)) {
+ recursiveWalk(subBreakdown, ed, referent, visitor);
+ }
+ visitor.exit(breakdown, report, edge);
+ }
+}
+
+/**
+ * Walk the given `report` that was generated by taking a census with the
+ * specified `breakdown`.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {Visitor} visitor
+ * The Visitor instance to call into while traversing.
+ */
+function walk(breakdown, report, visitor) {
+ recursiveWalk(breakdown, null, report, visitor);
+}
+exports.walk = walk;
+
+/** * diff *******************************************************************/
+
+/**
+ * Return true if the object is a Map, false otherwise. Works with Map objects
+ * from other globals, unlike `instanceof`.
+ *
+ * @returns {Boolean}
+ */
+function isMap(obj) {
+ return Object.prototype.toString.call(obj) === "[object Map]";
+}
+
+/**
+ * A Visitor for computing the difference between the census report being
+ * traversed and the given other census.
+ *
+ * @param {Object} otherCensus
+ * The other census report.
+ */
+function DiffVisitor(otherCensus) {
+ // The other census we are comparing against.
+ this._otherCensus = otherCensus;
+
+ // The total bytes and count of the basis census we are traversing.
+ this._totalBytes = 0;
+ this._totalCount = 0;
+
+ // Stack maintaining the current corresponding sub-report for the other
+ // census we are comparing against.
+ this._otherCensusStack = [];
+
+ // Stack maintaining the set of edges visited at each sub-report.
+ this._edgesVisited = [new Set()];
+
+ // The final delta census. Valid only after traversal.
+ this._results = null;
+
+ // Stack maintaining the results corresponding to each sub-report we are
+ // currently traversing.
+ this._resultsStack = [];
+}
+
+DiffVisitor.prototype = Object.create(Visitor.prototype);
+
+/**
+ * Given a report and an outgoing edge, get the edge's referent.
+ */
+DiffVisitor.prototype._get = function (report, edge) {
+ if (!report) {
+ return undefined;
+ }
+ return isMap(report) ? report.get(edge) : report[edge];
+};
+
+/**
+ * Given a report, an outgoing edge, and a value, set the edge's referent to
+ * the given value.
+ */
+DiffVisitor.prototype._set = function (report, edge, val) {
+ if (isMap(report)) {
+ report.set(edge, val);
+ } else {
+ report[edge] = val;
+ }
+};
+
+/**
+ * @overrides Visitor.prototype.enter
+ */
+DiffVisitor.prototype.enter = function (breakdown, report, edge) {
+ const newResults = breakdown.by === "allocationStack" ? new Map() : {};
+ let newOther;
+
+ if (!this._results) {
+ // This is the first time we have entered a sub-report.
+ this._results = newResults;
+ newOther = this._otherCensus;
+ } else {
+ const topResults = this._resultsStack[this._resultsStack.length - 1];
+ this._set(topResults, edge, newResults);
+
+ const topOther = this._otherCensusStack[this._otherCensusStack.length - 1];
+ newOther = this._get(topOther, edge);
+ }
+
+ this._resultsStack.push(newResults);
+ this._otherCensusStack.push(newOther);
+
+ const visited = this._edgesVisited[this._edgesVisited.length - 1];
+ visited.add(edge);
+ this._edgesVisited.push(new Set());
+};
+
+/**
+ * @overrides Visitor.prototype.exit
+ */
+DiffVisitor.prototype.exit = function (breakdown, report, edge) {
+ // Find all the edges in the other census report that were not traversed and
+ // add them to the results directly.
+ const other = this._otherCensusStack[this._otherCensusStack.length - 1];
+ if (other) {
+ const visited = this._edgesVisited[this._edgesVisited.length - 1];
+ const unvisited = getReportEdges(breakdown, other)
+ .map(e => e.edge)
+ .filter(e => !visited.has(e));
+ const results = this._resultsStack[this._resultsStack.length - 1];
+ for (const edg of unvisited) {
+ this._set(results, edg, this._get(other, edg));
+ }
+ }
+
+ this._otherCensusStack.pop();
+ this._resultsStack.pop();
+ this._edgesVisited.pop();
+};
+
+/**
+ * @overrides Visitor.prototype.count
+ */
+DiffVisitor.prototype.count = function (breakdown, report, edge) {
+ const other = this._otherCensusStack[this._otherCensusStack.length - 1];
+ const results = this._resultsStack[this._resultsStack.length - 1];
+
+ if (breakdown.count) {
+ this._totalCount += report.count;
+ }
+ if (breakdown.bytes) {
+ this._totalBytes += report.bytes;
+ }
+
+ if (other) {
+ if (breakdown.count) {
+ results.count = other.count - report.count;
+ }
+ if (breakdown.bytes) {
+ results.bytes = other.bytes - report.bytes;
+ }
+ } else {
+ if (breakdown.count) {
+ results.count = -report.count;
+ }
+ if (breakdown.bytes) {
+ results.bytes = -report.bytes;
+ }
+ }
+};
+
+const basisTotalBytes = (exports.basisTotalBytes = Symbol("basisTotalBytes"));
+const basisTotalCount = (exports.basisTotalCount = Symbol("basisTotalCount"));
+
+/**
+ * Get the resulting report of the difference between the traversed census
+ * report and the other census report.
+ *
+ * @returns {Object}
+ * The delta census report.
+ */
+DiffVisitor.prototype.results = function () {
+ if (!this._results) {
+ throw new Error("Attempt to get results before computing diff!");
+ }
+
+ if (this._resultsStack.length) {
+ throw new Error("Attempt to get results while still computing diff!");
+ }
+
+ this._results[basisTotalBytes] = this._totalBytes;
+ this._results[basisTotalCount] = this._totalCount;
+
+ return this._results;
+};
+
+/**
+ * Take the difference between two censuses. The resulting delta report
+ * contains the number/size of things that are in the `endCensus` that are not
+ * in the `startCensus`.
+ *
+ * @param {Object} breakdown
+ * The breakdown used to generate both census reports.
+ *
+ * @param {Object} startCensus
+ * The first census report.
+ *
+ * @param {Object} endCensus
+ * The second census report.
+ *
+ * @returns {Object}
+ * A delta report mirroring the structure of the two census reports (as
+ * specified by the given breakdown). Has two additional properties:
+ * - {Number} basisTotalBytes: the total number of bytes in the start
+ * census.
+ * - {Number} basisTotalCount: the total count in the start census.
+ */
+function diff(breakdown, startCensus, endCensus) {
+ const visitor = new DiffVisitor(endCensus);
+ walk(breakdown, startCensus, visitor);
+ return visitor.results();
+}
+exports.diff = diff;
+
+/**
+ * Creates a hash map mapping node IDs to its parent node.
+ *
+ * @param {CensusTreeNode} node
+ * @param {Object<number, TreeNode>} aggregator
+ *
+ * @return {Object<number, TreeNode>}
+ */
+const createParentMap = function (node, getId = n => n.id, aggregator = {}) {
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ const child = node.children[i];
+ aggregator[getId(child)] = node;
+ createParentMap(child, getId, aggregator);
+ }
+ }
+ return aggregator;
+};
+exports.createParentMap = createParentMap;
+
+const BUCKET = Object.freeze({ by: "bucket" });
+
+/**
+ * Convert a breakdown whose leaves are { by: "count" } to an identical
+ * breakdown, except with { by: "bucket" } leaves.
+ *
+ * @param {Object} breakdown
+ * @returns {Object}
+ */
+exports.countToBucketBreakdown = function (breakdown) {
+ if (typeof breakdown !== "object" || !breakdown) {
+ return breakdown;
+ }
+
+ if (breakdown.by === "count") {
+ return BUCKET;
+ }
+
+ const keys = Object.keys(breakdown);
+ const vals = keys.reduce((vs, k) => {
+ vs.push(exports.countToBucketBreakdown(breakdown[k]));
+ return vs;
+ }, []);
+
+ const result = {};
+ for (let i = 0, length = keys.length; i < length; i++) {
+ result[keys[i]] = vals[i];
+ }
+
+ return Object.freeze(result);
+};
+
+/**
+ * A Visitor for finding report leaves by their DFS index.
+ */
+function GetLeavesVisitor(targetIndices) {
+ this._index = -1;
+ this._targetIndices = targetIndices;
+ this._leaves = [];
+}
+
+GetLeavesVisitor.prototype = Object.create(Visitor.prototype);
+
+/**
+ * @overrides Visitor.prototype.enter
+ */
+GetLeavesVisitor.prototype.enter = function (breakdown, report, edge) {
+ this._index++;
+ if (this._targetIndices.has(this._index)) {
+ this._leaves.push(report);
+ }
+};
+
+/**
+ * Get the accumulated report leaves after traversal.
+ */
+GetLeavesVisitor.prototype.leaves = function () {
+ if (this._index === -1) {
+ throw new Error("Attempt to call `leaves` before traversing report!");
+ }
+ return this._leaves;
+};
+
+/**
+ * Given a set of indices of leaves in a pre-order depth-first traversal of the
+ * given census report, return the leaves.
+ *
+ * @param {Set<Number>} indices
+ * @param {Object} breakdown
+ * @param {Object} report
+ *
+ * @returns {Array<Object>}
+ */
+exports.getReportLeaves = function (indices, breakdown, report) {
+ const visitor = new GetLeavesVisitor(indices);
+ walk(breakdown, report, visitor);
+ return visitor.leaves();
+};
+
+/**
+ * Get a list of the individual node IDs that belong to the census report leaves
+ * of the given indices.
+ *
+ * @param {Set<Number>} indices
+ * @param {Object} breakdown
+ * @param {HeapSnapshot} snapshot
+ *
+ * @returns {Array<NodeId>}
+ */
+exports.getCensusIndividuals = function (indices, countBreakdown, snapshot) {
+ const bucketBreakdown = exports.countToBucketBreakdown(countBreakdown);
+ const bucketReport = snapshot.takeCensus({ breakdown: bucketBreakdown });
+ const buckets = exports.getReportLeaves(
+ indices,
+ bucketBreakdown,
+ bucketReport
+ );
+ return flatten(buckets);
+};
diff --git a/devtools/shared/heapsnapshot/CoreDump.pb.cc b/devtools/shared/heapsnapshot/CoreDump.pb.cc
new file mode 100644
index 0000000000..9283733771
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CoreDump.pb.cc
@@ -0,0 +1,2242 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: CoreDump.proto
+
+#include "CoreDump.pb.h"
+
+#include <algorithm>
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/wire_format_lite.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+// @@protoc_insertion_point(includes)
+#include <google/protobuf/port_def.inc>
+
+PROTOBUF_PRAGMA_INIT_SEG
+
+namespace _pb = ::PROTOBUF_NAMESPACE_ID;
+namespace _pbi = _pb::internal;
+
+namespace mozilla {
+namespace devtools {
+namespace protobuf {
+PROTOBUF_CONSTEXPR Metadata::Metadata(
+ ::_pbi::ConstantInitialized): _impl_{
+ /*decltype(_impl_._has_bits_)*/{}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_.timestamp_)*/uint64_t{0u}} {}
+struct MetadataDefaultTypeInternal {
+ PROTOBUF_CONSTEXPR MetadataDefaultTypeInternal()
+ : _instance(::_pbi::ConstantInitialized{}) {}
+ ~MetadataDefaultTypeInternal() {}
+ union {
+ Metadata _instance;
+ };
+};
+PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 MetadataDefaultTypeInternal _Metadata_default_instance_;
+PROTOBUF_CONSTEXPR StackFrame_Data::StackFrame_Data(
+ ::_pbi::ConstantInitialized): _impl_{
+ /*decltype(_impl_._has_bits_)*/{}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_.parent_)*/nullptr
+ , /*decltype(_impl_.id_)*/uint64_t{0u}
+ , /*decltype(_impl_.line_)*/0u
+ , /*decltype(_impl_.column_)*/0u
+ , /*decltype(_impl_.issystem_)*/false
+ , /*decltype(_impl_.isselfhosted_)*/false
+ , /*decltype(_impl_.SourceOrRef_)*/{}
+ , /*decltype(_impl_.FunctionDisplayNameOrRef_)*/{}
+ , /*decltype(_impl_._oneof_case_)*/{}} {}
+struct StackFrame_DataDefaultTypeInternal {
+ PROTOBUF_CONSTEXPR StackFrame_DataDefaultTypeInternal()
+ : _instance(::_pbi::ConstantInitialized{}) {}
+ ~StackFrame_DataDefaultTypeInternal() {}
+ union {
+ StackFrame_Data _instance;
+ };
+};
+PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 StackFrame_DataDefaultTypeInternal _StackFrame_Data_default_instance_;
+PROTOBUF_CONSTEXPR StackFrame::StackFrame(
+ ::_pbi::ConstantInitialized): _impl_{
+ /*decltype(_impl_.StackFrameType_)*/{}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_._oneof_case_)*/{}} {}
+struct StackFrameDefaultTypeInternal {
+ PROTOBUF_CONSTEXPR StackFrameDefaultTypeInternal()
+ : _instance(::_pbi::ConstantInitialized{}) {}
+ ~StackFrameDefaultTypeInternal() {}
+ union {
+ StackFrame _instance;
+ };
+};
+PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 StackFrameDefaultTypeInternal _StackFrame_default_instance_;
+PROTOBUF_CONSTEXPR Node::Node(
+ ::_pbi::ConstantInitialized): _impl_{
+ /*decltype(_impl_._has_bits_)*/{}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_.edges_)*/{}
+ , /*decltype(_impl_.allocationstack_)*/nullptr
+ , /*decltype(_impl_.id_)*/uint64_t{0u}
+ , /*decltype(_impl_.size_)*/uint64_t{0u}
+ , /*decltype(_impl_.coarsetype_)*/0u
+ , /*decltype(_impl_.TypeNameOrRef_)*/{}
+ , /*decltype(_impl_.JSObjectClassNameOrRef_)*/{}
+ , /*decltype(_impl_.ScriptFilenameOrRef_)*/{}
+ , /*decltype(_impl_.descriptiveTypeNameOrRef_)*/{}
+ , /*decltype(_impl_._oneof_case_)*/{}} {}
+struct NodeDefaultTypeInternal {
+ PROTOBUF_CONSTEXPR NodeDefaultTypeInternal()
+ : _instance(::_pbi::ConstantInitialized{}) {}
+ ~NodeDefaultTypeInternal() {}
+ union {
+ Node _instance;
+ };
+};
+PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 NodeDefaultTypeInternal _Node_default_instance_;
+PROTOBUF_CONSTEXPR Edge::Edge(
+ ::_pbi::ConstantInitialized): _impl_{
+ /*decltype(_impl_._has_bits_)*/{}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_.referent_)*/uint64_t{0u}
+ , /*decltype(_impl_.EdgeNameOrRef_)*/{}
+ , /*decltype(_impl_._oneof_case_)*/{}} {}
+struct EdgeDefaultTypeInternal {
+ PROTOBUF_CONSTEXPR EdgeDefaultTypeInternal()
+ : _instance(::_pbi::ConstantInitialized{}) {}
+ ~EdgeDefaultTypeInternal() {}
+ union {
+ Edge _instance;
+ };
+};
+PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 EdgeDefaultTypeInternal _Edge_default_instance_;
+} // namespace protobuf
+} // namespace devtools
+} // namespace mozilla
+namespace mozilla {
+namespace devtools {
+namespace protobuf {
+
+// ===================================================================
+
+class Metadata::_Internal {
+ public:
+ using HasBits = decltype(std::declval<Metadata>()._impl_._has_bits_);
+ static void set_has_timestamp(HasBits* has_bits) {
+ (*has_bits)[0] |= 1u;
+ }
+};
+
+Metadata::Metadata(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) {
+ SharedCtor(arena, is_message_owned);
+ // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.Metadata)
+}
+Metadata::Metadata(const Metadata& from)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite() {
+ Metadata* const _this = this; (void)_this;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){from._impl_._has_bits_}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.timestamp_){}};
+
+ _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+ _this->_impl_.timestamp_ = from._impl_.timestamp_;
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Metadata)
+}
+
+inline void Metadata::SharedCtor(
+ ::_pb::Arena* arena, bool is_message_owned) {
+ (void)arena;
+ (void)is_message_owned;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.timestamp_){uint64_t{0u}}
+ };
+}
+
+Metadata::~Metadata() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Metadata)
+ if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) {
+ (void)arena;
+ return;
+ }
+ SharedDtor();
+}
+
+inline void Metadata::SharedDtor() {
+ GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
+}
+
+void Metadata::SetCachedSize(int size) const {
+ _impl_._cached_size_.Set(size);
+}
+
+void Metadata::Clear() {
+// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.Metadata)
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ _impl_.timestamp_ = uint64_t{0u};
+ _impl_._has_bits_.Clear();
+ _internal_metadata_.Clear<std::string>();
+}
+
+const char* Metadata::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
+#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
+ _Internal::HasBits has_bits{};
+ while (!ctx->Done(&ptr)) {
+ uint32_t tag;
+ ptr = ::_pbi::ReadTag(ptr, &tag);
+ switch (tag >> 3) {
+ // optional uint64 timeStamp = 1;
+ case 1:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) {
+ _Internal::set_has_timestamp(&has_bits);
+ _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ default:
+ goto handle_unusual;
+ } // switch
+ handle_unusual:
+ if ((tag == 0) || ((tag & 7) == 4)) {
+ CHK_(ptr);
+ ctx->SetLastTag(tag);
+ goto message_done;
+ }
+ ptr = UnknownFieldParse(
+ tag,
+ _internal_metadata_.mutable_unknown_fields<std::string>(),
+ ptr, ctx);
+ CHK_(ptr != nullptr);
+ } // while
+message_done:
+ _impl_._has_bits_.Or(has_bits);
+ return ptr;
+failure:
+ ptr = nullptr;
+ goto message_done;
+#undef CHK_
+}
+
+uint8_t* Metadata::_InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Metadata)
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ cached_has_bits = _impl_._has_bits_[0];
+ // optional uint64 timeStamp = 1;
+ if (cached_has_bits & 0x00000001u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_timestamp(), target);
+ }
+
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(),
+ static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Metadata)
+ return target;
+}
+
+size_t Metadata::ByteSizeLong() const {
+// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.Metadata)
+ size_t total_size = 0;
+
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ // optional uint64 timeStamp = 1;
+ cached_has_bits = _impl_._has_bits_[0];
+ if (cached_has_bits & 0x00000001u) {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp());
+ }
+
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size();
+ }
+ int cached_size = ::_pbi::ToCachedSize(total_size);
+ SetCachedSize(cached_size);
+ return total_size;
+}
+
+void Metadata::CheckTypeAndMergeFrom(
+ const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) {
+ MergeFrom(*::_pbi::DownCast<const Metadata*>(
+ &from));
+}
+
+void Metadata::MergeFrom(const Metadata& from) {
+ Metadata* const _this = this;
+ // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.Metadata)
+ GOOGLE_DCHECK_NE(&from, _this);
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ if (from._internal_has_timestamp()) {
+ _this->_internal_set_timestamp(from._internal_timestamp());
+ }
+ _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+}
+
+void Metadata::CopyFrom(const Metadata& from) {
+// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.Metadata)
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Metadata::IsInitialized() const {
+ return true;
+}
+
+void Metadata::InternalSwap(Metadata* other) {
+ using std::swap;
+ _internal_metadata_.InternalSwap(&other->_internal_metadata_);
+ swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]);
+ swap(_impl_.timestamp_, other->_impl_.timestamp_);
+}
+
+std::string Metadata::GetTypeName() const {
+ return "mozilla.devtools.protobuf.Metadata";
+}
+
+
+// ===================================================================
+
+class StackFrame_Data::_Internal {
+ public:
+ using HasBits = decltype(std::declval<StackFrame_Data>()._impl_._has_bits_);
+ static void set_has_id(HasBits* has_bits) {
+ (*has_bits)[0] |= 2u;
+ }
+ static const ::mozilla::devtools::protobuf::StackFrame& parent(const StackFrame_Data* msg);
+ static void set_has_parent(HasBits* has_bits) {
+ (*has_bits)[0] |= 1u;
+ }
+ static void set_has_line(HasBits* has_bits) {
+ (*has_bits)[0] |= 4u;
+ }
+ static void set_has_column(HasBits* has_bits) {
+ (*has_bits)[0] |= 8u;
+ }
+ static void set_has_issystem(HasBits* has_bits) {
+ (*has_bits)[0] |= 16u;
+ }
+ static void set_has_isselfhosted(HasBits* has_bits) {
+ (*has_bits)[0] |= 32u;
+ }
+};
+
+const ::mozilla::devtools::protobuf::StackFrame&
+StackFrame_Data::_Internal::parent(const StackFrame_Data* msg) {
+ return *msg->_impl_.parent_;
+}
+StackFrame_Data::StackFrame_Data(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) {
+ SharedCtor(arena, is_message_owned);
+ // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.StackFrame.Data)
+}
+StackFrame_Data::StackFrame_Data(const StackFrame_Data& from)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite() {
+ StackFrame_Data* const _this = this; (void)_this;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){from._impl_._has_bits_}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.parent_){nullptr}
+ , decltype(_impl_.id_){}
+ , decltype(_impl_.line_){}
+ , decltype(_impl_.column_){}
+ , decltype(_impl_.issystem_){}
+ , decltype(_impl_.isselfhosted_){}
+ , decltype(_impl_.SourceOrRef_){}
+ , decltype(_impl_.FunctionDisplayNameOrRef_){}
+ , /*decltype(_impl_._oneof_case_)*/{}};
+
+ _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+ if (from._internal_has_parent()) {
+ _this->_impl_.parent_ = new ::mozilla::devtools::protobuf::StackFrame(*from._impl_.parent_);
+ }
+ ::memcpy(&_impl_.id_, &from._impl_.id_,
+ static_cast<size_t>(reinterpret_cast<char*>(&_impl_.isselfhosted_) -
+ reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.isselfhosted_));
+ clear_has_SourceOrRef();
+ switch (from.SourceOrRef_case()) {
+ case kSource: {
+ _this->_internal_set_source(from._internal_source());
+ break;
+ }
+ case kSourceRef: {
+ _this->_internal_set_sourceref(from._internal_sourceref());
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ clear_has_FunctionDisplayNameOrRef();
+ switch (from.FunctionDisplayNameOrRef_case()) {
+ case kFunctionDisplayName: {
+ _this->_internal_set_functiondisplayname(from._internal_functiondisplayname());
+ break;
+ }
+ case kFunctionDisplayNameRef: {
+ _this->_internal_set_functiondisplaynameref(from._internal_functiondisplaynameref());
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame.Data)
+}
+
+inline void StackFrame_Data::SharedCtor(
+ ::_pb::Arena* arena, bool is_message_owned) {
+ (void)arena;
+ (void)is_message_owned;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.parent_){nullptr}
+ , decltype(_impl_.id_){uint64_t{0u}}
+ , decltype(_impl_.line_){0u}
+ , decltype(_impl_.column_){0u}
+ , decltype(_impl_.issystem_){false}
+ , decltype(_impl_.isselfhosted_){false}
+ , decltype(_impl_.SourceOrRef_){}
+ , decltype(_impl_.FunctionDisplayNameOrRef_){}
+ , /*decltype(_impl_._oneof_case_)*/{}
+ };
+ clear_has_SourceOrRef();
+ clear_has_FunctionDisplayNameOrRef();
+}
+
+StackFrame_Data::~StackFrame_Data() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame.Data)
+ if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) {
+ (void)arena;
+ return;
+ }
+ SharedDtor();
+}
+
+inline void StackFrame_Data::SharedDtor() {
+ GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
+ if (this != internal_default_instance()) delete _impl_.parent_;
+ if (has_SourceOrRef()) {
+ clear_SourceOrRef();
+ }
+ if (has_FunctionDisplayNameOrRef()) {
+ clear_FunctionDisplayNameOrRef();
+ }
+}
+
+void StackFrame_Data::SetCachedSize(int size) const {
+ _impl_._cached_size_.Set(size);
+}
+
+void StackFrame_Data::clear_SourceOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.StackFrame.Data)
+ switch (SourceOrRef_case()) {
+ case kSource: {
+ _impl_.SourceOrRef_.source_.Destroy();
+ break;
+ }
+ case kSourceRef: {
+ // No need to clear
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[0] = SOURCEORREF_NOT_SET;
+}
+
+void StackFrame_Data::clear_FunctionDisplayNameOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.StackFrame.Data)
+ switch (FunctionDisplayNameOrRef_case()) {
+ case kFunctionDisplayName: {
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Destroy();
+ break;
+ }
+ case kFunctionDisplayNameRef: {
+ // No need to clear
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET;
+}
+
+
+void StackFrame_Data::Clear() {
+// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.StackFrame.Data)
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ cached_has_bits = _impl_._has_bits_[0];
+ if (cached_has_bits & 0x00000001u) {
+ GOOGLE_DCHECK(_impl_.parent_ != nullptr);
+ _impl_.parent_->Clear();
+ }
+ if (cached_has_bits & 0x0000003eu) {
+ ::memset(&_impl_.id_, 0, static_cast<size_t>(
+ reinterpret_cast<char*>(&_impl_.isselfhosted_) -
+ reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.isselfhosted_));
+ }
+ clear_SourceOrRef();
+ clear_FunctionDisplayNameOrRef();
+ _impl_._has_bits_.Clear();
+ _internal_metadata_.Clear<std::string>();
+}
+
+const char* StackFrame_Data::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
+#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
+ _Internal::HasBits has_bits{};
+ while (!ctx->Done(&ptr)) {
+ uint32_t tag;
+ ptr = ::_pbi::ReadTag(ptr, &tag);
+ switch (tag >> 3) {
+ // optional uint64 id = 1;
+ case 1:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) {
+ _Internal::set_has_id(&has_bits);
+ _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ case 2:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) {
+ ptr = ctx->ParseMessage(_internal_mutable_parent(), ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional uint32 line = 3;
+ case 3:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 24)) {
+ _Internal::set_has_line(&has_bits);
+ _impl_.line_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional uint32 column = 4;
+ case 4:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 32)) {
+ _Internal::set_has_column(&has_bits);
+ _impl_.column_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes source = 5;
+ case 5:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 42)) {
+ auto str = _internal_mutable_source();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 sourceRef = 6;
+ case 6:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 48)) {
+ _internal_set_sourceref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes functionDisplayName = 7;
+ case 7:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 58)) {
+ auto str = _internal_mutable_functiondisplayname();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 functionDisplayNameRef = 8;
+ case 8:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 64)) {
+ _internal_set_functiondisplaynameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional bool isSystem = 9;
+ case 9:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 72)) {
+ _Internal::set_has_issystem(&has_bits);
+ _impl_.issystem_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional bool isSelfHosted = 10;
+ case 10:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 80)) {
+ _Internal::set_has_isselfhosted(&has_bits);
+ _impl_.isselfhosted_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ default:
+ goto handle_unusual;
+ } // switch
+ handle_unusual:
+ if ((tag == 0) || ((tag & 7) == 4)) {
+ CHK_(ptr);
+ ctx->SetLastTag(tag);
+ goto message_done;
+ }
+ ptr = UnknownFieldParse(
+ tag,
+ _internal_metadata_.mutable_unknown_fields<std::string>(),
+ ptr, ctx);
+ CHK_(ptr != nullptr);
+ } // while
+message_done:
+ _impl_._has_bits_.Or(has_bits);
+ return ptr;
+failure:
+ ptr = nullptr;
+ goto message_done;
+#undef CHK_
+}
+
+uint8_t* StackFrame_Data::_InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame.Data)
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ cached_has_bits = _impl_._has_bits_[0];
+ // optional uint64 id = 1;
+ if (cached_has_bits & 0x00000002u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_id(), target);
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ if (cached_has_bits & 0x00000001u) {
+ target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::
+ InternalWriteMessage(2, _Internal::parent(this),
+ _Internal::parent(this).GetCachedSize(), target, stream);
+ }
+
+ // optional uint32 line = 3;
+ if (cached_has_bits & 0x00000004u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt32ToArray(3, this->_internal_line(), target);
+ }
+
+ // optional uint32 column = 4;
+ if (cached_has_bits & 0x00000008u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt32ToArray(4, this->_internal_column(), target);
+ }
+
+ switch (SourceOrRef_case()) {
+ case kSource: {
+ target = stream->WriteBytesMaybeAliased(
+ 5, this->_internal_source(), target);
+ break;
+ }
+ case kSourceRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(6, this->_internal_sourceref(), target);
+ break;
+ }
+ default: ;
+ }
+ switch (FunctionDisplayNameOrRef_case()) {
+ case kFunctionDisplayName: {
+ target = stream->WriteBytesMaybeAliased(
+ 7, this->_internal_functiondisplayname(), target);
+ break;
+ }
+ case kFunctionDisplayNameRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(8, this->_internal_functiondisplaynameref(), target);
+ break;
+ }
+ default: ;
+ }
+ // optional bool isSystem = 9;
+ if (cached_has_bits & 0x00000010u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteBoolToArray(9, this->_internal_issystem(), target);
+ }
+
+ // optional bool isSelfHosted = 10;
+ if (cached_has_bits & 0x00000020u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteBoolToArray(10, this->_internal_isselfhosted(), target);
+ }
+
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(),
+ static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame.Data)
+ return target;
+}
+
+size_t StackFrame_Data::ByteSizeLong() const {
+// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.StackFrame.Data)
+ size_t total_size = 0;
+
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ cached_has_bits = _impl_._has_bits_[0];
+ if (cached_has_bits & 0x0000003fu) {
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ if (cached_has_bits & 0x00000001u) {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(
+ *_impl_.parent_);
+ }
+
+ // optional uint64 id = 1;
+ if (cached_has_bits & 0x00000002u) {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id());
+ }
+
+ // optional uint32 line = 3;
+ if (cached_has_bits & 0x00000004u) {
+ total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_line());
+ }
+
+ // optional uint32 column = 4;
+ if (cached_has_bits & 0x00000008u) {
+ total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_column());
+ }
+
+ // optional bool isSystem = 9;
+ if (cached_has_bits & 0x00000010u) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool isSelfHosted = 10;
+ if (cached_has_bits & 0x00000020u) {
+ total_size += 1 + 1;
+ }
+
+ }
+ switch (SourceOrRef_case()) {
+ // bytes source = 5;
+ case kSource: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_source());
+ break;
+ }
+ // uint64 sourceRef = 6;
+ case kSourceRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_sourceref());
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (FunctionDisplayNameOrRef_case()) {
+ // bytes functionDisplayName = 7;
+ case kFunctionDisplayName: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_functiondisplayname());
+ break;
+ }
+ // uint64 functionDisplayNameRef = 8;
+ case kFunctionDisplayNameRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_functiondisplaynameref());
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size();
+ }
+ int cached_size = ::_pbi::ToCachedSize(total_size);
+ SetCachedSize(cached_size);
+ return total_size;
+}
+
+void StackFrame_Data::CheckTypeAndMergeFrom(
+ const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) {
+ MergeFrom(*::_pbi::DownCast<const StackFrame_Data*>(
+ &from));
+}
+
+void StackFrame_Data::MergeFrom(const StackFrame_Data& from) {
+ StackFrame_Data* const _this = this;
+ // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.StackFrame.Data)
+ GOOGLE_DCHECK_NE(&from, _this);
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ cached_has_bits = from._impl_._has_bits_[0];
+ if (cached_has_bits & 0x0000003fu) {
+ if (cached_has_bits & 0x00000001u) {
+ _this->_internal_mutable_parent()->::mozilla::devtools::protobuf::StackFrame::MergeFrom(
+ from._internal_parent());
+ }
+ if (cached_has_bits & 0x00000002u) {
+ _this->_impl_.id_ = from._impl_.id_;
+ }
+ if (cached_has_bits & 0x00000004u) {
+ _this->_impl_.line_ = from._impl_.line_;
+ }
+ if (cached_has_bits & 0x00000008u) {
+ _this->_impl_.column_ = from._impl_.column_;
+ }
+ if (cached_has_bits & 0x00000010u) {
+ _this->_impl_.issystem_ = from._impl_.issystem_;
+ }
+ if (cached_has_bits & 0x00000020u) {
+ _this->_impl_.isselfhosted_ = from._impl_.isselfhosted_;
+ }
+ _this->_impl_._has_bits_[0] |= cached_has_bits;
+ }
+ switch (from.SourceOrRef_case()) {
+ case kSource: {
+ _this->_internal_set_source(from._internal_source());
+ break;
+ }
+ case kSourceRef: {
+ _this->_internal_set_sourceref(from._internal_sourceref());
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.FunctionDisplayNameOrRef_case()) {
+ case kFunctionDisplayName: {
+ _this->_internal_set_functiondisplayname(from._internal_functiondisplayname());
+ break;
+ }
+ case kFunctionDisplayNameRef: {
+ _this->_internal_set_functiondisplaynameref(from._internal_functiondisplaynameref());
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+}
+
+void StackFrame_Data::CopyFrom(const StackFrame_Data& from) {
+// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.StackFrame.Data)
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool StackFrame_Data::IsInitialized() const {
+ return true;
+}
+
+void StackFrame_Data::InternalSwap(StackFrame_Data* other) {
+ using std::swap;
+ _internal_metadata_.InternalSwap(&other->_internal_metadata_);
+ swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]);
+ ::PROTOBUF_NAMESPACE_ID::internal::memswap<
+ PROTOBUF_FIELD_OFFSET(StackFrame_Data, _impl_.isselfhosted_)
+ + sizeof(StackFrame_Data::_impl_.isselfhosted_)
+ - PROTOBUF_FIELD_OFFSET(StackFrame_Data, _impl_.parent_)>(
+ reinterpret_cast<char*>(&_impl_.parent_),
+ reinterpret_cast<char*>(&other->_impl_.parent_));
+ swap(_impl_.SourceOrRef_, other->_impl_.SourceOrRef_);
+ swap(_impl_.FunctionDisplayNameOrRef_, other->_impl_.FunctionDisplayNameOrRef_);
+ swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]);
+ swap(_impl_._oneof_case_[1], other->_impl_._oneof_case_[1]);
+}
+
+std::string StackFrame_Data::GetTypeName() const {
+ return "mozilla.devtools.protobuf.StackFrame.Data";
+}
+
+
+// ===================================================================
+
+class StackFrame::_Internal {
+ public:
+ static const ::mozilla::devtools::protobuf::StackFrame_Data& data(const StackFrame* msg);
+};
+
+const ::mozilla::devtools::protobuf::StackFrame_Data&
+StackFrame::_Internal::data(const StackFrame* msg) {
+ return *msg->_impl_.StackFrameType_.data_;
+}
+void StackFrame::set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data) {
+ ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation();
+ clear_StackFrameType();
+ if (data) {
+ ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena =
+ ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(data);
+ if (message_arena != submessage_arena) {
+ data = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage(
+ message_arena, data, submessage_arena);
+ }
+ set_has_data();
+ _impl_.StackFrameType_.data_ = data;
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.data)
+}
+StackFrame::StackFrame(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) {
+ SharedCtor(arena, is_message_owned);
+ // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.StackFrame)
+}
+StackFrame::StackFrame(const StackFrame& from)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite() {
+ StackFrame* const _this = this; (void)_this;
+ new (&_impl_) Impl_{
+ decltype(_impl_.StackFrameType_){}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_._oneof_case_)*/{}};
+
+ _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+ clear_has_StackFrameType();
+ switch (from.StackFrameType_case()) {
+ case kData: {
+ _this->_internal_mutable_data()->::mozilla::devtools::protobuf::StackFrame_Data::MergeFrom(
+ from._internal_data());
+ break;
+ }
+ case kRef: {
+ _this->_internal_set_ref(from._internal_ref());
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame)
+}
+
+inline void StackFrame::SharedCtor(
+ ::_pb::Arena* arena, bool is_message_owned) {
+ (void)arena;
+ (void)is_message_owned;
+ new (&_impl_) Impl_{
+ decltype(_impl_.StackFrameType_){}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , /*decltype(_impl_._oneof_case_)*/{}
+ };
+ clear_has_StackFrameType();
+}
+
+StackFrame::~StackFrame() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame)
+ if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) {
+ (void)arena;
+ return;
+ }
+ SharedDtor();
+}
+
+inline void StackFrame::SharedDtor() {
+ GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
+ if (has_StackFrameType()) {
+ clear_StackFrameType();
+ }
+}
+
+void StackFrame::SetCachedSize(int size) const {
+ _impl_._cached_size_.Set(size);
+}
+
+void StackFrame::clear_StackFrameType() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.StackFrame)
+ switch (StackFrameType_case()) {
+ case kData: {
+ if (GetArenaForAllocation() == nullptr) {
+ delete _impl_.StackFrameType_.data_;
+ }
+ break;
+ }
+ case kRef: {
+ // No need to clear
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[0] = STACKFRAMETYPE_NOT_SET;
+}
+
+
+void StackFrame::Clear() {
+// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.StackFrame)
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ clear_StackFrameType();
+ _internal_metadata_.Clear<std::string>();
+}
+
+const char* StackFrame::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
+#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
+ while (!ctx->Done(&ptr)) {
+ uint32_t tag;
+ ptr = ::_pbi::ReadTag(ptr, &tag);
+ switch (tag >> 3) {
+ // .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ case 1:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 10)) {
+ ptr = ctx->ParseMessage(_internal_mutable_data(), ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 ref = 2;
+ case 2:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 16)) {
+ _internal_set_ref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ default:
+ goto handle_unusual;
+ } // switch
+ handle_unusual:
+ if ((tag == 0) || ((tag & 7) == 4)) {
+ CHK_(ptr);
+ ctx->SetLastTag(tag);
+ goto message_done;
+ }
+ ptr = UnknownFieldParse(
+ tag,
+ _internal_metadata_.mutable_unknown_fields<std::string>(),
+ ptr, ctx);
+ CHK_(ptr != nullptr);
+ } // while
+message_done:
+ return ptr;
+failure:
+ ptr = nullptr;
+ goto message_done;
+#undef CHK_
+}
+
+uint8_t* StackFrame::_InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame)
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ switch (StackFrameType_case()) {
+ case kData: {
+ target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::
+ InternalWriteMessage(1, _Internal::data(this),
+ _Internal::data(this).GetCachedSize(), target, stream);
+ break;
+ }
+ case kRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(2, this->_internal_ref(), target);
+ break;
+ }
+ default: ;
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(),
+ static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame)
+ return target;
+}
+
+size_t StackFrame::ByteSizeLong() const {
+// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.StackFrame)
+ size_t total_size = 0;
+
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ switch (StackFrameType_case()) {
+ // .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ case kData: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(
+ *_impl_.StackFrameType_.data_);
+ break;
+ }
+ // uint64 ref = 2;
+ case kRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_ref());
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size();
+ }
+ int cached_size = ::_pbi::ToCachedSize(total_size);
+ SetCachedSize(cached_size);
+ return total_size;
+}
+
+void StackFrame::CheckTypeAndMergeFrom(
+ const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) {
+ MergeFrom(*::_pbi::DownCast<const StackFrame*>(
+ &from));
+}
+
+void StackFrame::MergeFrom(const StackFrame& from) {
+ StackFrame* const _this = this;
+ // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.StackFrame)
+ GOOGLE_DCHECK_NE(&from, _this);
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ switch (from.StackFrameType_case()) {
+ case kData: {
+ _this->_internal_mutable_data()->::mozilla::devtools::protobuf::StackFrame_Data::MergeFrom(
+ from._internal_data());
+ break;
+ }
+ case kRef: {
+ _this->_internal_set_ref(from._internal_ref());
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+}
+
+void StackFrame::CopyFrom(const StackFrame& from) {
+// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.StackFrame)
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool StackFrame::IsInitialized() const {
+ return true;
+}
+
+void StackFrame::InternalSwap(StackFrame* other) {
+ using std::swap;
+ _internal_metadata_.InternalSwap(&other->_internal_metadata_);
+ swap(_impl_.StackFrameType_, other->_impl_.StackFrameType_);
+ swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]);
+}
+
+std::string StackFrame::GetTypeName() const {
+ return "mozilla.devtools.protobuf.StackFrame";
+}
+
+
+// ===================================================================
+
+class Node::_Internal {
+ public:
+ using HasBits = decltype(std::declval<Node>()._impl_._has_bits_);
+ static void set_has_id(HasBits* has_bits) {
+ (*has_bits)[0] |= 2u;
+ }
+ static void set_has_size(HasBits* has_bits) {
+ (*has_bits)[0] |= 4u;
+ }
+ static const ::mozilla::devtools::protobuf::StackFrame& allocationstack(const Node* msg);
+ static void set_has_allocationstack(HasBits* has_bits) {
+ (*has_bits)[0] |= 1u;
+ }
+ static void set_has_coarsetype(HasBits* has_bits) {
+ (*has_bits)[0] |= 8u;
+ }
+};
+
+const ::mozilla::devtools::protobuf::StackFrame&
+Node::_Internal::allocationstack(const Node* msg) {
+ return *msg->_impl_.allocationstack_;
+}
+Node::Node(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) {
+ SharedCtor(arena, is_message_owned);
+ // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.Node)
+}
+Node::Node(const Node& from)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite() {
+ Node* const _this = this; (void)_this;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){from._impl_._has_bits_}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.edges_){from._impl_.edges_}
+ , decltype(_impl_.allocationstack_){nullptr}
+ , decltype(_impl_.id_){}
+ , decltype(_impl_.size_){}
+ , decltype(_impl_.coarsetype_){}
+ , decltype(_impl_.TypeNameOrRef_){}
+ , decltype(_impl_.JSObjectClassNameOrRef_){}
+ , decltype(_impl_.ScriptFilenameOrRef_){}
+ , decltype(_impl_.descriptiveTypeNameOrRef_){}
+ , /*decltype(_impl_._oneof_case_)*/{}};
+
+ _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+ if (from._internal_has_allocationstack()) {
+ _this->_impl_.allocationstack_ = new ::mozilla::devtools::protobuf::StackFrame(*from._impl_.allocationstack_);
+ }
+ ::memcpy(&_impl_.id_, &from._impl_.id_,
+ static_cast<size_t>(reinterpret_cast<char*>(&_impl_.coarsetype_) -
+ reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.coarsetype_));
+ clear_has_TypeNameOrRef();
+ switch (from.TypeNameOrRef_case()) {
+ case kTypeName: {
+ _this->_internal_set_typename_(from._internal_typename_());
+ break;
+ }
+ case kTypeNameRef: {
+ _this->_internal_set_typenameref(from._internal_typenameref());
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ clear_has_JSObjectClassNameOrRef();
+ switch (from.JSObjectClassNameOrRef_case()) {
+ case kJsObjectClassName: {
+ _this->_internal_set_jsobjectclassname(from._internal_jsobjectclassname());
+ break;
+ }
+ case kJsObjectClassNameRef: {
+ _this->_internal_set_jsobjectclassnameref(from._internal_jsobjectclassnameref());
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ clear_has_ScriptFilenameOrRef();
+ switch (from.ScriptFilenameOrRef_case()) {
+ case kScriptFilename: {
+ _this->_internal_set_scriptfilename(from._internal_scriptfilename());
+ break;
+ }
+ case kScriptFilenameRef: {
+ _this->_internal_set_scriptfilenameref(from._internal_scriptfilenameref());
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ clear_has_descriptiveTypeNameOrRef();
+ switch (from.descriptiveTypeNameOrRef_case()) {
+ case kDescriptiveTypeName: {
+ _this->_internal_set_descriptivetypename(from._internal_descriptivetypename());
+ break;
+ }
+ case kDescriptiveTypeNameRef: {
+ _this->_internal_set_descriptivetypenameref(from._internal_descriptivetypenameref());
+ break;
+ }
+ case DESCRIPTIVETYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Node)
+}
+
+inline void Node::SharedCtor(
+ ::_pb::Arena* arena, bool is_message_owned) {
+ (void)arena;
+ (void)is_message_owned;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.edges_){arena}
+ , decltype(_impl_.allocationstack_){nullptr}
+ , decltype(_impl_.id_){uint64_t{0u}}
+ , decltype(_impl_.size_){uint64_t{0u}}
+ , decltype(_impl_.coarsetype_){0u}
+ , decltype(_impl_.TypeNameOrRef_){}
+ , decltype(_impl_.JSObjectClassNameOrRef_){}
+ , decltype(_impl_.ScriptFilenameOrRef_){}
+ , decltype(_impl_.descriptiveTypeNameOrRef_){}
+ , /*decltype(_impl_._oneof_case_)*/{}
+ };
+ clear_has_TypeNameOrRef();
+ clear_has_JSObjectClassNameOrRef();
+ clear_has_ScriptFilenameOrRef();
+ clear_has_descriptiveTypeNameOrRef();
+}
+
+Node::~Node() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Node)
+ if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) {
+ (void)arena;
+ return;
+ }
+ SharedDtor();
+}
+
+inline void Node::SharedDtor() {
+ GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
+ _impl_.edges_.~RepeatedPtrField();
+ if (this != internal_default_instance()) delete _impl_.allocationstack_;
+ if (has_TypeNameOrRef()) {
+ clear_TypeNameOrRef();
+ }
+ if (has_JSObjectClassNameOrRef()) {
+ clear_JSObjectClassNameOrRef();
+ }
+ if (has_ScriptFilenameOrRef()) {
+ clear_ScriptFilenameOrRef();
+ }
+ if (has_descriptiveTypeNameOrRef()) {
+ clear_descriptiveTypeNameOrRef();
+ }
+}
+
+void Node::SetCachedSize(int size) const {
+ _impl_._cached_size_.Set(size);
+}
+
+void Node::clear_TypeNameOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node)
+ switch (TypeNameOrRef_case()) {
+ case kTypeName: {
+ _impl_.TypeNameOrRef_.typename__.Destroy();
+ break;
+ }
+ case kTypeNameRef: {
+ // No need to clear
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[0] = TYPENAMEORREF_NOT_SET;
+}
+
+void Node::clear_JSObjectClassNameOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node)
+ switch (JSObjectClassNameOrRef_case()) {
+ case kJsObjectClassName: {
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Destroy();
+ break;
+ }
+ case kJsObjectClassNameRef: {
+ // No need to clear
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET;
+}
+
+void Node::clear_ScriptFilenameOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node)
+ switch (ScriptFilenameOrRef_case()) {
+ case kScriptFilename: {
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.Destroy();
+ break;
+ }
+ case kScriptFilenameRef: {
+ // No need to clear
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET;
+}
+
+void Node::clear_descriptiveTypeNameOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node)
+ switch (descriptiveTypeNameOrRef_case()) {
+ case kDescriptiveTypeName: {
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Destroy();
+ break;
+ }
+ case kDescriptiveTypeNameRef: {
+ // No need to clear
+ break;
+ }
+ case DESCRIPTIVETYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[3] = DESCRIPTIVETYPENAMEORREF_NOT_SET;
+}
+
+
+void Node::Clear() {
+// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.Node)
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ _impl_.edges_.Clear();
+ cached_has_bits = _impl_._has_bits_[0];
+ if (cached_has_bits & 0x00000001u) {
+ GOOGLE_DCHECK(_impl_.allocationstack_ != nullptr);
+ _impl_.allocationstack_->Clear();
+ }
+ if (cached_has_bits & 0x0000000eu) {
+ ::memset(&_impl_.id_, 0, static_cast<size_t>(
+ reinterpret_cast<char*>(&_impl_.coarsetype_) -
+ reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.coarsetype_));
+ }
+ clear_TypeNameOrRef();
+ clear_JSObjectClassNameOrRef();
+ clear_ScriptFilenameOrRef();
+ clear_descriptiveTypeNameOrRef();
+ _impl_._has_bits_.Clear();
+ _internal_metadata_.Clear<std::string>();
+}
+
+const char* Node::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
+#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
+ _Internal::HasBits has_bits{};
+ while (!ctx->Done(&ptr)) {
+ uint32_t tag;
+ ptr = ::_pbi::ReadTag(ptr, &tag);
+ switch (tag >> 3) {
+ // optional uint64 id = 1;
+ case 1:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) {
+ _Internal::set_has_id(&has_bits);
+ _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes typeName = 2;
+ case 2:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) {
+ auto str = _internal_mutable_typename_();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 typeNameRef = 3;
+ case 3:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 24)) {
+ _internal_set_typenameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional uint64 size = 4;
+ case 4:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 32)) {
+ _Internal::set_has_size(&has_bits);
+ _impl_.size_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ case 5:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 42)) {
+ ptr -= 1;
+ do {
+ ptr += 1;
+ ptr = ctx->ParseMessage(_internal_add_edges(), ptr);
+ CHK_(ptr);
+ if (!ctx->DataAvailable(ptr)) break;
+ } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<42>(ptr));
+ } else
+ goto handle_unusual;
+ continue;
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ case 6:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 50)) {
+ ptr = ctx->ParseMessage(_internal_mutable_allocationstack(), ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes jsObjectClassName = 7;
+ case 7:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 58)) {
+ auto str = _internal_mutable_jsobjectclassname();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 jsObjectClassNameRef = 8;
+ case 8:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 64)) {
+ _internal_set_jsobjectclassnameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // optional uint32 coarseType = 9 [default = 0];
+ case 9:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 72)) {
+ _Internal::set_has_coarsetype(&has_bits);
+ _impl_.coarsetype_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes scriptFilename = 10;
+ case 10:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 82)) {
+ auto str = _internal_mutable_scriptfilename();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 scriptFilenameRef = 11;
+ case 11:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 88)) {
+ _internal_set_scriptfilenameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes descriptiveTypeName = 12;
+ case 12:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 98)) {
+ auto str = _internal_mutable_descriptivetypename();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 descriptiveTypeNameRef = 13;
+ case 13:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 104)) {
+ _internal_set_descriptivetypenameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ default:
+ goto handle_unusual;
+ } // switch
+ handle_unusual:
+ if ((tag == 0) || ((tag & 7) == 4)) {
+ CHK_(ptr);
+ ctx->SetLastTag(tag);
+ goto message_done;
+ }
+ ptr = UnknownFieldParse(
+ tag,
+ _internal_metadata_.mutable_unknown_fields<std::string>(),
+ ptr, ctx);
+ CHK_(ptr != nullptr);
+ } // while
+message_done:
+ _impl_._has_bits_.Or(has_bits);
+ return ptr;
+failure:
+ ptr = nullptr;
+ goto message_done;
+#undef CHK_
+}
+
+uint8_t* Node::_InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Node)
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ cached_has_bits = _impl_._has_bits_[0];
+ // optional uint64 id = 1;
+ if (cached_has_bits & 0x00000002u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_id(), target);
+ }
+
+ switch (TypeNameOrRef_case()) {
+ case kTypeName: {
+ target = stream->WriteBytesMaybeAliased(
+ 2, this->_internal_typename_(), target);
+ break;
+ }
+ case kTypeNameRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(3, this->_internal_typenameref(), target);
+ break;
+ }
+ default: ;
+ }
+ // optional uint64 size = 4;
+ if (cached_has_bits & 0x00000004u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(4, this->_internal_size(), target);
+ }
+
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ for (unsigned i = 0,
+ n = static_cast<unsigned>(this->_internal_edges_size()); i < n; i++) {
+ const auto& repfield = this->_internal_edges(i);
+ target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::
+ InternalWriteMessage(5, repfield, repfield.GetCachedSize(), target, stream);
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ if (cached_has_bits & 0x00000001u) {
+ target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::
+ InternalWriteMessage(6, _Internal::allocationstack(this),
+ _Internal::allocationstack(this).GetCachedSize(), target, stream);
+ }
+
+ switch (JSObjectClassNameOrRef_case()) {
+ case kJsObjectClassName: {
+ target = stream->WriteBytesMaybeAliased(
+ 7, this->_internal_jsobjectclassname(), target);
+ break;
+ }
+ case kJsObjectClassNameRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(8, this->_internal_jsobjectclassnameref(), target);
+ break;
+ }
+ default: ;
+ }
+ // optional uint32 coarseType = 9 [default = 0];
+ if (cached_has_bits & 0x00000008u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt32ToArray(9, this->_internal_coarsetype(), target);
+ }
+
+ switch (ScriptFilenameOrRef_case()) {
+ case kScriptFilename: {
+ target = stream->WriteBytesMaybeAliased(
+ 10, this->_internal_scriptfilename(), target);
+ break;
+ }
+ case kScriptFilenameRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(11, this->_internal_scriptfilenameref(), target);
+ break;
+ }
+ default: ;
+ }
+ switch (descriptiveTypeNameOrRef_case()) {
+ case kDescriptiveTypeName: {
+ target = stream->WriteBytesMaybeAliased(
+ 12, this->_internal_descriptivetypename(), target);
+ break;
+ }
+ case kDescriptiveTypeNameRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(13, this->_internal_descriptivetypenameref(), target);
+ break;
+ }
+ default: ;
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(),
+ static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Node)
+ return target;
+}
+
+size_t Node::ByteSizeLong() const {
+// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.Node)
+ size_t total_size = 0;
+
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ total_size += 1UL * this->_internal_edges_size();
+ for (const auto& msg : this->_impl_.edges_) {
+ total_size +=
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg);
+ }
+
+ cached_has_bits = _impl_._has_bits_[0];
+ if (cached_has_bits & 0x0000000fu) {
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ if (cached_has_bits & 0x00000001u) {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(
+ *_impl_.allocationstack_);
+ }
+
+ // optional uint64 id = 1;
+ if (cached_has_bits & 0x00000002u) {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id());
+ }
+
+ // optional uint64 size = 4;
+ if (cached_has_bits & 0x00000004u) {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_size());
+ }
+
+ // optional uint32 coarseType = 9 [default = 0];
+ if (cached_has_bits & 0x00000008u) {
+ total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_coarsetype());
+ }
+
+ }
+ switch (TypeNameOrRef_case()) {
+ // bytes typeName = 2;
+ case kTypeName: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_typename_());
+ break;
+ }
+ // uint64 typeNameRef = 3;
+ case kTypeNameRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_typenameref());
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (JSObjectClassNameOrRef_case()) {
+ // bytes jsObjectClassName = 7;
+ case kJsObjectClassName: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_jsobjectclassname());
+ break;
+ }
+ // uint64 jsObjectClassNameRef = 8;
+ case kJsObjectClassNameRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_jsobjectclassnameref());
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (ScriptFilenameOrRef_case()) {
+ // bytes scriptFilename = 10;
+ case kScriptFilename: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_scriptfilename());
+ break;
+ }
+ // uint64 scriptFilenameRef = 11;
+ case kScriptFilenameRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_scriptfilenameref());
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (descriptiveTypeNameOrRef_case()) {
+ // bytes descriptiveTypeName = 12;
+ case kDescriptiveTypeName: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_descriptivetypename());
+ break;
+ }
+ // uint64 descriptiveTypeNameRef = 13;
+ case kDescriptiveTypeNameRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_descriptivetypenameref());
+ break;
+ }
+ case DESCRIPTIVETYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size();
+ }
+ int cached_size = ::_pbi::ToCachedSize(total_size);
+ SetCachedSize(cached_size);
+ return total_size;
+}
+
+void Node::CheckTypeAndMergeFrom(
+ const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) {
+ MergeFrom(*::_pbi::DownCast<const Node*>(
+ &from));
+}
+
+void Node::MergeFrom(const Node& from) {
+ Node* const _this = this;
+ // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.Node)
+ GOOGLE_DCHECK_NE(&from, _this);
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ _this->_impl_.edges_.MergeFrom(from._impl_.edges_);
+ cached_has_bits = from._impl_._has_bits_[0];
+ if (cached_has_bits & 0x0000000fu) {
+ if (cached_has_bits & 0x00000001u) {
+ _this->_internal_mutable_allocationstack()->::mozilla::devtools::protobuf::StackFrame::MergeFrom(
+ from._internal_allocationstack());
+ }
+ if (cached_has_bits & 0x00000002u) {
+ _this->_impl_.id_ = from._impl_.id_;
+ }
+ if (cached_has_bits & 0x00000004u) {
+ _this->_impl_.size_ = from._impl_.size_;
+ }
+ if (cached_has_bits & 0x00000008u) {
+ _this->_impl_.coarsetype_ = from._impl_.coarsetype_;
+ }
+ _this->_impl_._has_bits_[0] |= cached_has_bits;
+ }
+ switch (from.TypeNameOrRef_case()) {
+ case kTypeName: {
+ _this->_internal_set_typename_(from._internal_typename_());
+ break;
+ }
+ case kTypeNameRef: {
+ _this->_internal_set_typenameref(from._internal_typenameref());
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.JSObjectClassNameOrRef_case()) {
+ case kJsObjectClassName: {
+ _this->_internal_set_jsobjectclassname(from._internal_jsobjectclassname());
+ break;
+ }
+ case kJsObjectClassNameRef: {
+ _this->_internal_set_jsobjectclassnameref(from._internal_jsobjectclassnameref());
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.ScriptFilenameOrRef_case()) {
+ case kScriptFilename: {
+ _this->_internal_set_scriptfilename(from._internal_scriptfilename());
+ break;
+ }
+ case kScriptFilenameRef: {
+ _this->_internal_set_scriptfilenameref(from._internal_scriptfilenameref());
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.descriptiveTypeNameOrRef_case()) {
+ case kDescriptiveTypeName: {
+ _this->_internal_set_descriptivetypename(from._internal_descriptivetypename());
+ break;
+ }
+ case kDescriptiveTypeNameRef: {
+ _this->_internal_set_descriptivetypenameref(from._internal_descriptivetypenameref());
+ break;
+ }
+ case DESCRIPTIVETYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+}
+
+void Node::CopyFrom(const Node& from) {
+// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.Node)
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Node::IsInitialized() const {
+ return true;
+}
+
+void Node::InternalSwap(Node* other) {
+ using std::swap;
+ _internal_metadata_.InternalSwap(&other->_internal_metadata_);
+ swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]);
+ _impl_.edges_.InternalSwap(&other->_impl_.edges_);
+ ::PROTOBUF_NAMESPACE_ID::internal::memswap<
+ PROTOBUF_FIELD_OFFSET(Node, _impl_.coarsetype_)
+ + sizeof(Node::_impl_.coarsetype_)
+ - PROTOBUF_FIELD_OFFSET(Node, _impl_.allocationstack_)>(
+ reinterpret_cast<char*>(&_impl_.allocationstack_),
+ reinterpret_cast<char*>(&other->_impl_.allocationstack_));
+ swap(_impl_.TypeNameOrRef_, other->_impl_.TypeNameOrRef_);
+ swap(_impl_.JSObjectClassNameOrRef_, other->_impl_.JSObjectClassNameOrRef_);
+ swap(_impl_.ScriptFilenameOrRef_, other->_impl_.ScriptFilenameOrRef_);
+ swap(_impl_.descriptiveTypeNameOrRef_, other->_impl_.descriptiveTypeNameOrRef_);
+ swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]);
+ swap(_impl_._oneof_case_[1], other->_impl_._oneof_case_[1]);
+ swap(_impl_._oneof_case_[2], other->_impl_._oneof_case_[2]);
+ swap(_impl_._oneof_case_[3], other->_impl_._oneof_case_[3]);
+}
+
+std::string Node::GetTypeName() const {
+ return "mozilla.devtools.protobuf.Node";
+}
+
+
+// ===================================================================
+
+class Edge::_Internal {
+ public:
+ using HasBits = decltype(std::declval<Edge>()._impl_._has_bits_);
+ static void set_has_referent(HasBits* has_bits) {
+ (*has_bits)[0] |= 1u;
+ }
+};
+
+Edge::Edge(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) {
+ SharedCtor(arena, is_message_owned);
+ // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.Edge)
+}
+Edge::Edge(const Edge& from)
+ : ::PROTOBUF_NAMESPACE_ID::MessageLite() {
+ Edge* const _this = this; (void)_this;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){from._impl_._has_bits_}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.referent_){}
+ , decltype(_impl_.EdgeNameOrRef_){}
+ , /*decltype(_impl_._oneof_case_)*/{}};
+
+ _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+ _this->_impl_.referent_ = from._impl_.referent_;
+ clear_has_EdgeNameOrRef();
+ switch (from.EdgeNameOrRef_case()) {
+ case kName: {
+ _this->_internal_set_name(from._internal_name());
+ break;
+ }
+ case kNameRef: {
+ _this->_internal_set_nameref(from._internal_nameref());
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Edge)
+}
+
+inline void Edge::SharedCtor(
+ ::_pb::Arena* arena, bool is_message_owned) {
+ (void)arena;
+ (void)is_message_owned;
+ new (&_impl_) Impl_{
+ decltype(_impl_._has_bits_){}
+ , /*decltype(_impl_._cached_size_)*/{}
+ , decltype(_impl_.referent_){uint64_t{0u}}
+ , decltype(_impl_.EdgeNameOrRef_){}
+ , /*decltype(_impl_._oneof_case_)*/{}
+ };
+ clear_has_EdgeNameOrRef();
+}
+
+Edge::~Edge() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Edge)
+ if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) {
+ (void)arena;
+ return;
+ }
+ SharedDtor();
+}
+
+inline void Edge::SharedDtor() {
+ GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
+ if (has_EdgeNameOrRef()) {
+ clear_EdgeNameOrRef();
+ }
+}
+
+void Edge::SetCachedSize(int size) const {
+ _impl_._cached_size_.Set(size);
+}
+
+void Edge::clear_EdgeNameOrRef() {
+// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Edge)
+ switch (EdgeNameOrRef_case()) {
+ case kName: {
+ _impl_.EdgeNameOrRef_.name_.Destroy();
+ break;
+ }
+ case kNameRef: {
+ // No need to clear
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _impl_._oneof_case_[0] = EDGENAMEORREF_NOT_SET;
+}
+
+
+void Edge::Clear() {
+// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.Edge)
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ _impl_.referent_ = uint64_t{0u};
+ clear_EdgeNameOrRef();
+ _impl_._has_bits_.Clear();
+ _internal_metadata_.Clear<std::string>();
+}
+
+const char* Edge::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
+#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
+ _Internal::HasBits has_bits{};
+ while (!ctx->Done(&ptr)) {
+ uint32_t tag;
+ ptr = ::_pbi::ReadTag(ptr, &tag);
+ switch (tag >> 3) {
+ // optional uint64 referent = 1;
+ case 1:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) {
+ _Internal::set_has_referent(&has_bits);
+ _impl_.referent_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // bytes name = 2;
+ case 2:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) {
+ auto str = _internal_mutable_name();
+ ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ // uint64 nameRef = 3;
+ case 3:
+ if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 24)) {
+ _internal_set_nameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr));
+ CHK_(ptr);
+ } else
+ goto handle_unusual;
+ continue;
+ default:
+ goto handle_unusual;
+ } // switch
+ handle_unusual:
+ if ((tag == 0) || ((tag & 7) == 4)) {
+ CHK_(ptr);
+ ctx->SetLastTag(tag);
+ goto message_done;
+ }
+ ptr = UnknownFieldParse(
+ tag,
+ _internal_metadata_.mutable_unknown_fields<std::string>(),
+ ptr, ctx);
+ CHK_(ptr != nullptr);
+ } // while
+message_done:
+ _impl_._has_bits_.Or(has_bits);
+ return ptr;
+failure:
+ ptr = nullptr;
+ goto message_done;
+#undef CHK_
+}
+
+uint8_t* Edge::_InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Edge)
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ cached_has_bits = _impl_._has_bits_[0];
+ // optional uint64 referent = 1;
+ if (cached_has_bits & 0x00000001u) {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_referent(), target);
+ }
+
+ switch (EdgeNameOrRef_case()) {
+ case kName: {
+ target = stream->WriteBytesMaybeAliased(
+ 2, this->_internal_name(), target);
+ break;
+ }
+ case kNameRef: {
+ target = stream->EnsureSpace(target);
+ target = ::_pbi::WireFormatLite::WriteUInt64ToArray(3, this->_internal_nameref(), target);
+ break;
+ }
+ default: ;
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(),
+ static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Edge)
+ return target;
+}
+
+size_t Edge::ByteSizeLong() const {
+// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.Edge)
+ size_t total_size = 0;
+
+ uint32_t cached_has_bits = 0;
+ // Prevent compiler warnings about cached_has_bits being unused
+ (void) cached_has_bits;
+
+ // optional uint64 referent = 1;
+ cached_has_bits = _impl_._has_bits_[0];
+ if (cached_has_bits & 0x00000001u) {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_referent());
+ }
+
+ switch (EdgeNameOrRef_case()) {
+ // bytes name = 2;
+ case kName: {
+ total_size += 1 +
+ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize(
+ this->_internal_name());
+ break;
+ }
+ // uint64 nameRef = 3;
+ case kNameRef: {
+ total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_nameref());
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
+ total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size();
+ }
+ int cached_size = ::_pbi::ToCachedSize(total_size);
+ SetCachedSize(cached_size);
+ return total_size;
+}
+
+void Edge::CheckTypeAndMergeFrom(
+ const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) {
+ MergeFrom(*::_pbi::DownCast<const Edge*>(
+ &from));
+}
+
+void Edge::MergeFrom(const Edge& from) {
+ Edge* const _this = this;
+ // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.Edge)
+ GOOGLE_DCHECK_NE(&from, _this);
+ uint32_t cached_has_bits = 0;
+ (void) cached_has_bits;
+
+ if (from._internal_has_referent()) {
+ _this->_internal_set_referent(from._internal_referent());
+ }
+ switch (from.EdgeNameOrRef_case()) {
+ case kName: {
+ _this->_internal_set_name(from._internal_name());
+ break;
+ }
+ case kNameRef: {
+ _this->_internal_set_nameref(from._internal_nameref());
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_);
+}
+
+void Edge::CopyFrom(const Edge& from) {
+// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.Edge)
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Edge::IsInitialized() const {
+ return true;
+}
+
+void Edge::InternalSwap(Edge* other) {
+ using std::swap;
+ _internal_metadata_.InternalSwap(&other->_internal_metadata_);
+ swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]);
+ swap(_impl_.referent_, other->_impl_.referent_);
+ swap(_impl_.EdgeNameOrRef_, other->_impl_.EdgeNameOrRef_);
+ swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]);
+}
+
+std::string Edge::GetTypeName() const {
+ return "mozilla.devtools.protobuf.Edge";
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+} // namespace protobuf
+} // namespace devtools
+} // namespace mozilla
+PROTOBUF_NAMESPACE_OPEN
+template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::Metadata*
+Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::Metadata >(Arena* arena) {
+ return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::Metadata >(arena);
+}
+template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::StackFrame_Data*
+Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::StackFrame_Data >(Arena* arena) {
+ return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::StackFrame_Data >(arena);
+}
+template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::StackFrame*
+Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::StackFrame >(Arena* arena) {
+ return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::StackFrame >(arena);
+}
+template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::Node*
+Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::Node >(Arena* arena) {
+ return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::Node >(arena);
+}
+template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::Edge*
+Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::Edge >(Arena* arena) {
+ return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::Edge >(arena);
+}
+PROTOBUF_NAMESPACE_CLOSE
+
+// @@protoc_insertion_point(global_scope)
+#include <google/protobuf/port_undef.inc>
diff --git a/devtools/shared/heapsnapshot/CoreDump.pb.h b/devtools/shared/heapsnapshot/CoreDump.pb.h
new file mode 100644
index 0000000000..734c38c017
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CoreDump.pb.h
@@ -0,0 +1,2883 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: CoreDump.proto
+
+#ifndef GOOGLE_PROTOBUF_INCLUDED_CoreDump_2eproto
+#define GOOGLE_PROTOBUF_INCLUDED_CoreDump_2eproto
+
+#include <limits>
+#include <string>
+
+#include <google/protobuf/port_def.inc>
+#if PROTOBUF_VERSION < 3021000
+#error This file was generated by a newer version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please update
+#error your headers.
+#endif
+#if 3021006 < PROTOBUF_MIN_PROTOC_VERSION
+#error This file was generated by an older version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please
+#error regenerate this file with a newer version of protoc.
+#endif
+
+#include <google/protobuf/port_undef.inc>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/arena.h>
+#include <google/protobuf/arenastring.h>
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/metadata_lite.h>
+#include <google/protobuf/message_lite.h>
+#include <google/protobuf/repeated_field.h> // IWYU pragma: export
+#include <google/protobuf/extension_set.h> // IWYU pragma: export
+// @@protoc_insertion_point(includes)
+#include <google/protobuf/port_def.inc>
+#define PROTOBUF_INTERNAL_EXPORT_CoreDump_2eproto
+PROTOBUF_NAMESPACE_OPEN
+namespace internal {
+class AnyMetadata;
+} // namespace internal
+PROTOBUF_NAMESPACE_CLOSE
+
+// Internal implementation detail -- do not use these members.
+struct TableStruct_CoreDump_2eproto {
+ static const uint32_t offsets[];
+};
+namespace mozilla {
+namespace devtools {
+namespace protobuf {
+class Edge;
+struct EdgeDefaultTypeInternal;
+extern EdgeDefaultTypeInternal _Edge_default_instance_;
+class Metadata;
+struct MetadataDefaultTypeInternal;
+extern MetadataDefaultTypeInternal _Metadata_default_instance_;
+class Node;
+struct NodeDefaultTypeInternal;
+extern NodeDefaultTypeInternal _Node_default_instance_;
+class StackFrame;
+struct StackFrameDefaultTypeInternal;
+extern StackFrameDefaultTypeInternal _StackFrame_default_instance_;
+class StackFrame_Data;
+struct StackFrame_DataDefaultTypeInternal;
+extern StackFrame_DataDefaultTypeInternal _StackFrame_Data_default_instance_;
+} // namespace protobuf
+} // namespace devtools
+} // namespace mozilla
+PROTOBUF_NAMESPACE_OPEN
+template<> ::mozilla::devtools::protobuf::Edge* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::Edge>(Arena*);
+template<> ::mozilla::devtools::protobuf::Metadata* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::Metadata>(Arena*);
+template<> ::mozilla::devtools::protobuf::Node* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::Node>(Arena*);
+template<> ::mozilla::devtools::protobuf::StackFrame* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame>(Arena*);
+template<> ::mozilla::devtools::protobuf::StackFrame_Data* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame_Data>(Arena*);
+PROTOBUF_NAMESPACE_CLOSE
+namespace mozilla {
+namespace devtools {
+namespace protobuf {
+
+// ===================================================================
+
+class Metadata final :
+ public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.Metadata) */ {
+ public:
+ inline Metadata() : Metadata(nullptr) {}
+ ~Metadata() override;
+ explicit PROTOBUF_CONSTEXPR Metadata(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
+
+ Metadata(const Metadata& from);
+ Metadata(Metadata&& from) noexcept
+ : Metadata() {
+ *this = ::std::move(from);
+ }
+
+ inline Metadata& operator=(const Metadata& from) {
+ CopyFrom(from);
+ return *this;
+ }
+ inline Metadata& operator=(Metadata&& from) noexcept {
+ if (this == &from) return *this;
+ if (GetOwningArena() == from.GetOwningArena()
+ #ifdef PROTOBUF_FORCE_COPY_IN_MOVE
+ && GetOwningArena() != nullptr
+ #endif // !PROTOBUF_FORCE_COPY_IN_MOVE
+ ) {
+ InternalSwap(&from);
+ } else {
+ CopyFrom(from);
+ }
+ return *this;
+ }
+
+ inline const std::string& unknown_fields() const {
+ return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString);
+ }
+ inline std::string* mutable_unknown_fields() {
+ return _internal_metadata_.mutable_unknown_fields<std::string>();
+ }
+
+ static const Metadata& default_instance() {
+ return *internal_default_instance();
+ }
+ static inline const Metadata* internal_default_instance() {
+ return reinterpret_cast<const Metadata*>(
+ &_Metadata_default_instance_);
+ }
+ static constexpr int kIndexInFileMessages =
+ 0;
+
+ friend void swap(Metadata& a, Metadata& b) {
+ a.Swap(&b);
+ }
+ inline void Swap(Metadata* other) {
+ if (other == this) return;
+ #ifdef PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() != nullptr &&
+ GetOwningArena() == other->GetOwningArena()) {
+ #else // PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() == other->GetOwningArena()) {
+ #endif // !PROTOBUF_FORCE_COPY_IN_SWAP
+ InternalSwap(other);
+ } else {
+ ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
+ }
+ }
+ void UnsafeArenaSwap(Metadata* other) {
+ if (other == this) return;
+ GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
+ InternalSwap(other);
+ }
+
+ // implements Message ----------------------------------------------
+
+ Metadata* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
+ return CreateMaybeMessage<Metadata>(arena);
+ }
+ void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final;
+ void CopyFrom(const Metadata& from);
+ void MergeFrom(const Metadata& from);
+ PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
+ bool IsInitialized() const final;
+
+ size_t ByteSizeLong() const final;
+ const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
+ uint8_t* _InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
+ int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
+
+ private:
+ void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ void InternalSwap(Metadata* other);
+
+ private:
+ friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
+ static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
+ return "mozilla.devtools.protobuf.Metadata";
+ }
+ protected:
+ explicit Metadata(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned = false);
+ public:
+
+ std::string GetTypeName() const final;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ enum : int {
+ kTimeStampFieldNumber = 1,
+ };
+ // optional uint64 timeStamp = 1;
+ bool has_timestamp() const;
+ private:
+ bool _internal_has_timestamp() const;
+ public:
+ void clear_timestamp();
+ uint64_t timestamp() const;
+ void set_timestamp(uint64_t value);
+ private:
+ uint64_t _internal_timestamp() const;
+ void _internal_set_timestamp(uint64_t value);
+ public:
+
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Metadata)
+ private:
+ class _Internal;
+
+ template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
+ typedef void InternalArenaConstructable_;
+ typedef void DestructorSkippable_;
+ struct Impl_ {
+ ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_;
+ mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
+ uint64_t timestamp_;
+ };
+ union { Impl_ _impl_; };
+ friend struct ::TableStruct_CoreDump_2eproto;
+};
+// -------------------------------------------------------------------
+
+class StackFrame_Data final :
+ public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.StackFrame.Data) */ {
+ public:
+ inline StackFrame_Data() : StackFrame_Data(nullptr) {}
+ ~StackFrame_Data() override;
+ explicit PROTOBUF_CONSTEXPR StackFrame_Data(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
+
+ StackFrame_Data(const StackFrame_Data& from);
+ StackFrame_Data(StackFrame_Data&& from) noexcept
+ : StackFrame_Data() {
+ *this = ::std::move(from);
+ }
+
+ inline StackFrame_Data& operator=(const StackFrame_Data& from) {
+ CopyFrom(from);
+ return *this;
+ }
+ inline StackFrame_Data& operator=(StackFrame_Data&& from) noexcept {
+ if (this == &from) return *this;
+ if (GetOwningArena() == from.GetOwningArena()
+ #ifdef PROTOBUF_FORCE_COPY_IN_MOVE
+ && GetOwningArena() != nullptr
+ #endif // !PROTOBUF_FORCE_COPY_IN_MOVE
+ ) {
+ InternalSwap(&from);
+ } else {
+ CopyFrom(from);
+ }
+ return *this;
+ }
+
+ inline const std::string& unknown_fields() const {
+ return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString);
+ }
+ inline std::string* mutable_unknown_fields() {
+ return _internal_metadata_.mutable_unknown_fields<std::string>();
+ }
+
+ static const StackFrame_Data& default_instance() {
+ return *internal_default_instance();
+ }
+ enum SourceOrRefCase {
+ kSource = 5,
+ kSourceRef = 6,
+ SOURCEORREF_NOT_SET = 0,
+ };
+
+ enum FunctionDisplayNameOrRefCase {
+ kFunctionDisplayName = 7,
+ kFunctionDisplayNameRef = 8,
+ FUNCTIONDISPLAYNAMEORREF_NOT_SET = 0,
+ };
+
+ static inline const StackFrame_Data* internal_default_instance() {
+ return reinterpret_cast<const StackFrame_Data*>(
+ &_StackFrame_Data_default_instance_);
+ }
+ static constexpr int kIndexInFileMessages =
+ 1;
+
+ friend void swap(StackFrame_Data& a, StackFrame_Data& b) {
+ a.Swap(&b);
+ }
+ inline void Swap(StackFrame_Data* other) {
+ if (other == this) return;
+ #ifdef PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() != nullptr &&
+ GetOwningArena() == other->GetOwningArena()) {
+ #else // PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() == other->GetOwningArena()) {
+ #endif // !PROTOBUF_FORCE_COPY_IN_SWAP
+ InternalSwap(other);
+ } else {
+ ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
+ }
+ }
+ void UnsafeArenaSwap(StackFrame_Data* other) {
+ if (other == this) return;
+ GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
+ InternalSwap(other);
+ }
+
+ // implements Message ----------------------------------------------
+
+ StackFrame_Data* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
+ return CreateMaybeMessage<StackFrame_Data>(arena);
+ }
+ void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final;
+ void CopyFrom(const StackFrame_Data& from);
+ void MergeFrom(const StackFrame_Data& from);
+ PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
+ bool IsInitialized() const final;
+
+ size_t ByteSizeLong() const final;
+ const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
+ uint8_t* _InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
+ int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
+
+ private:
+ void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ void InternalSwap(StackFrame_Data* other);
+
+ private:
+ friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
+ static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
+ return "mozilla.devtools.protobuf.StackFrame.Data";
+ }
+ protected:
+ explicit StackFrame_Data(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned = false);
+ public:
+
+ std::string GetTypeName() const final;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ enum : int {
+ kParentFieldNumber = 2,
+ kIdFieldNumber = 1,
+ kLineFieldNumber = 3,
+ kColumnFieldNumber = 4,
+ kIsSystemFieldNumber = 9,
+ kIsSelfHostedFieldNumber = 10,
+ kSourceFieldNumber = 5,
+ kSourceRefFieldNumber = 6,
+ kFunctionDisplayNameFieldNumber = 7,
+ kFunctionDisplayNameRefFieldNumber = 8,
+ };
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ bool has_parent() const;
+ private:
+ bool _internal_has_parent() const;
+ public:
+ void clear_parent();
+ const ::mozilla::devtools::protobuf::StackFrame& parent() const;
+ PROTOBUF_NODISCARD ::mozilla::devtools::protobuf::StackFrame* release_parent();
+ ::mozilla::devtools::protobuf::StackFrame* mutable_parent();
+ void set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent);
+ private:
+ const ::mozilla::devtools::protobuf::StackFrame& _internal_parent() const;
+ ::mozilla::devtools::protobuf::StackFrame* _internal_mutable_parent();
+ public:
+ void unsafe_arena_set_allocated_parent(
+ ::mozilla::devtools::protobuf::StackFrame* parent);
+ ::mozilla::devtools::protobuf::StackFrame* unsafe_arena_release_parent();
+
+ // optional uint64 id = 1;
+ bool has_id() const;
+ private:
+ bool _internal_has_id() const;
+ public:
+ void clear_id();
+ uint64_t id() const;
+ void set_id(uint64_t value);
+ private:
+ uint64_t _internal_id() const;
+ void _internal_set_id(uint64_t value);
+ public:
+
+ // optional uint32 line = 3;
+ bool has_line() const;
+ private:
+ bool _internal_has_line() const;
+ public:
+ void clear_line();
+ uint32_t line() const;
+ void set_line(uint32_t value);
+ private:
+ uint32_t _internal_line() const;
+ void _internal_set_line(uint32_t value);
+ public:
+
+ // optional uint32 column = 4;
+ bool has_column() const;
+ private:
+ bool _internal_has_column() const;
+ public:
+ void clear_column();
+ uint32_t column() const;
+ void set_column(uint32_t value);
+ private:
+ uint32_t _internal_column() const;
+ void _internal_set_column(uint32_t value);
+ public:
+
+ // optional bool isSystem = 9;
+ bool has_issystem() const;
+ private:
+ bool _internal_has_issystem() const;
+ public:
+ void clear_issystem();
+ bool issystem() const;
+ void set_issystem(bool value);
+ private:
+ bool _internal_issystem() const;
+ void _internal_set_issystem(bool value);
+ public:
+
+ // optional bool isSelfHosted = 10;
+ bool has_isselfhosted() const;
+ private:
+ bool _internal_has_isselfhosted() const;
+ public:
+ void clear_isselfhosted();
+ bool isselfhosted() const;
+ void set_isselfhosted(bool value);
+ private:
+ bool _internal_isselfhosted() const;
+ void _internal_set_isselfhosted(bool value);
+ public:
+
+ // bytes source = 5;
+ bool has_source() const;
+ private:
+ bool _internal_has_source() const;
+ public:
+ void clear_source();
+ const std::string& source() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_source(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_source();
+ PROTOBUF_NODISCARD std::string* release_source();
+ void set_allocated_source(std::string* source);
+ private:
+ const std::string& _internal_source() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_source(const std::string& value);
+ std::string* _internal_mutable_source();
+ public:
+
+ // uint64 sourceRef = 6;
+ bool has_sourceref() const;
+ private:
+ bool _internal_has_sourceref() const;
+ public:
+ void clear_sourceref();
+ uint64_t sourceref() const;
+ void set_sourceref(uint64_t value);
+ private:
+ uint64_t _internal_sourceref() const;
+ void _internal_set_sourceref(uint64_t value);
+ public:
+
+ // bytes functionDisplayName = 7;
+ bool has_functiondisplayname() const;
+ private:
+ bool _internal_has_functiondisplayname() const;
+ public:
+ void clear_functiondisplayname();
+ const std::string& functiondisplayname() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_functiondisplayname(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_functiondisplayname();
+ PROTOBUF_NODISCARD std::string* release_functiondisplayname();
+ void set_allocated_functiondisplayname(std::string* functiondisplayname);
+ private:
+ const std::string& _internal_functiondisplayname() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_functiondisplayname(const std::string& value);
+ std::string* _internal_mutable_functiondisplayname();
+ public:
+
+ // uint64 functionDisplayNameRef = 8;
+ bool has_functiondisplaynameref() const;
+ private:
+ bool _internal_has_functiondisplaynameref() const;
+ public:
+ void clear_functiondisplaynameref();
+ uint64_t functiondisplaynameref() const;
+ void set_functiondisplaynameref(uint64_t value);
+ private:
+ uint64_t _internal_functiondisplaynameref() const;
+ void _internal_set_functiondisplaynameref(uint64_t value);
+ public:
+
+ void clear_SourceOrRef();
+ SourceOrRefCase SourceOrRef_case() const;
+ void clear_FunctionDisplayNameOrRef();
+ FunctionDisplayNameOrRefCase FunctionDisplayNameOrRef_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame.Data)
+ private:
+ class _Internal;
+ void set_has_source();
+ void set_has_sourceref();
+ void set_has_functiondisplayname();
+ void set_has_functiondisplaynameref();
+
+ inline bool has_SourceOrRef() const;
+ inline void clear_has_SourceOrRef();
+
+ inline bool has_FunctionDisplayNameOrRef() const;
+ inline void clear_has_FunctionDisplayNameOrRef();
+
+ template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
+ typedef void InternalArenaConstructable_;
+ typedef void DestructorSkippable_;
+ struct Impl_ {
+ ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_;
+ mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
+ ::mozilla::devtools::protobuf::StackFrame* parent_;
+ uint64_t id_;
+ uint32_t line_;
+ uint32_t column_;
+ bool issystem_;
+ bool isselfhosted_;
+ union SourceOrRefUnion {
+ constexpr SourceOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr source_;
+ uint64_t sourceref_;
+ } SourceOrRef_;
+ union FunctionDisplayNameOrRefUnion {
+ constexpr FunctionDisplayNameOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr functiondisplayname_;
+ uint64_t functiondisplaynameref_;
+ } FunctionDisplayNameOrRef_;
+ uint32_t _oneof_case_[2];
+
+ };
+ union { Impl_ _impl_; };
+ friend struct ::TableStruct_CoreDump_2eproto;
+};
+// -------------------------------------------------------------------
+
+class StackFrame final :
+ public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.StackFrame) */ {
+ public:
+ inline StackFrame() : StackFrame(nullptr) {}
+ ~StackFrame() override;
+ explicit PROTOBUF_CONSTEXPR StackFrame(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
+
+ StackFrame(const StackFrame& from);
+ StackFrame(StackFrame&& from) noexcept
+ : StackFrame() {
+ *this = ::std::move(from);
+ }
+
+ inline StackFrame& operator=(const StackFrame& from) {
+ CopyFrom(from);
+ return *this;
+ }
+ inline StackFrame& operator=(StackFrame&& from) noexcept {
+ if (this == &from) return *this;
+ if (GetOwningArena() == from.GetOwningArena()
+ #ifdef PROTOBUF_FORCE_COPY_IN_MOVE
+ && GetOwningArena() != nullptr
+ #endif // !PROTOBUF_FORCE_COPY_IN_MOVE
+ ) {
+ InternalSwap(&from);
+ } else {
+ CopyFrom(from);
+ }
+ return *this;
+ }
+
+ inline const std::string& unknown_fields() const {
+ return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString);
+ }
+ inline std::string* mutable_unknown_fields() {
+ return _internal_metadata_.mutable_unknown_fields<std::string>();
+ }
+
+ static const StackFrame& default_instance() {
+ return *internal_default_instance();
+ }
+ enum StackFrameTypeCase {
+ kData = 1,
+ kRef = 2,
+ STACKFRAMETYPE_NOT_SET = 0,
+ };
+
+ static inline const StackFrame* internal_default_instance() {
+ return reinterpret_cast<const StackFrame*>(
+ &_StackFrame_default_instance_);
+ }
+ static constexpr int kIndexInFileMessages =
+ 2;
+
+ friend void swap(StackFrame& a, StackFrame& b) {
+ a.Swap(&b);
+ }
+ inline void Swap(StackFrame* other) {
+ if (other == this) return;
+ #ifdef PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() != nullptr &&
+ GetOwningArena() == other->GetOwningArena()) {
+ #else // PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() == other->GetOwningArena()) {
+ #endif // !PROTOBUF_FORCE_COPY_IN_SWAP
+ InternalSwap(other);
+ } else {
+ ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
+ }
+ }
+ void UnsafeArenaSwap(StackFrame* other) {
+ if (other == this) return;
+ GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
+ InternalSwap(other);
+ }
+
+ // implements Message ----------------------------------------------
+
+ StackFrame* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
+ return CreateMaybeMessage<StackFrame>(arena);
+ }
+ void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final;
+ void CopyFrom(const StackFrame& from);
+ void MergeFrom(const StackFrame& from);
+ PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
+ bool IsInitialized() const final;
+
+ size_t ByteSizeLong() const final;
+ const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
+ uint8_t* _InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
+ int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
+
+ private:
+ void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ void InternalSwap(StackFrame* other);
+
+ private:
+ friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
+ static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
+ return "mozilla.devtools.protobuf.StackFrame";
+ }
+ protected:
+ explicit StackFrame(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned = false);
+ public:
+
+ std::string GetTypeName() const final;
+
+ // nested types ----------------------------------------------------
+
+ typedef StackFrame_Data Data;
+
+ // accessors -------------------------------------------------------
+
+ enum : int {
+ kDataFieldNumber = 1,
+ kRefFieldNumber = 2,
+ };
+ // .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ bool has_data() const;
+ private:
+ bool _internal_has_data() const;
+ public:
+ void clear_data();
+ const ::mozilla::devtools::protobuf::StackFrame_Data& data() const;
+ PROTOBUF_NODISCARD ::mozilla::devtools::protobuf::StackFrame_Data* release_data();
+ ::mozilla::devtools::protobuf::StackFrame_Data* mutable_data();
+ void set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data);
+ private:
+ const ::mozilla::devtools::protobuf::StackFrame_Data& _internal_data() const;
+ ::mozilla::devtools::protobuf::StackFrame_Data* _internal_mutable_data();
+ public:
+ void unsafe_arena_set_allocated_data(
+ ::mozilla::devtools::protobuf::StackFrame_Data* data);
+ ::mozilla::devtools::protobuf::StackFrame_Data* unsafe_arena_release_data();
+
+ // uint64 ref = 2;
+ bool has_ref() const;
+ private:
+ bool _internal_has_ref() const;
+ public:
+ void clear_ref();
+ uint64_t ref() const;
+ void set_ref(uint64_t value);
+ private:
+ uint64_t _internal_ref() const;
+ void _internal_set_ref(uint64_t value);
+ public:
+
+ void clear_StackFrameType();
+ StackFrameTypeCase StackFrameType_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame)
+ private:
+ class _Internal;
+ void set_has_data();
+ void set_has_ref();
+
+ inline bool has_StackFrameType() const;
+ inline void clear_has_StackFrameType();
+
+ template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
+ typedef void InternalArenaConstructable_;
+ typedef void DestructorSkippable_;
+ struct Impl_ {
+ union StackFrameTypeUnion {
+ constexpr StackFrameTypeUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::mozilla::devtools::protobuf::StackFrame_Data* data_;
+ uint64_t ref_;
+ } StackFrameType_;
+ mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
+ uint32_t _oneof_case_[1];
+
+ };
+ union { Impl_ _impl_; };
+ friend struct ::TableStruct_CoreDump_2eproto;
+};
+// -------------------------------------------------------------------
+
+class Node final :
+ public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.Node) */ {
+ public:
+ inline Node() : Node(nullptr) {}
+ ~Node() override;
+ explicit PROTOBUF_CONSTEXPR Node(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
+
+ Node(const Node& from);
+ Node(Node&& from) noexcept
+ : Node() {
+ *this = ::std::move(from);
+ }
+
+ inline Node& operator=(const Node& from) {
+ CopyFrom(from);
+ return *this;
+ }
+ inline Node& operator=(Node&& from) noexcept {
+ if (this == &from) return *this;
+ if (GetOwningArena() == from.GetOwningArena()
+ #ifdef PROTOBUF_FORCE_COPY_IN_MOVE
+ && GetOwningArena() != nullptr
+ #endif // !PROTOBUF_FORCE_COPY_IN_MOVE
+ ) {
+ InternalSwap(&from);
+ } else {
+ CopyFrom(from);
+ }
+ return *this;
+ }
+
+ inline const std::string& unknown_fields() const {
+ return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString);
+ }
+ inline std::string* mutable_unknown_fields() {
+ return _internal_metadata_.mutable_unknown_fields<std::string>();
+ }
+
+ static const Node& default_instance() {
+ return *internal_default_instance();
+ }
+ enum TypeNameOrRefCase {
+ kTypeName = 2,
+ kTypeNameRef = 3,
+ TYPENAMEORREF_NOT_SET = 0,
+ };
+
+ enum JSObjectClassNameOrRefCase {
+ kJsObjectClassName = 7,
+ kJsObjectClassNameRef = 8,
+ JSOBJECTCLASSNAMEORREF_NOT_SET = 0,
+ };
+
+ enum ScriptFilenameOrRefCase {
+ kScriptFilename = 10,
+ kScriptFilenameRef = 11,
+ SCRIPTFILENAMEORREF_NOT_SET = 0,
+ };
+
+ enum DescriptiveTypeNameOrRefCase {
+ kDescriptiveTypeName = 12,
+ kDescriptiveTypeNameRef = 13,
+ DESCRIPTIVETYPENAMEORREF_NOT_SET = 0,
+ };
+
+ static inline const Node* internal_default_instance() {
+ return reinterpret_cast<const Node*>(
+ &_Node_default_instance_);
+ }
+ static constexpr int kIndexInFileMessages =
+ 3;
+
+ friend void swap(Node& a, Node& b) {
+ a.Swap(&b);
+ }
+ inline void Swap(Node* other) {
+ if (other == this) return;
+ #ifdef PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() != nullptr &&
+ GetOwningArena() == other->GetOwningArena()) {
+ #else // PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() == other->GetOwningArena()) {
+ #endif // !PROTOBUF_FORCE_COPY_IN_SWAP
+ InternalSwap(other);
+ } else {
+ ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
+ }
+ }
+ void UnsafeArenaSwap(Node* other) {
+ if (other == this) return;
+ GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
+ InternalSwap(other);
+ }
+
+ // implements Message ----------------------------------------------
+
+ Node* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
+ return CreateMaybeMessage<Node>(arena);
+ }
+ void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final;
+ void CopyFrom(const Node& from);
+ void MergeFrom(const Node& from);
+ PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
+ bool IsInitialized() const final;
+
+ size_t ByteSizeLong() const final;
+ const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
+ uint8_t* _InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
+ int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
+
+ private:
+ void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ void InternalSwap(Node* other);
+
+ private:
+ friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
+ static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
+ return "mozilla.devtools.protobuf.Node";
+ }
+ protected:
+ explicit Node(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned = false);
+ public:
+
+ std::string GetTypeName() const final;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ enum : int {
+ kEdgesFieldNumber = 5,
+ kAllocationStackFieldNumber = 6,
+ kIdFieldNumber = 1,
+ kSizeFieldNumber = 4,
+ kCoarseTypeFieldNumber = 9,
+ kTypeNameFieldNumber = 2,
+ kTypeNameRefFieldNumber = 3,
+ kJsObjectClassNameFieldNumber = 7,
+ kJsObjectClassNameRefFieldNumber = 8,
+ kScriptFilenameFieldNumber = 10,
+ kScriptFilenameRefFieldNumber = 11,
+ kDescriptiveTypeNameFieldNumber = 12,
+ kDescriptiveTypeNameRefFieldNumber = 13,
+ };
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ int edges_size() const;
+ private:
+ int _internal_edges_size() const;
+ public:
+ void clear_edges();
+ ::mozilla::devtools::protobuf::Edge* mutable_edges(int index);
+ ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >*
+ mutable_edges();
+ private:
+ const ::mozilla::devtools::protobuf::Edge& _internal_edges(int index) const;
+ ::mozilla::devtools::protobuf::Edge* _internal_add_edges();
+ public:
+ const ::mozilla::devtools::protobuf::Edge& edges(int index) const;
+ ::mozilla::devtools::protobuf::Edge* add_edges();
+ const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >&
+ edges() const;
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ bool has_allocationstack() const;
+ private:
+ bool _internal_has_allocationstack() const;
+ public:
+ void clear_allocationstack();
+ const ::mozilla::devtools::protobuf::StackFrame& allocationstack() const;
+ PROTOBUF_NODISCARD ::mozilla::devtools::protobuf::StackFrame* release_allocationstack();
+ ::mozilla::devtools::protobuf::StackFrame* mutable_allocationstack();
+ void set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack);
+ private:
+ const ::mozilla::devtools::protobuf::StackFrame& _internal_allocationstack() const;
+ ::mozilla::devtools::protobuf::StackFrame* _internal_mutable_allocationstack();
+ public:
+ void unsafe_arena_set_allocated_allocationstack(
+ ::mozilla::devtools::protobuf::StackFrame* allocationstack);
+ ::mozilla::devtools::protobuf::StackFrame* unsafe_arena_release_allocationstack();
+
+ // optional uint64 id = 1;
+ bool has_id() const;
+ private:
+ bool _internal_has_id() const;
+ public:
+ void clear_id();
+ uint64_t id() const;
+ void set_id(uint64_t value);
+ private:
+ uint64_t _internal_id() const;
+ void _internal_set_id(uint64_t value);
+ public:
+
+ // optional uint64 size = 4;
+ bool has_size() const;
+ private:
+ bool _internal_has_size() const;
+ public:
+ void clear_size();
+ uint64_t size() const;
+ void set_size(uint64_t value);
+ private:
+ uint64_t _internal_size() const;
+ void _internal_set_size(uint64_t value);
+ public:
+
+ // optional uint32 coarseType = 9 [default = 0];
+ bool has_coarsetype() const;
+ private:
+ bool _internal_has_coarsetype() const;
+ public:
+ void clear_coarsetype();
+ uint32_t coarsetype() const;
+ void set_coarsetype(uint32_t value);
+ private:
+ uint32_t _internal_coarsetype() const;
+ void _internal_set_coarsetype(uint32_t value);
+ public:
+
+ // bytes typeName = 2;
+ bool has_typename_() const;
+ private:
+ bool _internal_has_typename_() const;
+ public:
+ void clear_typename_();
+ const std::string& typename_() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_typename_(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_typename_();
+ PROTOBUF_NODISCARD std::string* release_typename_();
+ void set_allocated_typename_(std::string* typename_);
+ private:
+ const std::string& _internal_typename_() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_typename_(const std::string& value);
+ std::string* _internal_mutable_typename_();
+ public:
+
+ // uint64 typeNameRef = 3;
+ bool has_typenameref() const;
+ private:
+ bool _internal_has_typenameref() const;
+ public:
+ void clear_typenameref();
+ uint64_t typenameref() const;
+ void set_typenameref(uint64_t value);
+ private:
+ uint64_t _internal_typenameref() const;
+ void _internal_set_typenameref(uint64_t value);
+ public:
+
+ // bytes jsObjectClassName = 7;
+ bool has_jsobjectclassname() const;
+ private:
+ bool _internal_has_jsobjectclassname() const;
+ public:
+ void clear_jsobjectclassname();
+ const std::string& jsobjectclassname() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_jsobjectclassname(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_jsobjectclassname();
+ PROTOBUF_NODISCARD std::string* release_jsobjectclassname();
+ void set_allocated_jsobjectclassname(std::string* jsobjectclassname);
+ private:
+ const std::string& _internal_jsobjectclassname() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_jsobjectclassname(const std::string& value);
+ std::string* _internal_mutable_jsobjectclassname();
+ public:
+
+ // uint64 jsObjectClassNameRef = 8;
+ bool has_jsobjectclassnameref() const;
+ private:
+ bool _internal_has_jsobjectclassnameref() const;
+ public:
+ void clear_jsobjectclassnameref();
+ uint64_t jsobjectclassnameref() const;
+ void set_jsobjectclassnameref(uint64_t value);
+ private:
+ uint64_t _internal_jsobjectclassnameref() const;
+ void _internal_set_jsobjectclassnameref(uint64_t value);
+ public:
+
+ // bytes scriptFilename = 10;
+ bool has_scriptfilename() const;
+ private:
+ bool _internal_has_scriptfilename() const;
+ public:
+ void clear_scriptfilename();
+ const std::string& scriptfilename() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_scriptfilename(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_scriptfilename();
+ PROTOBUF_NODISCARD std::string* release_scriptfilename();
+ void set_allocated_scriptfilename(std::string* scriptfilename);
+ private:
+ const std::string& _internal_scriptfilename() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_scriptfilename(const std::string& value);
+ std::string* _internal_mutable_scriptfilename();
+ public:
+
+ // uint64 scriptFilenameRef = 11;
+ bool has_scriptfilenameref() const;
+ private:
+ bool _internal_has_scriptfilenameref() const;
+ public:
+ void clear_scriptfilenameref();
+ uint64_t scriptfilenameref() const;
+ void set_scriptfilenameref(uint64_t value);
+ private:
+ uint64_t _internal_scriptfilenameref() const;
+ void _internal_set_scriptfilenameref(uint64_t value);
+ public:
+
+ // bytes descriptiveTypeName = 12;
+ bool has_descriptivetypename() const;
+ private:
+ bool _internal_has_descriptivetypename() const;
+ public:
+ void clear_descriptivetypename();
+ const std::string& descriptivetypename() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_descriptivetypename(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_descriptivetypename();
+ PROTOBUF_NODISCARD std::string* release_descriptivetypename();
+ void set_allocated_descriptivetypename(std::string* descriptivetypename);
+ private:
+ const std::string& _internal_descriptivetypename() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_descriptivetypename(const std::string& value);
+ std::string* _internal_mutable_descriptivetypename();
+ public:
+
+ // uint64 descriptiveTypeNameRef = 13;
+ bool has_descriptivetypenameref() const;
+ private:
+ bool _internal_has_descriptivetypenameref() const;
+ public:
+ void clear_descriptivetypenameref();
+ uint64_t descriptivetypenameref() const;
+ void set_descriptivetypenameref(uint64_t value);
+ private:
+ uint64_t _internal_descriptivetypenameref() const;
+ void _internal_set_descriptivetypenameref(uint64_t value);
+ public:
+
+ void clear_TypeNameOrRef();
+ TypeNameOrRefCase TypeNameOrRef_case() const;
+ void clear_JSObjectClassNameOrRef();
+ JSObjectClassNameOrRefCase JSObjectClassNameOrRef_case() const;
+ void clear_ScriptFilenameOrRef();
+ ScriptFilenameOrRefCase ScriptFilenameOrRef_case() const;
+ void clear_descriptiveTypeNameOrRef();
+ DescriptiveTypeNameOrRefCase descriptiveTypeNameOrRef_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Node)
+ private:
+ class _Internal;
+ void set_has_typename_();
+ void set_has_typenameref();
+ void set_has_jsobjectclassname();
+ void set_has_jsobjectclassnameref();
+ void set_has_scriptfilename();
+ void set_has_scriptfilenameref();
+ void set_has_descriptivetypename();
+ void set_has_descriptivetypenameref();
+
+ inline bool has_TypeNameOrRef() const;
+ inline void clear_has_TypeNameOrRef();
+
+ inline bool has_JSObjectClassNameOrRef() const;
+ inline void clear_has_JSObjectClassNameOrRef();
+
+ inline bool has_ScriptFilenameOrRef() const;
+ inline void clear_has_ScriptFilenameOrRef();
+
+ inline bool has_descriptiveTypeNameOrRef() const;
+ inline void clear_has_descriptiveTypeNameOrRef();
+
+ template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
+ typedef void InternalArenaConstructable_;
+ typedef void DestructorSkippable_;
+ struct Impl_ {
+ ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_;
+ mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
+ ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge > edges_;
+ ::mozilla::devtools::protobuf::StackFrame* allocationstack_;
+ uint64_t id_;
+ uint64_t size_;
+ uint32_t coarsetype_;
+ union TypeNameOrRefUnion {
+ constexpr TypeNameOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr typename__;
+ uint64_t typenameref_;
+ } TypeNameOrRef_;
+ union JSObjectClassNameOrRefUnion {
+ constexpr JSObjectClassNameOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr jsobjectclassname_;
+ uint64_t jsobjectclassnameref_;
+ } JSObjectClassNameOrRef_;
+ union ScriptFilenameOrRefUnion {
+ constexpr ScriptFilenameOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr scriptfilename_;
+ uint64_t scriptfilenameref_;
+ } ScriptFilenameOrRef_;
+ union DescriptiveTypeNameOrRefUnion {
+ constexpr DescriptiveTypeNameOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr descriptivetypename_;
+ uint64_t descriptivetypenameref_;
+ } descriptiveTypeNameOrRef_;
+ uint32_t _oneof_case_[4];
+
+ };
+ union { Impl_ _impl_; };
+ friend struct ::TableStruct_CoreDump_2eproto;
+};
+// -------------------------------------------------------------------
+
+class Edge final :
+ public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.Edge) */ {
+ public:
+ inline Edge() : Edge(nullptr) {}
+ ~Edge() override;
+ explicit PROTOBUF_CONSTEXPR Edge(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
+
+ Edge(const Edge& from);
+ Edge(Edge&& from) noexcept
+ : Edge() {
+ *this = ::std::move(from);
+ }
+
+ inline Edge& operator=(const Edge& from) {
+ CopyFrom(from);
+ return *this;
+ }
+ inline Edge& operator=(Edge&& from) noexcept {
+ if (this == &from) return *this;
+ if (GetOwningArena() == from.GetOwningArena()
+ #ifdef PROTOBUF_FORCE_COPY_IN_MOVE
+ && GetOwningArena() != nullptr
+ #endif // !PROTOBUF_FORCE_COPY_IN_MOVE
+ ) {
+ InternalSwap(&from);
+ } else {
+ CopyFrom(from);
+ }
+ return *this;
+ }
+
+ inline const std::string& unknown_fields() const {
+ return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString);
+ }
+ inline std::string* mutable_unknown_fields() {
+ return _internal_metadata_.mutable_unknown_fields<std::string>();
+ }
+
+ static const Edge& default_instance() {
+ return *internal_default_instance();
+ }
+ enum EdgeNameOrRefCase {
+ kName = 2,
+ kNameRef = 3,
+ EDGENAMEORREF_NOT_SET = 0,
+ };
+
+ static inline const Edge* internal_default_instance() {
+ return reinterpret_cast<const Edge*>(
+ &_Edge_default_instance_);
+ }
+ static constexpr int kIndexInFileMessages =
+ 4;
+
+ friend void swap(Edge& a, Edge& b) {
+ a.Swap(&b);
+ }
+ inline void Swap(Edge* other) {
+ if (other == this) return;
+ #ifdef PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() != nullptr &&
+ GetOwningArena() == other->GetOwningArena()) {
+ #else // PROTOBUF_FORCE_COPY_IN_SWAP
+ if (GetOwningArena() == other->GetOwningArena()) {
+ #endif // !PROTOBUF_FORCE_COPY_IN_SWAP
+ InternalSwap(other);
+ } else {
+ ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
+ }
+ }
+ void UnsafeArenaSwap(Edge* other) {
+ if (other == this) return;
+ GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
+ InternalSwap(other);
+ }
+
+ // implements Message ----------------------------------------------
+
+ Edge* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
+ return CreateMaybeMessage<Edge>(arena);
+ }
+ void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final;
+ void CopyFrom(const Edge& from);
+ void MergeFrom(const Edge& from);
+ PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
+ bool IsInitialized() const final;
+
+ size_t ByteSizeLong() const final;
+ const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
+ uint8_t* _InternalSerialize(
+ uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
+ int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
+
+ private:
+ void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ void InternalSwap(Edge* other);
+
+ private:
+ friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
+ static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
+ return "mozilla.devtools.protobuf.Edge";
+ }
+ protected:
+ explicit Edge(::PROTOBUF_NAMESPACE_ID::Arena* arena,
+ bool is_message_owned = false);
+ public:
+
+ std::string GetTypeName() const final;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ enum : int {
+ kReferentFieldNumber = 1,
+ kNameFieldNumber = 2,
+ kNameRefFieldNumber = 3,
+ };
+ // optional uint64 referent = 1;
+ bool has_referent() const;
+ private:
+ bool _internal_has_referent() const;
+ public:
+ void clear_referent();
+ uint64_t referent() const;
+ void set_referent(uint64_t value);
+ private:
+ uint64_t _internal_referent() const;
+ void _internal_set_referent(uint64_t value);
+ public:
+
+ // bytes name = 2;
+ bool has_name() const;
+ private:
+ bool _internal_has_name() const;
+ public:
+ void clear_name();
+ const std::string& name() const;
+ template <typename ArgT0 = const std::string&, typename... ArgT>
+ void set_name(ArgT0&& arg0, ArgT... args);
+ std::string* mutable_name();
+ PROTOBUF_NODISCARD std::string* release_name();
+ void set_allocated_name(std::string* name);
+ private:
+ const std::string& _internal_name() const;
+ inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value);
+ std::string* _internal_mutable_name();
+ public:
+
+ // uint64 nameRef = 3;
+ bool has_nameref() const;
+ private:
+ bool _internal_has_nameref() const;
+ public:
+ void clear_nameref();
+ uint64_t nameref() const;
+ void set_nameref(uint64_t value);
+ private:
+ uint64_t _internal_nameref() const;
+ void _internal_set_nameref(uint64_t value);
+ public:
+
+ void clear_EdgeNameOrRef();
+ EdgeNameOrRefCase EdgeNameOrRef_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Edge)
+ private:
+ class _Internal;
+ void set_has_name();
+ void set_has_nameref();
+
+ inline bool has_EdgeNameOrRef() const;
+ inline void clear_has_EdgeNameOrRef();
+
+ template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
+ typedef void InternalArenaConstructable_;
+ typedef void DestructorSkippable_;
+ struct Impl_ {
+ ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_;
+ mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
+ uint64_t referent_;
+ union EdgeNameOrRefUnion {
+ constexpr EdgeNameOrRefUnion() : _constinit_{} {}
+ ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_;
+ ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_;
+ uint64_t nameref_;
+ } EdgeNameOrRef_;
+ uint32_t _oneof_case_[1];
+
+ };
+ union { Impl_ _impl_; };
+ friend struct ::TableStruct_CoreDump_2eproto;
+};
+// ===================================================================
+
+
+// ===================================================================
+
+#ifdef __GNUC__
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif // __GNUC__
+// Metadata
+
+// optional uint64 timeStamp = 1;
+inline bool Metadata::_internal_has_timestamp() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0;
+ return value;
+}
+inline bool Metadata::has_timestamp() const {
+ return _internal_has_timestamp();
+}
+inline void Metadata::clear_timestamp() {
+ _impl_.timestamp_ = uint64_t{0u};
+ _impl_._has_bits_[0] &= ~0x00000001u;
+}
+inline uint64_t Metadata::_internal_timestamp() const {
+ return _impl_.timestamp_;
+}
+inline uint64_t Metadata::timestamp() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Metadata.timeStamp)
+ return _internal_timestamp();
+}
+inline void Metadata::_internal_set_timestamp(uint64_t value) {
+ _impl_._has_bits_[0] |= 0x00000001u;
+ _impl_.timestamp_ = value;
+}
+inline void Metadata::set_timestamp(uint64_t value) {
+ _internal_set_timestamp(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Metadata.timeStamp)
+}
+
+// -------------------------------------------------------------------
+
+// StackFrame_Data
+
+// optional uint64 id = 1;
+inline bool StackFrame_Data::_internal_has_id() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0;
+ return value;
+}
+inline bool StackFrame_Data::has_id() const {
+ return _internal_has_id();
+}
+inline void StackFrame_Data::clear_id() {
+ _impl_.id_ = uint64_t{0u};
+ _impl_._has_bits_[0] &= ~0x00000002u;
+}
+inline uint64_t StackFrame_Data::_internal_id() const {
+ return _impl_.id_;
+}
+inline uint64_t StackFrame_Data::id() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.id)
+ return _internal_id();
+}
+inline void StackFrame_Data::_internal_set_id(uint64_t value) {
+ _impl_._has_bits_[0] |= 0x00000002u;
+ _impl_.id_ = value;
+}
+inline void StackFrame_Data::set_id(uint64_t value) {
+ _internal_set_id(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.id)
+}
+
+// optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+inline bool StackFrame_Data::_internal_has_parent() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0;
+ PROTOBUF_ASSUME(!value || _impl_.parent_ != nullptr);
+ return value;
+}
+inline bool StackFrame_Data::has_parent() const {
+ return _internal_has_parent();
+}
+inline void StackFrame_Data::clear_parent() {
+ if (_impl_.parent_ != nullptr) _impl_.parent_->Clear();
+ _impl_._has_bits_[0] &= ~0x00000001u;
+}
+inline const ::mozilla::devtools::protobuf::StackFrame& StackFrame_Data::_internal_parent() const {
+ const ::mozilla::devtools::protobuf::StackFrame* p = _impl_.parent_;
+ return p != nullptr ? *p : reinterpret_cast<const ::mozilla::devtools::protobuf::StackFrame&>(
+ ::mozilla::devtools::protobuf::_StackFrame_default_instance_);
+}
+inline const ::mozilla::devtools::protobuf::StackFrame& StackFrame_Data::parent() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.parent)
+ return _internal_parent();
+}
+inline void StackFrame_Data::unsafe_arena_set_allocated_parent(
+ ::mozilla::devtools::protobuf::StackFrame* parent) {
+ if (GetArenaForAllocation() == nullptr) {
+ delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.parent_);
+ }
+ _impl_.parent_ = parent;
+ if (parent) {
+ _impl_._has_bits_[0] |= 0x00000001u;
+ } else {
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ }
+ // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.parent)
+}
+inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::release_parent() {
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.parent_;
+ _impl_.parent_ = nullptr;
+#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE
+ auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp);
+ temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+ if (GetArenaForAllocation() == nullptr) { delete old; }
+#else // PROTOBUF_FORCE_COPY_IN_RELEASE
+ if (GetArenaForAllocation() != nullptr) {
+ temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+ }
+#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE
+ return temp;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::unsafe_arena_release_parent() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.Data.parent)
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.parent_;
+ _impl_.parent_ = nullptr;
+ return temp;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::_internal_mutable_parent() {
+ _impl_._has_bits_[0] |= 0x00000001u;
+ if (_impl_.parent_ == nullptr) {
+ auto* p = CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame>(GetArenaForAllocation());
+ _impl_.parent_ = p;
+ }
+ return _impl_.parent_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::mutable_parent() {
+ ::mozilla::devtools::protobuf::StackFrame* _msg = _internal_mutable_parent();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.parent)
+ return _msg;
+}
+inline void StackFrame_Data::set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent) {
+ ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation();
+ if (message_arena == nullptr) {
+ delete _impl_.parent_;
+ }
+ if (parent) {
+ ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena =
+ ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(parent);
+ if (message_arena != submessage_arena) {
+ parent = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage(
+ message_arena, parent, submessage_arena);
+ }
+ _impl_._has_bits_[0] |= 0x00000001u;
+ } else {
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ }
+ _impl_.parent_ = parent;
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.parent)
+}
+
+// optional uint32 line = 3;
+inline bool StackFrame_Data::_internal_has_line() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0;
+ return value;
+}
+inline bool StackFrame_Data::has_line() const {
+ return _internal_has_line();
+}
+inline void StackFrame_Data::clear_line() {
+ _impl_.line_ = 0u;
+ _impl_._has_bits_[0] &= ~0x00000004u;
+}
+inline uint32_t StackFrame_Data::_internal_line() const {
+ return _impl_.line_;
+}
+inline uint32_t StackFrame_Data::line() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.line)
+ return _internal_line();
+}
+inline void StackFrame_Data::_internal_set_line(uint32_t value) {
+ _impl_._has_bits_[0] |= 0x00000004u;
+ _impl_.line_ = value;
+}
+inline void StackFrame_Data::set_line(uint32_t value) {
+ _internal_set_line(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.line)
+}
+
+// optional uint32 column = 4;
+inline bool StackFrame_Data::_internal_has_column() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0;
+ return value;
+}
+inline bool StackFrame_Data::has_column() const {
+ return _internal_has_column();
+}
+inline void StackFrame_Data::clear_column() {
+ _impl_.column_ = 0u;
+ _impl_._has_bits_[0] &= ~0x00000008u;
+}
+inline uint32_t StackFrame_Data::_internal_column() const {
+ return _impl_.column_;
+}
+inline uint32_t StackFrame_Data::column() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.column)
+ return _internal_column();
+}
+inline void StackFrame_Data::_internal_set_column(uint32_t value) {
+ _impl_._has_bits_[0] |= 0x00000008u;
+ _impl_.column_ = value;
+}
+inline void StackFrame_Data::set_column(uint32_t value) {
+ _internal_set_column(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.column)
+}
+
+// bytes source = 5;
+inline bool StackFrame_Data::_internal_has_source() const {
+ return SourceOrRef_case() == kSource;
+}
+inline bool StackFrame_Data::has_source() const {
+ return _internal_has_source();
+}
+inline void StackFrame_Data::set_has_source() {
+ _impl_._oneof_case_[0] = kSource;
+}
+inline void StackFrame_Data::clear_source() {
+ if (_internal_has_source()) {
+ _impl_.SourceOrRef_.source_.Destroy();
+ clear_has_SourceOrRef();
+ }
+}
+inline const std::string& StackFrame_Data::source() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.source)
+ return _internal_source();
+}
+template <typename ArgT0, typename... ArgT>
+inline void StackFrame_Data::set_source(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ _impl_.SourceOrRef_.source_.InitDefault();
+ }
+ _impl_.SourceOrRef_.source_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.source)
+}
+inline std::string* StackFrame_Data::mutable_source() {
+ std::string* _s = _internal_mutable_source();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.source)
+ return _s;
+}
+inline const std::string& StackFrame_Data::_internal_source() const {
+ if (_internal_has_source()) {
+ return _impl_.SourceOrRef_.source_.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void StackFrame_Data::_internal_set_source(const std::string& value) {
+ if (!_internal_has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ _impl_.SourceOrRef_.source_.InitDefault();
+ }
+ _impl_.SourceOrRef_.source_.Set(value, GetArenaForAllocation());
+}
+inline std::string* StackFrame_Data::_internal_mutable_source() {
+ if (!_internal_has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ _impl_.SourceOrRef_.source_.InitDefault();
+ }
+ return _impl_.SourceOrRef_.source_.Mutable( GetArenaForAllocation());
+}
+inline std::string* StackFrame_Data::release_source() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.Data.source)
+ if (_internal_has_source()) {
+ clear_has_SourceOrRef();
+ return _impl_.SourceOrRef_.source_.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void StackFrame_Data::set_allocated_source(std::string* source) {
+ if (has_SourceOrRef()) {
+ clear_SourceOrRef();
+ }
+ if (source != nullptr) {
+ set_has_source();
+ _impl_.SourceOrRef_.source_.InitAllocated(source, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.source)
+}
+
+// uint64 sourceRef = 6;
+inline bool StackFrame_Data::_internal_has_sourceref() const {
+ return SourceOrRef_case() == kSourceRef;
+}
+inline bool StackFrame_Data::has_sourceref() const {
+ return _internal_has_sourceref();
+}
+inline void StackFrame_Data::set_has_sourceref() {
+ _impl_._oneof_case_[0] = kSourceRef;
+}
+inline void StackFrame_Data::clear_sourceref() {
+ if (_internal_has_sourceref()) {
+ _impl_.SourceOrRef_.sourceref_ = uint64_t{0u};
+ clear_has_SourceOrRef();
+ }
+}
+inline uint64_t StackFrame_Data::_internal_sourceref() const {
+ if (_internal_has_sourceref()) {
+ return _impl_.SourceOrRef_.sourceref_;
+ }
+ return uint64_t{0u};
+}
+inline void StackFrame_Data::_internal_set_sourceref(uint64_t value) {
+ if (!_internal_has_sourceref()) {
+ clear_SourceOrRef();
+ set_has_sourceref();
+ }
+ _impl_.SourceOrRef_.sourceref_ = value;
+}
+inline uint64_t StackFrame_Data::sourceref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.sourceRef)
+ return _internal_sourceref();
+}
+inline void StackFrame_Data::set_sourceref(uint64_t value) {
+ _internal_set_sourceref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.sourceRef)
+}
+
+// bytes functionDisplayName = 7;
+inline bool StackFrame_Data::_internal_has_functiondisplayname() const {
+ return FunctionDisplayNameOrRef_case() == kFunctionDisplayName;
+}
+inline bool StackFrame_Data::has_functiondisplayname() const {
+ return _internal_has_functiondisplayname();
+}
+inline void StackFrame_Data::set_has_functiondisplayname() {
+ _impl_._oneof_case_[1] = kFunctionDisplayName;
+}
+inline void StackFrame_Data::clear_functiondisplayname() {
+ if (_internal_has_functiondisplayname()) {
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Destroy();
+ clear_has_FunctionDisplayNameOrRef();
+ }
+}
+inline const std::string& StackFrame_Data::functiondisplayname() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName)
+ return _internal_functiondisplayname();
+}
+template <typename ArgT0, typename... ArgT>
+inline void StackFrame_Data::set_functiondisplayname(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitDefault();
+ }
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName)
+}
+inline std::string* StackFrame_Data::mutable_functiondisplayname() {
+ std::string* _s = _internal_mutable_functiondisplayname();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName)
+ return _s;
+}
+inline const std::string& StackFrame_Data::_internal_functiondisplayname() const {
+ if (_internal_has_functiondisplayname()) {
+ return _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void StackFrame_Data::_internal_set_functiondisplayname(const std::string& value) {
+ if (!_internal_has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitDefault();
+ }
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Set(value, GetArenaForAllocation());
+}
+inline std::string* StackFrame_Data::_internal_mutable_functiondisplayname() {
+ if (!_internal_has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitDefault();
+ }
+ return _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Mutable( GetArenaForAllocation());
+}
+inline std::string* StackFrame_Data::release_functiondisplayname() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName)
+ if (_internal_has_functiondisplayname()) {
+ clear_has_FunctionDisplayNameOrRef();
+ return _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void StackFrame_Data::set_allocated_functiondisplayname(std::string* functiondisplayname) {
+ if (has_FunctionDisplayNameOrRef()) {
+ clear_FunctionDisplayNameOrRef();
+ }
+ if (functiondisplayname != nullptr) {
+ set_has_functiondisplayname();
+ _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitAllocated(functiondisplayname, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName)
+}
+
+// uint64 functionDisplayNameRef = 8;
+inline bool StackFrame_Data::_internal_has_functiondisplaynameref() const {
+ return FunctionDisplayNameOrRef_case() == kFunctionDisplayNameRef;
+}
+inline bool StackFrame_Data::has_functiondisplaynameref() const {
+ return _internal_has_functiondisplaynameref();
+}
+inline void StackFrame_Data::set_has_functiondisplaynameref() {
+ _impl_._oneof_case_[1] = kFunctionDisplayNameRef;
+}
+inline void StackFrame_Data::clear_functiondisplaynameref() {
+ if (_internal_has_functiondisplaynameref()) {
+ _impl_.FunctionDisplayNameOrRef_.functiondisplaynameref_ = uint64_t{0u};
+ clear_has_FunctionDisplayNameOrRef();
+ }
+}
+inline uint64_t StackFrame_Data::_internal_functiondisplaynameref() const {
+ if (_internal_has_functiondisplaynameref()) {
+ return _impl_.FunctionDisplayNameOrRef_.functiondisplaynameref_;
+ }
+ return uint64_t{0u};
+}
+inline void StackFrame_Data::_internal_set_functiondisplaynameref(uint64_t value) {
+ if (!_internal_has_functiondisplaynameref()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplaynameref();
+ }
+ _impl_.FunctionDisplayNameOrRef_.functiondisplaynameref_ = value;
+}
+inline uint64_t StackFrame_Data::functiondisplaynameref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayNameRef)
+ return _internal_functiondisplaynameref();
+}
+inline void StackFrame_Data::set_functiondisplaynameref(uint64_t value) {
+ _internal_set_functiondisplaynameref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayNameRef)
+}
+
+// optional bool isSystem = 9;
+inline bool StackFrame_Data::_internal_has_issystem() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0;
+ return value;
+}
+inline bool StackFrame_Data::has_issystem() const {
+ return _internal_has_issystem();
+}
+inline void StackFrame_Data::clear_issystem() {
+ _impl_.issystem_ = false;
+ _impl_._has_bits_[0] &= ~0x00000010u;
+}
+inline bool StackFrame_Data::_internal_issystem() const {
+ return _impl_.issystem_;
+}
+inline bool StackFrame_Data::issystem() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSystem)
+ return _internal_issystem();
+}
+inline void StackFrame_Data::_internal_set_issystem(bool value) {
+ _impl_._has_bits_[0] |= 0x00000010u;
+ _impl_.issystem_ = value;
+}
+inline void StackFrame_Data::set_issystem(bool value) {
+ _internal_set_issystem(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSystem)
+}
+
+// optional bool isSelfHosted = 10;
+inline bool StackFrame_Data::_internal_has_isselfhosted() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0;
+ return value;
+}
+inline bool StackFrame_Data::has_isselfhosted() const {
+ return _internal_has_isselfhosted();
+}
+inline void StackFrame_Data::clear_isselfhosted() {
+ _impl_.isselfhosted_ = false;
+ _impl_._has_bits_[0] &= ~0x00000020u;
+}
+inline bool StackFrame_Data::_internal_isselfhosted() const {
+ return _impl_.isselfhosted_;
+}
+inline bool StackFrame_Data::isselfhosted() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted)
+ return _internal_isselfhosted();
+}
+inline void StackFrame_Data::_internal_set_isselfhosted(bool value) {
+ _impl_._has_bits_[0] |= 0x00000020u;
+ _impl_.isselfhosted_ = value;
+}
+inline void StackFrame_Data::set_isselfhosted(bool value) {
+ _internal_set_isselfhosted(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted)
+}
+
+inline bool StackFrame_Data::has_SourceOrRef() const {
+ return SourceOrRef_case() != SOURCEORREF_NOT_SET;
+}
+inline void StackFrame_Data::clear_has_SourceOrRef() {
+ _impl_._oneof_case_[0] = SOURCEORREF_NOT_SET;
+}
+inline bool StackFrame_Data::has_FunctionDisplayNameOrRef() const {
+ return FunctionDisplayNameOrRef_case() != FUNCTIONDISPLAYNAMEORREF_NOT_SET;
+}
+inline void StackFrame_Data::clear_has_FunctionDisplayNameOrRef() {
+ _impl_._oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET;
+}
+inline StackFrame_Data::SourceOrRefCase StackFrame_Data::SourceOrRef_case() const {
+ return StackFrame_Data::SourceOrRefCase(_impl_._oneof_case_[0]);
+}
+inline StackFrame_Data::FunctionDisplayNameOrRefCase StackFrame_Data::FunctionDisplayNameOrRef_case() const {
+ return StackFrame_Data::FunctionDisplayNameOrRefCase(_impl_._oneof_case_[1]);
+}
+// -------------------------------------------------------------------
+
+// StackFrame
+
+// .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+inline bool StackFrame::_internal_has_data() const {
+ return StackFrameType_case() == kData;
+}
+inline bool StackFrame::has_data() const {
+ return _internal_has_data();
+}
+inline void StackFrame::set_has_data() {
+ _impl_._oneof_case_[0] = kData;
+}
+inline void StackFrame::clear_data() {
+ if (_internal_has_data()) {
+ if (GetArenaForAllocation() == nullptr) {
+ delete _impl_.StackFrameType_.data_;
+ }
+ clear_has_StackFrameType();
+ }
+}
+inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::release_data() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.data)
+ if (_internal_has_data()) {
+ clear_has_StackFrameType();
+ ::mozilla::devtools::protobuf::StackFrame_Data* temp = _impl_.StackFrameType_.data_;
+ if (GetArenaForAllocation() != nullptr) {
+ temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+ }
+ _impl_.StackFrameType_.data_ = nullptr;
+ return temp;
+ } else {
+ return nullptr;
+ }
+}
+inline const ::mozilla::devtools::protobuf::StackFrame_Data& StackFrame::_internal_data() const {
+ return _internal_has_data()
+ ? *_impl_.StackFrameType_.data_
+ : reinterpret_cast< ::mozilla::devtools::protobuf::StackFrame_Data&>(::mozilla::devtools::protobuf::_StackFrame_Data_default_instance_);
+}
+inline const ::mozilla::devtools::protobuf::StackFrame_Data& StackFrame::data() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.data)
+ return _internal_data();
+}
+inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::unsafe_arena_release_data() {
+ // @@protoc_insertion_point(field_unsafe_arena_release:mozilla.devtools.protobuf.StackFrame.data)
+ if (_internal_has_data()) {
+ clear_has_StackFrameType();
+ ::mozilla::devtools::protobuf::StackFrame_Data* temp = _impl_.StackFrameType_.data_;
+ _impl_.StackFrameType_.data_ = nullptr;
+ return temp;
+ } else {
+ return nullptr;
+ }
+}
+inline void StackFrame::unsafe_arena_set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data) {
+ clear_StackFrameType();
+ if (data) {
+ set_has_data();
+ _impl_.StackFrameType_.data_ = data;
+ }
+ // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.devtools.protobuf.StackFrame.data)
+}
+inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::_internal_mutable_data() {
+ if (!_internal_has_data()) {
+ clear_StackFrameType();
+ set_has_data();
+ _impl_.StackFrameType_.data_ = CreateMaybeMessage< ::mozilla::devtools::protobuf::StackFrame_Data >(GetArenaForAllocation());
+ }
+ return _impl_.StackFrameType_.data_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::mutable_data() {
+ ::mozilla::devtools::protobuf::StackFrame_Data* _msg = _internal_mutable_data();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.data)
+ return _msg;
+}
+
+// uint64 ref = 2;
+inline bool StackFrame::_internal_has_ref() const {
+ return StackFrameType_case() == kRef;
+}
+inline bool StackFrame::has_ref() const {
+ return _internal_has_ref();
+}
+inline void StackFrame::set_has_ref() {
+ _impl_._oneof_case_[0] = kRef;
+}
+inline void StackFrame::clear_ref() {
+ if (_internal_has_ref()) {
+ _impl_.StackFrameType_.ref_ = uint64_t{0u};
+ clear_has_StackFrameType();
+ }
+}
+inline uint64_t StackFrame::_internal_ref() const {
+ if (_internal_has_ref()) {
+ return _impl_.StackFrameType_.ref_;
+ }
+ return uint64_t{0u};
+}
+inline void StackFrame::_internal_set_ref(uint64_t value) {
+ if (!_internal_has_ref()) {
+ clear_StackFrameType();
+ set_has_ref();
+ }
+ _impl_.StackFrameType_.ref_ = value;
+}
+inline uint64_t StackFrame::ref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.ref)
+ return _internal_ref();
+}
+inline void StackFrame::set_ref(uint64_t value) {
+ _internal_set_ref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.ref)
+}
+
+inline bool StackFrame::has_StackFrameType() const {
+ return StackFrameType_case() != STACKFRAMETYPE_NOT_SET;
+}
+inline void StackFrame::clear_has_StackFrameType() {
+ _impl_._oneof_case_[0] = STACKFRAMETYPE_NOT_SET;
+}
+inline StackFrame::StackFrameTypeCase StackFrame::StackFrameType_case() const {
+ return StackFrame::StackFrameTypeCase(_impl_._oneof_case_[0]);
+}
+// -------------------------------------------------------------------
+
+// Node
+
+// optional uint64 id = 1;
+inline bool Node::_internal_has_id() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0;
+ return value;
+}
+inline bool Node::has_id() const {
+ return _internal_has_id();
+}
+inline void Node::clear_id() {
+ _impl_.id_ = uint64_t{0u};
+ _impl_._has_bits_[0] &= ~0x00000002u;
+}
+inline uint64_t Node::_internal_id() const {
+ return _impl_.id_;
+}
+inline uint64_t Node::id() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.id)
+ return _internal_id();
+}
+inline void Node::_internal_set_id(uint64_t value) {
+ _impl_._has_bits_[0] |= 0x00000002u;
+ _impl_.id_ = value;
+}
+inline void Node::set_id(uint64_t value) {
+ _internal_set_id(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.id)
+}
+
+// bytes typeName = 2;
+inline bool Node::_internal_has_typename_() const {
+ return TypeNameOrRef_case() == kTypeName;
+}
+inline bool Node::has_typename_() const {
+ return _internal_has_typename_();
+}
+inline void Node::set_has_typename_() {
+ _impl_._oneof_case_[0] = kTypeName;
+}
+inline void Node::clear_typename_() {
+ if (_internal_has_typename_()) {
+ _impl_.TypeNameOrRef_.typename__.Destroy();
+ clear_has_TypeNameOrRef();
+ }
+}
+inline const std::string& Node::typename_() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.typeName)
+ return _internal_typename_();
+}
+template <typename ArgT0, typename... ArgT>
+inline void Node::set_typename_(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ _impl_.TypeNameOrRef_.typename__.InitDefault();
+ }
+ _impl_.TypeNameOrRef_.typename__.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.typeName)
+}
+inline std::string* Node::mutable_typename_() {
+ std::string* _s = _internal_mutable_typename_();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.typeName)
+ return _s;
+}
+inline const std::string& Node::_internal_typename_() const {
+ if (_internal_has_typename_()) {
+ return _impl_.TypeNameOrRef_.typename__.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::_internal_set_typename_(const std::string& value) {
+ if (!_internal_has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ _impl_.TypeNameOrRef_.typename__.InitDefault();
+ }
+ _impl_.TypeNameOrRef_.typename__.Set(value, GetArenaForAllocation());
+}
+inline std::string* Node::_internal_mutable_typename_() {
+ if (!_internal_has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ _impl_.TypeNameOrRef_.typename__.InitDefault();
+ }
+ return _impl_.TypeNameOrRef_.typename__.Mutable( GetArenaForAllocation());
+}
+inline std::string* Node::release_typename_() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.typeName)
+ if (_internal_has_typename_()) {
+ clear_has_TypeNameOrRef();
+ return _impl_.TypeNameOrRef_.typename__.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void Node::set_allocated_typename_(std::string* typename_) {
+ if (has_TypeNameOrRef()) {
+ clear_TypeNameOrRef();
+ }
+ if (typename_ != nullptr) {
+ set_has_typename_();
+ _impl_.TypeNameOrRef_.typename__.InitAllocated(typename_, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.typeName)
+}
+
+// uint64 typeNameRef = 3;
+inline bool Node::_internal_has_typenameref() const {
+ return TypeNameOrRef_case() == kTypeNameRef;
+}
+inline bool Node::has_typenameref() const {
+ return _internal_has_typenameref();
+}
+inline void Node::set_has_typenameref() {
+ _impl_._oneof_case_[0] = kTypeNameRef;
+}
+inline void Node::clear_typenameref() {
+ if (_internal_has_typenameref()) {
+ _impl_.TypeNameOrRef_.typenameref_ = uint64_t{0u};
+ clear_has_TypeNameOrRef();
+ }
+}
+inline uint64_t Node::_internal_typenameref() const {
+ if (_internal_has_typenameref()) {
+ return _impl_.TypeNameOrRef_.typenameref_;
+ }
+ return uint64_t{0u};
+}
+inline void Node::_internal_set_typenameref(uint64_t value) {
+ if (!_internal_has_typenameref()) {
+ clear_TypeNameOrRef();
+ set_has_typenameref();
+ }
+ _impl_.TypeNameOrRef_.typenameref_ = value;
+}
+inline uint64_t Node::typenameref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.typeNameRef)
+ return _internal_typenameref();
+}
+inline void Node::set_typenameref(uint64_t value) {
+ _internal_set_typenameref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.typeNameRef)
+}
+
+// optional uint64 size = 4;
+inline bool Node::_internal_has_size() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0;
+ return value;
+}
+inline bool Node::has_size() const {
+ return _internal_has_size();
+}
+inline void Node::clear_size() {
+ _impl_.size_ = uint64_t{0u};
+ _impl_._has_bits_[0] &= ~0x00000004u;
+}
+inline uint64_t Node::_internal_size() const {
+ return _impl_.size_;
+}
+inline uint64_t Node::size() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.size)
+ return _internal_size();
+}
+inline void Node::_internal_set_size(uint64_t value) {
+ _impl_._has_bits_[0] |= 0x00000004u;
+ _impl_.size_ = value;
+}
+inline void Node::set_size(uint64_t value) {
+ _internal_set_size(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.size)
+}
+
+// repeated .mozilla.devtools.protobuf.Edge edges = 5;
+inline int Node::_internal_edges_size() const {
+ return _impl_.edges_.size();
+}
+inline int Node::edges_size() const {
+ return _internal_edges_size();
+}
+inline void Node::clear_edges() {
+ _impl_.edges_.Clear();
+}
+inline ::mozilla::devtools::protobuf::Edge* Node::mutable_edges(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.edges)
+ return _impl_.edges_.Mutable(index);
+}
+inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >*
+Node::mutable_edges() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.devtools.protobuf.Node.edges)
+ return &_impl_.edges_;
+}
+inline const ::mozilla::devtools::protobuf::Edge& Node::_internal_edges(int index) const {
+ return _impl_.edges_.Get(index);
+}
+inline const ::mozilla::devtools::protobuf::Edge& Node::edges(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.edges)
+ return _internal_edges(index);
+}
+inline ::mozilla::devtools::protobuf::Edge* Node::_internal_add_edges() {
+ return _impl_.edges_.Add();
+}
+inline ::mozilla::devtools::protobuf::Edge* Node::add_edges() {
+ ::mozilla::devtools::protobuf::Edge* _add = _internal_add_edges();
+ // @@protoc_insertion_point(field_add:mozilla.devtools.protobuf.Node.edges)
+ return _add;
+}
+inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >&
+Node::edges() const {
+ // @@protoc_insertion_point(field_list:mozilla.devtools.protobuf.Node.edges)
+ return _impl_.edges_;
+}
+
+// optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+inline bool Node::_internal_has_allocationstack() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0;
+ PROTOBUF_ASSUME(!value || _impl_.allocationstack_ != nullptr);
+ return value;
+}
+inline bool Node::has_allocationstack() const {
+ return _internal_has_allocationstack();
+}
+inline void Node::clear_allocationstack() {
+ if (_impl_.allocationstack_ != nullptr) _impl_.allocationstack_->Clear();
+ _impl_._has_bits_[0] &= ~0x00000001u;
+}
+inline const ::mozilla::devtools::protobuf::StackFrame& Node::_internal_allocationstack() const {
+ const ::mozilla::devtools::protobuf::StackFrame* p = _impl_.allocationstack_;
+ return p != nullptr ? *p : reinterpret_cast<const ::mozilla::devtools::protobuf::StackFrame&>(
+ ::mozilla::devtools::protobuf::_StackFrame_default_instance_);
+}
+inline const ::mozilla::devtools::protobuf::StackFrame& Node::allocationstack() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.allocationStack)
+ return _internal_allocationstack();
+}
+inline void Node::unsafe_arena_set_allocated_allocationstack(
+ ::mozilla::devtools::protobuf::StackFrame* allocationstack) {
+ if (GetArenaForAllocation() == nullptr) {
+ delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.allocationstack_);
+ }
+ _impl_.allocationstack_ = allocationstack;
+ if (allocationstack) {
+ _impl_._has_bits_[0] |= 0x00000001u;
+ } else {
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ }
+ // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.devtools.protobuf.Node.allocationStack)
+}
+inline ::mozilla::devtools::protobuf::StackFrame* Node::release_allocationstack() {
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.allocationstack_;
+ _impl_.allocationstack_ = nullptr;
+#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE
+ auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp);
+ temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+ if (GetArenaForAllocation() == nullptr) { delete old; }
+#else // PROTOBUF_FORCE_COPY_IN_RELEASE
+ if (GetArenaForAllocation() != nullptr) {
+ temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+ }
+#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE
+ return temp;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* Node::unsafe_arena_release_allocationstack() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.allocationStack)
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.allocationstack_;
+ _impl_.allocationstack_ = nullptr;
+ return temp;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* Node::_internal_mutable_allocationstack() {
+ _impl_._has_bits_[0] |= 0x00000001u;
+ if (_impl_.allocationstack_ == nullptr) {
+ auto* p = CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame>(GetArenaForAllocation());
+ _impl_.allocationstack_ = p;
+ }
+ return _impl_.allocationstack_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* Node::mutable_allocationstack() {
+ ::mozilla::devtools::protobuf::StackFrame* _msg = _internal_mutable_allocationstack();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.allocationStack)
+ return _msg;
+}
+inline void Node::set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack) {
+ ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation();
+ if (message_arena == nullptr) {
+ delete _impl_.allocationstack_;
+ }
+ if (allocationstack) {
+ ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena =
+ ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(allocationstack);
+ if (message_arena != submessage_arena) {
+ allocationstack = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage(
+ message_arena, allocationstack, submessage_arena);
+ }
+ _impl_._has_bits_[0] |= 0x00000001u;
+ } else {
+ _impl_._has_bits_[0] &= ~0x00000001u;
+ }
+ _impl_.allocationstack_ = allocationstack;
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.allocationStack)
+}
+
+// bytes jsObjectClassName = 7;
+inline bool Node::_internal_has_jsobjectclassname() const {
+ return JSObjectClassNameOrRef_case() == kJsObjectClassName;
+}
+inline bool Node::has_jsobjectclassname() const {
+ return _internal_has_jsobjectclassname();
+}
+inline void Node::set_has_jsobjectclassname() {
+ _impl_._oneof_case_[1] = kJsObjectClassName;
+}
+inline void Node::clear_jsobjectclassname() {
+ if (_internal_has_jsobjectclassname()) {
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Destroy();
+ clear_has_JSObjectClassNameOrRef();
+ }
+}
+inline const std::string& Node::jsobjectclassname() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.jsObjectClassName)
+ return _internal_jsobjectclassname();
+}
+template <typename ArgT0, typename... ArgT>
+inline void Node::set_jsobjectclassname(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitDefault();
+ }
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.jsObjectClassName)
+}
+inline std::string* Node::mutable_jsobjectclassname() {
+ std::string* _s = _internal_mutable_jsobjectclassname();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.jsObjectClassName)
+ return _s;
+}
+inline const std::string& Node::_internal_jsobjectclassname() const {
+ if (_internal_has_jsobjectclassname()) {
+ return _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::_internal_set_jsobjectclassname(const std::string& value) {
+ if (!_internal_has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitDefault();
+ }
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Set(value, GetArenaForAllocation());
+}
+inline std::string* Node::_internal_mutable_jsobjectclassname() {
+ if (!_internal_has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitDefault();
+ }
+ return _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Mutable( GetArenaForAllocation());
+}
+inline std::string* Node::release_jsobjectclassname() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.jsObjectClassName)
+ if (_internal_has_jsobjectclassname()) {
+ clear_has_JSObjectClassNameOrRef();
+ return _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void Node::set_allocated_jsobjectclassname(std::string* jsobjectclassname) {
+ if (has_JSObjectClassNameOrRef()) {
+ clear_JSObjectClassNameOrRef();
+ }
+ if (jsobjectclassname != nullptr) {
+ set_has_jsobjectclassname();
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitAllocated(jsobjectclassname, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.jsObjectClassName)
+}
+
+// uint64 jsObjectClassNameRef = 8;
+inline bool Node::_internal_has_jsobjectclassnameref() const {
+ return JSObjectClassNameOrRef_case() == kJsObjectClassNameRef;
+}
+inline bool Node::has_jsobjectclassnameref() const {
+ return _internal_has_jsobjectclassnameref();
+}
+inline void Node::set_has_jsobjectclassnameref() {
+ _impl_._oneof_case_[1] = kJsObjectClassNameRef;
+}
+inline void Node::clear_jsobjectclassnameref() {
+ if (_internal_has_jsobjectclassnameref()) {
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassnameref_ = uint64_t{0u};
+ clear_has_JSObjectClassNameOrRef();
+ }
+}
+inline uint64_t Node::_internal_jsobjectclassnameref() const {
+ if (_internal_has_jsobjectclassnameref()) {
+ return _impl_.JSObjectClassNameOrRef_.jsobjectclassnameref_;
+ }
+ return uint64_t{0u};
+}
+inline void Node::_internal_set_jsobjectclassnameref(uint64_t value) {
+ if (!_internal_has_jsobjectclassnameref()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassnameref();
+ }
+ _impl_.JSObjectClassNameOrRef_.jsobjectclassnameref_ = value;
+}
+inline uint64_t Node::jsobjectclassnameref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.jsObjectClassNameRef)
+ return _internal_jsobjectclassnameref();
+}
+inline void Node::set_jsobjectclassnameref(uint64_t value) {
+ _internal_set_jsobjectclassnameref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.jsObjectClassNameRef)
+}
+
+// optional uint32 coarseType = 9 [default = 0];
+inline bool Node::_internal_has_coarsetype() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0;
+ return value;
+}
+inline bool Node::has_coarsetype() const {
+ return _internal_has_coarsetype();
+}
+inline void Node::clear_coarsetype() {
+ _impl_.coarsetype_ = 0u;
+ _impl_._has_bits_[0] &= ~0x00000008u;
+}
+inline uint32_t Node::_internal_coarsetype() const {
+ return _impl_.coarsetype_;
+}
+inline uint32_t Node::coarsetype() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.coarseType)
+ return _internal_coarsetype();
+}
+inline void Node::_internal_set_coarsetype(uint32_t value) {
+ _impl_._has_bits_[0] |= 0x00000008u;
+ _impl_.coarsetype_ = value;
+}
+inline void Node::set_coarsetype(uint32_t value) {
+ _internal_set_coarsetype(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.coarseType)
+}
+
+// bytes scriptFilename = 10;
+inline bool Node::_internal_has_scriptfilename() const {
+ return ScriptFilenameOrRef_case() == kScriptFilename;
+}
+inline bool Node::has_scriptfilename() const {
+ return _internal_has_scriptfilename();
+}
+inline void Node::set_has_scriptfilename() {
+ _impl_._oneof_case_[2] = kScriptFilename;
+}
+inline void Node::clear_scriptfilename() {
+ if (_internal_has_scriptfilename()) {
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.Destroy();
+ clear_has_ScriptFilenameOrRef();
+ }
+}
+inline const std::string& Node::scriptfilename() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.scriptFilename)
+ return _internal_scriptfilename();
+}
+template <typename ArgT0, typename... ArgT>
+inline void Node::set_scriptfilename(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.InitDefault();
+ }
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.scriptFilename)
+}
+inline std::string* Node::mutable_scriptfilename() {
+ std::string* _s = _internal_mutable_scriptfilename();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.scriptFilename)
+ return _s;
+}
+inline const std::string& Node::_internal_scriptfilename() const {
+ if (_internal_has_scriptfilename()) {
+ return _impl_.ScriptFilenameOrRef_.scriptfilename_.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::_internal_set_scriptfilename(const std::string& value) {
+ if (!_internal_has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.InitDefault();
+ }
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.Set(value, GetArenaForAllocation());
+}
+inline std::string* Node::_internal_mutable_scriptfilename() {
+ if (!_internal_has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.InitDefault();
+ }
+ return _impl_.ScriptFilenameOrRef_.scriptfilename_.Mutable( GetArenaForAllocation());
+}
+inline std::string* Node::release_scriptfilename() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.scriptFilename)
+ if (_internal_has_scriptfilename()) {
+ clear_has_ScriptFilenameOrRef();
+ return _impl_.ScriptFilenameOrRef_.scriptfilename_.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void Node::set_allocated_scriptfilename(std::string* scriptfilename) {
+ if (has_ScriptFilenameOrRef()) {
+ clear_ScriptFilenameOrRef();
+ }
+ if (scriptfilename != nullptr) {
+ set_has_scriptfilename();
+ _impl_.ScriptFilenameOrRef_.scriptfilename_.InitAllocated(scriptfilename, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.scriptFilename)
+}
+
+// uint64 scriptFilenameRef = 11;
+inline bool Node::_internal_has_scriptfilenameref() const {
+ return ScriptFilenameOrRef_case() == kScriptFilenameRef;
+}
+inline bool Node::has_scriptfilenameref() const {
+ return _internal_has_scriptfilenameref();
+}
+inline void Node::set_has_scriptfilenameref() {
+ _impl_._oneof_case_[2] = kScriptFilenameRef;
+}
+inline void Node::clear_scriptfilenameref() {
+ if (_internal_has_scriptfilenameref()) {
+ _impl_.ScriptFilenameOrRef_.scriptfilenameref_ = uint64_t{0u};
+ clear_has_ScriptFilenameOrRef();
+ }
+}
+inline uint64_t Node::_internal_scriptfilenameref() const {
+ if (_internal_has_scriptfilenameref()) {
+ return _impl_.ScriptFilenameOrRef_.scriptfilenameref_;
+ }
+ return uint64_t{0u};
+}
+inline void Node::_internal_set_scriptfilenameref(uint64_t value) {
+ if (!_internal_has_scriptfilenameref()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilenameref();
+ }
+ _impl_.ScriptFilenameOrRef_.scriptfilenameref_ = value;
+}
+inline uint64_t Node::scriptfilenameref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.scriptFilenameRef)
+ return _internal_scriptfilenameref();
+}
+inline void Node::set_scriptfilenameref(uint64_t value) {
+ _internal_set_scriptfilenameref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.scriptFilenameRef)
+}
+
+// bytes descriptiveTypeName = 12;
+inline bool Node::_internal_has_descriptivetypename() const {
+ return descriptiveTypeNameOrRef_case() == kDescriptiveTypeName;
+}
+inline bool Node::has_descriptivetypename() const {
+ return _internal_has_descriptivetypename();
+}
+inline void Node::set_has_descriptivetypename() {
+ _impl_._oneof_case_[3] = kDescriptiveTypeName;
+}
+inline void Node::clear_descriptivetypename() {
+ if (_internal_has_descriptivetypename()) {
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Destroy();
+ clear_has_descriptiveTypeNameOrRef();
+ }
+}
+inline const std::string& Node::descriptivetypename() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.descriptiveTypeName)
+ return _internal_descriptivetypename();
+}
+template <typename ArgT0, typename... ArgT>
+inline void Node::set_descriptivetypename(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_descriptivetypename()) {
+ clear_descriptiveTypeNameOrRef();
+ set_has_descriptivetypename();
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitDefault();
+ }
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.descriptiveTypeName)
+}
+inline std::string* Node::mutable_descriptivetypename() {
+ std::string* _s = _internal_mutable_descriptivetypename();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.descriptiveTypeName)
+ return _s;
+}
+inline const std::string& Node::_internal_descriptivetypename() const {
+ if (_internal_has_descriptivetypename()) {
+ return _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::_internal_set_descriptivetypename(const std::string& value) {
+ if (!_internal_has_descriptivetypename()) {
+ clear_descriptiveTypeNameOrRef();
+ set_has_descriptivetypename();
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitDefault();
+ }
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Set(value, GetArenaForAllocation());
+}
+inline std::string* Node::_internal_mutable_descriptivetypename() {
+ if (!_internal_has_descriptivetypename()) {
+ clear_descriptiveTypeNameOrRef();
+ set_has_descriptivetypename();
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitDefault();
+ }
+ return _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Mutable( GetArenaForAllocation());
+}
+inline std::string* Node::release_descriptivetypename() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.descriptiveTypeName)
+ if (_internal_has_descriptivetypename()) {
+ clear_has_descriptiveTypeNameOrRef();
+ return _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void Node::set_allocated_descriptivetypename(std::string* descriptivetypename) {
+ if (has_descriptiveTypeNameOrRef()) {
+ clear_descriptiveTypeNameOrRef();
+ }
+ if (descriptivetypename != nullptr) {
+ set_has_descriptivetypename();
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitAllocated(descriptivetypename, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.descriptiveTypeName)
+}
+
+// uint64 descriptiveTypeNameRef = 13;
+inline bool Node::_internal_has_descriptivetypenameref() const {
+ return descriptiveTypeNameOrRef_case() == kDescriptiveTypeNameRef;
+}
+inline bool Node::has_descriptivetypenameref() const {
+ return _internal_has_descriptivetypenameref();
+}
+inline void Node::set_has_descriptivetypenameref() {
+ _impl_._oneof_case_[3] = kDescriptiveTypeNameRef;
+}
+inline void Node::clear_descriptivetypenameref() {
+ if (_internal_has_descriptivetypenameref()) {
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypenameref_ = uint64_t{0u};
+ clear_has_descriptiveTypeNameOrRef();
+ }
+}
+inline uint64_t Node::_internal_descriptivetypenameref() const {
+ if (_internal_has_descriptivetypenameref()) {
+ return _impl_.descriptiveTypeNameOrRef_.descriptivetypenameref_;
+ }
+ return uint64_t{0u};
+}
+inline void Node::_internal_set_descriptivetypenameref(uint64_t value) {
+ if (!_internal_has_descriptivetypenameref()) {
+ clear_descriptiveTypeNameOrRef();
+ set_has_descriptivetypenameref();
+ }
+ _impl_.descriptiveTypeNameOrRef_.descriptivetypenameref_ = value;
+}
+inline uint64_t Node::descriptivetypenameref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.descriptiveTypeNameRef)
+ return _internal_descriptivetypenameref();
+}
+inline void Node::set_descriptivetypenameref(uint64_t value) {
+ _internal_set_descriptivetypenameref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.descriptiveTypeNameRef)
+}
+
+inline bool Node::has_TypeNameOrRef() const {
+ return TypeNameOrRef_case() != TYPENAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_TypeNameOrRef() {
+ _impl_._oneof_case_[0] = TYPENAMEORREF_NOT_SET;
+}
+inline bool Node::has_JSObjectClassNameOrRef() const {
+ return JSObjectClassNameOrRef_case() != JSOBJECTCLASSNAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_JSObjectClassNameOrRef() {
+ _impl_._oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET;
+}
+inline bool Node::has_ScriptFilenameOrRef() const {
+ return ScriptFilenameOrRef_case() != SCRIPTFILENAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_ScriptFilenameOrRef() {
+ _impl_._oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET;
+}
+inline bool Node::has_descriptiveTypeNameOrRef() const {
+ return descriptiveTypeNameOrRef_case() != DESCRIPTIVETYPENAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_descriptiveTypeNameOrRef() {
+ _impl_._oneof_case_[3] = DESCRIPTIVETYPENAMEORREF_NOT_SET;
+}
+inline Node::TypeNameOrRefCase Node::TypeNameOrRef_case() const {
+ return Node::TypeNameOrRefCase(_impl_._oneof_case_[0]);
+}
+inline Node::JSObjectClassNameOrRefCase Node::JSObjectClassNameOrRef_case() const {
+ return Node::JSObjectClassNameOrRefCase(_impl_._oneof_case_[1]);
+}
+inline Node::ScriptFilenameOrRefCase Node::ScriptFilenameOrRef_case() const {
+ return Node::ScriptFilenameOrRefCase(_impl_._oneof_case_[2]);
+}
+inline Node::DescriptiveTypeNameOrRefCase Node::descriptiveTypeNameOrRef_case() const {
+ return Node::DescriptiveTypeNameOrRefCase(_impl_._oneof_case_[3]);
+}
+// -------------------------------------------------------------------
+
+// Edge
+
+// optional uint64 referent = 1;
+inline bool Edge::_internal_has_referent() const {
+ bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0;
+ return value;
+}
+inline bool Edge::has_referent() const {
+ return _internal_has_referent();
+}
+inline void Edge::clear_referent() {
+ _impl_.referent_ = uint64_t{0u};
+ _impl_._has_bits_[0] &= ~0x00000001u;
+}
+inline uint64_t Edge::_internal_referent() const {
+ return _impl_.referent_;
+}
+inline uint64_t Edge::referent() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.referent)
+ return _internal_referent();
+}
+inline void Edge::_internal_set_referent(uint64_t value) {
+ _impl_._has_bits_[0] |= 0x00000001u;
+ _impl_.referent_ = value;
+}
+inline void Edge::set_referent(uint64_t value) {
+ _internal_set_referent(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.referent)
+}
+
+// bytes name = 2;
+inline bool Edge::_internal_has_name() const {
+ return EdgeNameOrRef_case() == kName;
+}
+inline bool Edge::has_name() const {
+ return _internal_has_name();
+}
+inline void Edge::set_has_name() {
+ _impl_._oneof_case_[0] = kName;
+}
+inline void Edge::clear_name() {
+ if (_internal_has_name()) {
+ _impl_.EdgeNameOrRef_.name_.Destroy();
+ clear_has_EdgeNameOrRef();
+ }
+}
+inline const std::string& Edge::name() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.name)
+ return _internal_name();
+}
+template <typename ArgT0, typename... ArgT>
+inline void Edge::set_name(ArgT0&& arg0, ArgT... args) {
+ if (!_internal_has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ _impl_.EdgeNameOrRef_.name_.InitDefault();
+ }
+ _impl_.EdgeNameOrRef_.name_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.name)
+}
+inline std::string* Edge::mutable_name() {
+ std::string* _s = _internal_mutable_name();
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Edge.name)
+ return _s;
+}
+inline const std::string& Edge::_internal_name() const {
+ if (_internal_has_name()) {
+ return _impl_.EdgeNameOrRef_.name_.Get();
+ }
+ return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited();
+}
+inline void Edge::_internal_set_name(const std::string& value) {
+ if (!_internal_has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ _impl_.EdgeNameOrRef_.name_.InitDefault();
+ }
+ _impl_.EdgeNameOrRef_.name_.Set(value, GetArenaForAllocation());
+}
+inline std::string* Edge::_internal_mutable_name() {
+ if (!_internal_has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ _impl_.EdgeNameOrRef_.name_.InitDefault();
+ }
+ return _impl_.EdgeNameOrRef_.name_.Mutable( GetArenaForAllocation());
+}
+inline std::string* Edge::release_name() {
+ // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Edge.name)
+ if (_internal_has_name()) {
+ clear_has_EdgeNameOrRef();
+ return _impl_.EdgeNameOrRef_.name_.Release();
+ } else {
+ return nullptr;
+ }
+}
+inline void Edge::set_allocated_name(std::string* name) {
+ if (has_EdgeNameOrRef()) {
+ clear_EdgeNameOrRef();
+ }
+ if (name != nullptr) {
+ set_has_name();
+ _impl_.EdgeNameOrRef_.name_.InitAllocated(name, GetArenaForAllocation());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Edge.name)
+}
+
+// uint64 nameRef = 3;
+inline bool Edge::_internal_has_nameref() const {
+ return EdgeNameOrRef_case() == kNameRef;
+}
+inline bool Edge::has_nameref() const {
+ return _internal_has_nameref();
+}
+inline void Edge::set_has_nameref() {
+ _impl_._oneof_case_[0] = kNameRef;
+}
+inline void Edge::clear_nameref() {
+ if (_internal_has_nameref()) {
+ _impl_.EdgeNameOrRef_.nameref_ = uint64_t{0u};
+ clear_has_EdgeNameOrRef();
+ }
+}
+inline uint64_t Edge::_internal_nameref() const {
+ if (_internal_has_nameref()) {
+ return _impl_.EdgeNameOrRef_.nameref_;
+ }
+ return uint64_t{0u};
+}
+inline void Edge::_internal_set_nameref(uint64_t value) {
+ if (!_internal_has_nameref()) {
+ clear_EdgeNameOrRef();
+ set_has_nameref();
+ }
+ _impl_.EdgeNameOrRef_.nameref_ = value;
+}
+inline uint64_t Edge::nameref() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.nameRef)
+ return _internal_nameref();
+}
+inline void Edge::set_nameref(uint64_t value) {
+ _internal_set_nameref(value);
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.nameRef)
+}
+
+inline bool Edge::has_EdgeNameOrRef() const {
+ return EdgeNameOrRef_case() != EDGENAMEORREF_NOT_SET;
+}
+inline void Edge::clear_has_EdgeNameOrRef() {
+ _impl_._oneof_case_[0] = EDGENAMEORREF_NOT_SET;
+}
+inline Edge::EdgeNameOrRefCase Edge::EdgeNameOrRef_case() const {
+ return Edge::EdgeNameOrRefCase(_impl_._oneof_case_[0]);
+}
+#ifdef __GNUC__
+ #pragma GCC diagnostic pop
+#endif // __GNUC__
+// -------------------------------------------------------------------
+
+// -------------------------------------------------------------------
+
+// -------------------------------------------------------------------
+
+// -------------------------------------------------------------------
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace protobuf
+} // namespace devtools
+} // namespace mozilla
+
+// @@protoc_insertion_point(global_scope)
+
+#include <google/protobuf/port_undef.inc>
+#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_CoreDump_2eproto
diff --git a/devtools/shared/heapsnapshot/CoreDump.proto b/devtools/shared/heapsnapshot/CoreDump.proto
new file mode 100644
index 0000000000..4d0bf27f01
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CoreDump.proto
@@ -0,0 +1,152 @@
+/* -*- Mode: protobuf; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=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/. */
+
+// # Core Dumps
+//
+// A core dump is a serialized snapshot of the heap graph. We serialize the heap
+// as a series of protobuf messages with each message prefixed by its Varint32
+// byte size so we can delimit individual protobuf messages (protobuf parsers
+// cannot determine where a message ends on their own).
+//
+// The first protobuf message is an instance of the `Metadata` message. All
+// subsequent messages will be instances of the `Node` message. The first of
+// these `Node` messages is the root node of the serialized heap graph. Here is
+// a diagram of our core dump format:
+//
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of following `Metadata` message. |
+// +-----------------------------------------------------------------------+
+// | message: The core dump `Metadata` message. |
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of the following `Node` message. |
+// +-----------------------------------------------------------------------+
+// | message: The first `Node` message. This is the root node. |
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of the following `Node` message. |
+// +-----------------------------------------------------------------------+
+// | message: A `Node` message. |
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of the following `Node` message. |
+// +-----------------------------------------------------------------------+
+// | message: A `Node` message. |
+// +-----------------------------------------------------------------------+
+// | . |
+// | . |
+// | . |
+// +-----------------------------------------------------------------------+
+//
+// Core dumps should always be written with a
+// `google::protobuf::io::GzipOutputStream` and read from a
+// `google::protobuf::io::GzipInputStream`.
+//
+// Note that all strings are de-duplicated. The first time the N^th unique
+// string is encountered, the full string is serialized. Subsequent times that
+// same string is encountered, it is referenced by N. This de-duplication
+// happens across string properties, not on a per-property basis. For example,
+// if the same K^th unique string is first used as an Edge::EdgeNameOrRef and
+// then as a StackFrame::Data::FunctionDisplayNameOrRef, the first will be the
+// actual string as the functionDisplayName oneof property, and the second will
+// be a reference to the first as the edgeNameRef oneof property whose value is
+// K.
+//
+// We would ordinarily abstract these de-duplicated strings with messages of
+// their own, but unfortunately, the protobuf compiler does not have a way to
+// inline a messsage within another message and the child message must be
+// referenced by pointer. This leads to extra mallocs that we wish to avoid.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package mozilla.devtools.protobuf;
+
+// A collection of metadata about this core dump.
+message Metadata {
+ // Number of microseconds since midnight (00:00:00) 1 January 1970 UTC.
+ optional uint64 timeStamp = 1;
+}
+
+// A serialized version of `JS::ubi::StackFrame`. Older parent frame tails are
+// de-duplicated to cut down on [de]serialization and size costs.
+message StackFrame {
+ oneof StackFrameType {
+ // This is the first time this stack frame has been serialized, and so
+ // here is all of its data.
+ Data data = 1;
+ // A reference to a stack frame that has already been serialized and has
+ // the given number as its id.
+ uint64 ref = 2;
+ }
+
+ message Data {
+ optional uint64 id = 1;
+ optional StackFrame parent = 2;
+ optional uint32 line = 3;
+ optional uint32 column = 4;
+
+ // De-duplicated two-byte string.
+ oneof SourceOrRef {
+ bytes source = 5;
+ uint64 sourceRef = 6;
+ }
+
+ // De-duplicated two-byte string.
+ oneof FunctionDisplayNameOrRef {
+ bytes functionDisplayName = 7;
+ uint64 functionDisplayNameRef = 8;
+ }
+
+ optional bool isSystem = 9;
+ optional bool isSelfHosted = 10;
+ }
+}
+
+// A serialized version of `JS::ubi::Node` and its outgoing edges.
+message Node {
+ optional uint64 id = 1;
+
+ // De-duplicated two-byte string.
+ oneof TypeNameOrRef {
+ bytes typeName = 2;
+ uint64 typeNameRef = 3;
+ }
+
+ optional uint64 size = 4;
+ repeated Edge edges = 5;
+ optional StackFrame allocationStack = 6;
+
+ // De-duplicated one-byte string.
+ oneof JSObjectClassNameOrRef {
+ bytes jsObjectClassName = 7;
+ uint64 jsObjectClassNameRef = 8;
+ }
+
+ // JS::ubi::CoarseType. Defaults to Other.
+ optional uint32 coarseType = 9 [default = 0];
+
+ // De-duplicated one-byte string.
+ oneof ScriptFilenameOrRef {
+ bytes scriptFilename = 10;
+ uint64 scriptFilenameRef = 11;
+ }
+
+ // De-duplicated one-byte string.
+ oneof descriptiveTypeNameOrRef {
+ bytes descriptiveTypeName = 12;
+ uint64 descriptiveTypeNameRef = 13;
+ }
+}
+
+// A serialized edge from the heap graph.
+message Edge {
+ optional uint64 referent = 1;
+
+ // De-duplicated two-byte string.
+ oneof EdgeNameOrRef {
+ bytes name = 2;
+ uint64 nameRef = 3;
+ }
+}
diff --git a/devtools/shared/heapsnapshot/DeserializedNode.cpp b/devtools/shared/heapsnapshot/DeserializedNode.cpp
new file mode 100644
index 0000000000..7de7d17faa
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DeserializedNode.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/DeserializedNode.h"
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "nsCRTGlue.h"
+
+namespace mozilla {
+namespace devtools {
+
+DeserializedEdge::DeserializedEdge(DeserializedEdge&& rhs) {
+ referent = rhs.referent;
+ name = rhs.name;
+}
+
+DeserializedEdge& DeserializedEdge::operator=(DeserializedEdge&& rhs) {
+ MOZ_ASSERT(&rhs != this);
+ this->~DeserializedEdge();
+ new (this) DeserializedEdge(std::move(rhs));
+ return *this;
+}
+
+JS::ubi::Node DeserializedNode::getEdgeReferent(const DeserializedEdge& edge) {
+ auto ptr = owner->nodes.lookup(edge.referent);
+ MOZ_ASSERT(ptr);
+
+ // `HashSets` only provide const access to their values, because mutating a
+ // value might change its hash, rendering it unfindable in the set.
+ // Unfortunately, the `ubi::Node` constructor requires a non-const pointer to
+ // its referent. However, the only aspect of a `DeserializedNode` we hash on
+ // is its id, which can't be changed via `ubi::Node`, so this cast can't cause
+ // the trouble `HashSet` is concerned a non-const reference would cause.
+ return JS::ubi::Node(const_cast<DeserializedNode*>(&*ptr));
+}
+
+JS::ubi::StackFrame DeserializedStackFrame::getParentStackFrame() const {
+ MOZ_ASSERT(parent.isSome());
+ auto ptr = owner->frames.lookup(parent.ref());
+ MOZ_ASSERT(ptr);
+ // See above comment in DeserializedNode::getEdgeReferent about why this
+ // const_cast is needed and safe.
+ return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
+}
+
+} // namespace devtools
+} // namespace mozilla
+
+namespace JS {
+namespace ubi {
+
+const char16_t Concrete<DeserializedNode>::concreteTypeName[] =
+ u"mozilla::devtools::DeserializedNode";
+
+const char16_t* Concrete<DeserializedNode>::typeName() const {
+ return get().typeName;
+}
+
+Node::Size Concrete<DeserializedNode>::size(
+ mozilla::MallocSizeOf mallocSizeof) const {
+ return get().size;
+}
+
+class DeserializedEdgeRange : public EdgeRange {
+ DeserializedNode* node;
+ Edge currentEdge;
+ size_t i;
+
+ void settle() {
+ if (i >= node->edges.length()) {
+ front_ = nullptr;
+ return;
+ }
+
+ auto& edge = node->edges[i];
+ auto referent = node->getEdgeReferent(edge);
+ currentEdge = Edge(edge.name ? NS_xstrdup(edge.name) : nullptr, referent);
+ front_ = &currentEdge;
+ }
+
+ public:
+ explicit DeserializedEdgeRange(DeserializedNode& node) : node(&node), i(0) {
+ settle();
+ }
+
+ void popFront() override {
+ i++;
+ settle();
+ }
+};
+
+StackFrame Concrete<DeserializedNode>::allocationStack() const {
+ MOZ_ASSERT(hasAllocationStack());
+ auto id = get().allocationStack.ref();
+ auto ptr = get().owner->frames.lookup(id);
+ MOZ_ASSERT(ptr);
+ // See above comment in DeserializedNode::getEdgeReferent about why this
+ // const_cast is needed and safe.
+ return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
+}
+
+js::UniquePtr<EdgeRange> Concrete<DeserializedNode>::edges(JSContext* cx,
+ bool) const {
+ js::UniquePtr<DeserializedEdgeRange> range(
+ js_new<DeserializedEdgeRange>(get()));
+
+ if (!range) return nullptr;
+
+ return js::UniquePtr<EdgeRange>(range.release());
+}
+
+StackFrame ConcreteStackFrame<DeserializedStackFrame>::parent() const {
+ return get().parent.isNothing() ? StackFrame() : get().getParentStackFrame();
+}
+
+bool ConcreteStackFrame<DeserializedStackFrame>::constructSavedFrameStack(
+ JSContext* cx, JS::MutableHandle<JSObject*> outSavedFrameStack) const {
+ StackFrame f(&get());
+ return ConstructSavedFrameStackSlow(cx, f, outSavedFrameStack);
+}
+
+} // namespace ubi
+} // namespace JS
diff --git a/devtools/shared/heapsnapshot/DeserializedNode.h b/devtools/shared/heapsnapshot/DeserializedNode.h
new file mode 100644
index 0000000000..cc831b4dc5
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DeserializedNode.h
@@ -0,0 +1,308 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_DeserializedNode__
+#define mozilla_devtools_DeserializedNode__
+
+#include <utility>
+
+#include "js/ColumnNumber.h" // JS::TaggedColumnNumberOneOrigin
+#include "js/UbiNode.h"
+#include "js/UniquePtr.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Vector.h"
+#include "mozilla/devtools/CoreDump.pb.h"
+
+// `Deserialized{Node,Edge}` translate protobuf messages from our core dump
+// format into structures we can rely upon for implementing `JS::ubi::Node`
+// specializations on top of. All of the properties of the protobuf messages are
+// optional for future compatibility, and this is the layer where we validate
+// that the properties that do actually exist in any given message fulfill our
+// semantic requirements.
+//
+// Both `DeserializedNode` and `DeserializedEdge` are always owned by a
+// `HeapSnapshot` instance, and their lifetimes must not extend after that of
+// their owning `HeapSnapshot`.
+
+namespace mozilla {
+namespace devtools {
+
+class HeapSnapshot;
+
+using NodeId = uint64_t;
+using StackFrameId = uint64_t;
+
+// A `DeserializedEdge` represents an edge in the heap graph pointing to the
+// node with id equal to `DeserializedEdge::referent` that we deserialized from
+// a core dump.
+struct DeserializedEdge {
+ NodeId referent;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char16_t* name;
+
+ explicit DeserializedEdge(NodeId referent, const char16_t* edgeName = nullptr)
+ : referent(referent), name(edgeName) {}
+ DeserializedEdge(DeserializedEdge&& rhs);
+ DeserializedEdge& operator=(DeserializedEdge&& rhs);
+
+ private:
+ DeserializedEdge(const DeserializedEdge&) = delete;
+ DeserializedEdge& operator=(const DeserializedEdge&) = delete;
+};
+
+// A `DeserializedNode` is a node in the heap graph that we deserialized from a
+// core dump.
+struct DeserializedNode {
+ using EdgeVector = Vector<DeserializedEdge>;
+ using UniqueStringPtr = UniquePtr<char16_t[]>;
+
+ NodeId id;
+ JS::ubi::CoarseType coarseType;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char16_t* typeName;
+ uint64_t size;
+ EdgeVector edges;
+ Maybe<StackFrameId> allocationStack;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char* jsObjectClassName;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char* scriptFilename;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char16_t* descriptiveTypeName;
+ // A weak pointer to this node's owning `HeapSnapshot`. Safe without
+ // AddRef'ing because this node's lifetime is equal to that of its owner.
+ HeapSnapshot* owner;
+
+ DeserializedNode(NodeId id, JS::ubi::CoarseType coarseType,
+ const char16_t* typeName, uint64_t size, EdgeVector&& edges,
+ const Maybe<StackFrameId>& allocationStack,
+ const char* className, const char* filename,
+ const char16_t* descriptiveName, HeapSnapshot& owner)
+ : id(id),
+ coarseType(coarseType),
+ typeName(typeName),
+ size(size),
+ edges(std::move(edges)),
+ allocationStack(allocationStack),
+ jsObjectClassName(className),
+ scriptFilename(filename),
+ descriptiveTypeName(descriptiveName),
+ owner(&owner) {}
+ virtual ~DeserializedNode() {}
+
+ DeserializedNode(DeserializedNode&& rhs)
+ : id(rhs.id),
+ coarseType(rhs.coarseType),
+ typeName(rhs.typeName),
+ size(rhs.size),
+ edges(std::move(rhs.edges)),
+ allocationStack(rhs.allocationStack),
+ jsObjectClassName(rhs.jsObjectClassName),
+ scriptFilename(rhs.scriptFilename),
+ descriptiveTypeName(rhs.descriptiveTypeName),
+ owner(rhs.owner) {}
+
+ DeserializedNode& operator=(DeserializedNode&& rhs) {
+ MOZ_ASSERT(&rhs != this);
+ this->~DeserializedNode();
+ new (this) DeserializedNode(std::move(rhs));
+ return *this;
+ }
+
+ // Get a borrowed reference to the given edge's referent. This method is
+ // virtual to provide a hook for gmock and gtest.
+ virtual JS::ubi::Node getEdgeReferent(const DeserializedEdge& edge);
+
+ struct HashPolicy;
+
+ protected:
+ // This is only for use with `MockDeserializedNode` in testing.
+ DeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
+ : id(id),
+ coarseType(JS::ubi::CoarseType::Other),
+ typeName(typeName),
+ size(size),
+ edges(),
+ allocationStack(Nothing()),
+ jsObjectClassName(nullptr),
+ scriptFilename(nullptr),
+ descriptiveTypeName(nullptr),
+ owner(nullptr) {}
+
+ private:
+ DeserializedNode(const DeserializedNode&) = delete;
+ DeserializedNode& operator=(const DeserializedNode&) = delete;
+};
+
+static inline js::HashNumber hashIdDerivedFromPtr(uint64_t id) {
+ return mozilla::HashGeneric(id);
+}
+
+struct DeserializedNode::HashPolicy {
+ using Lookup = NodeId;
+
+ static js::HashNumber hash(const Lookup& lookup) {
+ return hashIdDerivedFromPtr(lookup);
+ }
+
+ static bool match(const DeserializedNode& existing, const Lookup& lookup) {
+ return existing.id == lookup;
+ }
+};
+
+// A `DeserializedStackFrame` is a stack frame referred to by a thing in the
+// heap graph that we deserialized from a core dump.
+struct DeserializedStackFrame {
+ StackFrameId id;
+ Maybe<StackFrameId> parent;
+ uint32_t line;
+ JS::TaggedColumnNumberOneOrigin column;
+ // Borrowed references to strings owned by this DeserializedStackFrame's
+ // owning HeapSnapshot.
+ const char16_t* source;
+ const char16_t* functionDisplayName;
+ bool isSystem;
+ bool isSelfHosted;
+ // A weak pointer to this frame's owning `HeapSnapshot`. Safe without
+ // AddRef'ing because this frame's lifetime is equal to that of its owner.
+ HeapSnapshot* owner;
+
+ explicit DeserializedStackFrame(
+ StackFrameId id, const Maybe<StackFrameId>& parent, uint32_t line,
+ JS::TaggedColumnNumberOneOrigin column, const char16_t* source,
+ const char16_t* functionDisplayName, bool isSystem, bool isSelfHosted,
+ HeapSnapshot& owner)
+ : id(id),
+ parent(parent),
+ line(line),
+ column(column),
+ source(source),
+ functionDisplayName(functionDisplayName),
+ isSystem(isSystem),
+ isSelfHosted(isSelfHosted),
+ owner(&owner) {
+ MOZ_ASSERT(source);
+ }
+
+ JS::ubi::StackFrame getParentStackFrame() const;
+
+ struct HashPolicy;
+
+ protected:
+ // This is exposed only for MockDeserializedStackFrame in the gtests.
+ explicit DeserializedStackFrame()
+ : id(0),
+ parent(Nothing()),
+ line(0),
+ source(nullptr),
+ functionDisplayName(nullptr),
+ isSystem(false),
+ isSelfHosted(false),
+ owner(nullptr){};
+};
+
+struct DeserializedStackFrame::HashPolicy {
+ using Lookup = StackFrameId;
+
+ static js::HashNumber hash(const Lookup& lookup) {
+ return hashIdDerivedFromPtr(lookup);
+ }
+
+ static bool match(const DeserializedStackFrame& existing,
+ const Lookup& lookup) {
+ return existing.id == lookup;
+ }
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+namespace JS {
+namespace ubi {
+
+using mozilla::devtools::DeserializedNode;
+using mozilla::devtools::DeserializedStackFrame;
+
+template <>
+class Concrete<DeserializedNode> : public Base {
+ protected:
+ explicit Concrete(DeserializedNode* ptr) : Base(ptr) {}
+ DeserializedNode& get() const { return *static_cast<DeserializedNode*>(ptr); }
+
+ public:
+ static void construct(void* storage, DeserializedNode* ptr) {
+ new (storage) Concrete(ptr);
+ }
+
+ CoarseType coarseType() const final { return get().coarseType; }
+ Id identifier() const override { return get().id; }
+ bool isLive() const override { return false; }
+ const char16_t* typeName() const override;
+ Node::Size size(mozilla::MallocSizeOf mallocSizeof) const override;
+ const char* jsObjectClassName() const override {
+ return get().jsObjectClassName;
+ }
+ const char* scriptFilename() const final { return get().scriptFilename; }
+ const char16_t* descriptiveTypeName() const override {
+ return get().descriptiveTypeName;
+ }
+
+ bool hasAllocationStack() const override {
+ return get().allocationStack.isSome();
+ }
+ StackFrame allocationStack() const override;
+
+ // We ignore the `bool wantNames` parameter because we can't control whether
+ // the core dump was serialized with edge names or not.
+ js::UniquePtr<EdgeRange> edges(JSContext* cx, bool) const override;
+
+ static const char16_t concreteTypeName[];
+};
+
+template <>
+class ConcreteStackFrame<DeserializedStackFrame> : public BaseStackFrame {
+ protected:
+ explicit ConcreteStackFrame(DeserializedStackFrame* ptr)
+ : BaseStackFrame(ptr) {}
+
+ DeserializedStackFrame& get() const {
+ return *static_cast<DeserializedStackFrame*>(ptr);
+ }
+
+ public:
+ static void construct(void* storage, DeserializedStackFrame* ptr) {
+ new (storage) ConcreteStackFrame(ptr);
+ }
+
+ uint64_t identifier() const override { return get().id; }
+ uint32_t line() const override { return get().line; }
+ JS::TaggedColumnNumberOneOrigin column() const override {
+ return get().column;
+ }
+ bool isSystem() const override { return get().isSystem; }
+ bool isSelfHosted(JSContext* cx) const override { return get().isSelfHosted; }
+ void trace(JSTracer* trc) override {}
+ AtomOrTwoByteChars source() const override {
+ return AtomOrTwoByteChars(get().source);
+ }
+ uint32_t sourceId() const override {
+ // Source IDs are local to their host process and are not serialized.
+ return 0;
+ }
+ AtomOrTwoByteChars functionDisplayName() const override {
+ return AtomOrTwoByteChars(get().functionDisplayName);
+ }
+
+ StackFrame parent() const override;
+ bool constructSavedFrameStack(
+ JSContext* cx,
+ JS::MutableHandle<JSObject*> outSavedFrameStack) const override;
+};
+
+} // namespace ubi
+} // namespace JS
+
+#endif // mozilla_devtools_DeserializedNode__
diff --git a/devtools/shared/heapsnapshot/DominatorTree.cpp b/devtools/shared/heapsnapshot/DominatorTree.cpp
new file mode 100644
index 0000000000..065db13576
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DominatorTree.cpp
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/DominatorTree.h"
+#include "mozilla/dom/DominatorTreeBinding.h"
+#include "mozilla/ErrorResult.h"
+
+namespace mozilla {
+namespace devtools {
+
+dom::Nullable<uint64_t> DominatorTree::GetRetainedSize(uint64_t aNodeId,
+ ErrorResult& aRv) {
+ JS::ubi::Node::Id id(aNodeId);
+ auto node = mHeapSnapshot->getNodeById(id);
+ if (node.isNothing()) return dom::Nullable<uint64_t>();
+
+ auto mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
+ JS::ubi::Node::Size size = 0;
+ if (!mDominatorTree.getRetainedSize(*node, mallocSizeOf, size)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return dom::Nullable<uint64_t>();
+ }
+
+ MOZ_ASSERT(size != 0,
+ "The node should not have been unknown since we got it from the "
+ "heap snapshot.");
+ return dom::Nullable<uint64_t>(size);
+}
+
+struct NodeAndRetainedSize {
+ JS::ubi::Node mNode;
+ JS::ubi::Node::Size mSize;
+
+ NodeAndRetainedSize(const JS::ubi::Node& aNode, JS::ubi::Node::Size aSize)
+ : mNode(aNode), mSize(aSize) {}
+
+ struct Comparator {
+ static bool Equals(const NodeAndRetainedSize& aLhs,
+ const NodeAndRetainedSize& aRhs) {
+ return aLhs.mSize == aRhs.mSize;
+ }
+
+ static bool LessThan(const NodeAndRetainedSize& aLhs,
+ const NodeAndRetainedSize& aRhs) {
+ // Use > because we want to sort from greatest to least retained size.
+ return aLhs.mSize > aRhs.mSize;
+ }
+ };
+};
+
+void DominatorTree::GetImmediatelyDominated(
+ uint64_t aNodeId, dom::Nullable<nsTArray<uint64_t>>& aOutResult,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aOutResult.IsNull());
+
+ JS::ubi::Node::Id id(aNodeId);
+ Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id);
+ if (node.isNothing()) return;
+
+ // Get all immediately dominated nodes and their retained sizes.
+ MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
+ Maybe<JS::ubi::DominatorTree::DominatedSetRange> range =
+ mDominatorTree.getDominatedSet(*node);
+ MOZ_ASSERT(
+ range.isSome(),
+ "The node should be known, since we got it from the heap snapshot.");
+ size_t length = range->length();
+ nsTArray<NodeAndRetainedSize> dominatedNodes(length);
+ for (const JS::ubi::Node& dominatedNode : *range) {
+ JS::ubi::Node::Size retainedSize = 0;
+ if (NS_WARN_IF(!mDominatorTree.getRetainedSize(dominatedNode, mallocSizeOf,
+ retainedSize))) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ MOZ_ASSERT(retainedSize != 0,
+ "retainedSize should not be zero since we know the node is in "
+ "the dominator tree.");
+
+ dominatedNodes.AppendElement(
+ NodeAndRetainedSize(dominatedNode, retainedSize));
+ }
+
+ // Sort them by retained size.
+ NodeAndRetainedSize::Comparator comparator;
+ dominatedNodes.Sort(comparator);
+
+ // Fill the result with the nodes' ids.
+ JS::ubi::Node root = mDominatorTree.root();
+ aOutResult.SetValue(nsTArray<uint64_t>(length));
+ for (const NodeAndRetainedSize& entry : dominatedNodes) {
+ // The root dominates itself, but we don't want to expose that to JS.
+ if (entry.mNode == root) continue;
+
+ aOutResult.Value().AppendElement(entry.mNode.identifier());
+ }
+}
+
+dom::Nullable<uint64_t> DominatorTree::GetImmediateDominator(
+ uint64_t aNodeId) const {
+ JS::ubi::Node::Id id(aNodeId);
+ Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id);
+ if (node.isNothing()) return dom::Nullable<uint64_t>();
+
+ JS::ubi::Node dominator = mDominatorTree.getImmediateDominator(*node);
+ if (!dominator || dominator == *node) return dom::Nullable<uint64_t>();
+
+ return dom::Nullable<uint64_t>(dominator.identifier());
+}
+
+/*** Cycle Collection Boilerplate
+ * *****************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DominatorTree, mParent, mHeapSnapshot)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DominatorTree)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DominatorTree)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DominatorTree)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* virtual */
+JSObject* DominatorTree::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::DominatorTree_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/DominatorTree.h b/devtools/shared/heapsnapshot/DominatorTree.h
new file mode 100644
index 0000000000..fb3275cf23
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DominatorTree.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_DominatorTree__
+#define mozilla_devtools_DominatorTree__
+
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/RefCounted.h"
+#include "js/UbiNodeDominatorTree.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace devtools {
+
+class DominatorTree final : public nsISupports, public nsWrapperCache {
+ protected:
+ nsCOMPtr<nsISupports> mParent;
+
+ virtual ~DominatorTree() {}
+
+ private:
+ JS::ubi::DominatorTree mDominatorTree;
+ RefPtr<HeapSnapshot> mHeapSnapshot;
+
+ public:
+ explicit DominatorTree(JS::ubi::DominatorTree&& aDominatorTree,
+ HeapSnapshot* aHeapSnapshot, nsISupports* aParent)
+ : mParent(aParent),
+ mDominatorTree(std::move(aDominatorTree)),
+ mHeapSnapshot(aHeapSnapshot) {
+ MOZ_ASSERT(aParent);
+ MOZ_ASSERT(aHeapSnapshot);
+ };
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS;
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DominatorTree);
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // readonly attribute NodeId root
+ uint64_t Root() const { return mDominatorTree.root().identifier(); }
+
+ // [Throws] NodeSize getRetainedSize(NodeId node)
+ dom::Nullable<uint64_t> GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv);
+
+ // [Throws] sequence<NodeId>? getImmediatelyDominated(NodeId node);
+ void GetImmediatelyDominated(uint64_t aNodeId,
+ dom::Nullable<nsTArray<uint64_t>>& aOutDominated,
+ ErrorResult& aRv);
+
+ // NodeId? getImmediateDominator(NodeId node);
+ dom::Nullable<uint64_t> GetImmediateDominator(uint64_t aNodeId) const;
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_DominatorTree__
diff --git a/devtools/shared/heapsnapshot/DominatorTreeNode.js b/devtools/shared/heapsnapshot/DominatorTreeNode.js
new file mode 100644
index 0000000000..9fe5cf1032
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DominatorTreeNode.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/. */
+"use strict";
+
+const {
+ immutableUpdate,
+} = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
+const {
+ Visitor,
+ walk,
+} = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+const {
+ deduplicatePaths,
+} = require("resource://devtools/shared/heapsnapshot/shortest-paths.js");
+
+const DEFAULT_MAX_DEPTH = 4;
+const DEFAULT_MAX_SIBLINGS = 15;
+const DEFAULT_MAX_NUM_PATHS = 5;
+
+/**
+ * A single node in a dominator tree.
+ *
+ * @param {NodeId} nodeId
+ * @param {NodeSize} retainedSize
+ */
+function DominatorTreeNode(nodeId, label, shallowSize, retainedSize) {
+ // The id of this node.
+ this.nodeId = nodeId;
+
+ // The label structure generated by describing the given node.
+ this.label = label;
+
+ // The shallow size of this node.
+ this.shallowSize = shallowSize;
+
+ // The retained size of this node.
+ this.retainedSize = retainedSize;
+
+ // The id of this node's parent or undefined if this node is the root.
+ this.parentId = undefined;
+
+ // An array of immediately dominated child `DominatorTreeNode`s, or undefined.
+ this.children = undefined;
+
+ // An object of the form returned by `deduplicatePaths`, encoding the set of
+ // the N shortest retaining paths for this node as a graph.
+ this.shortestPaths = undefined;
+
+ // True iff the `children` property does not contain every immediately
+ // dominated node.
+ //
+ // * If children is an array and this property is true: the array does not
+ // contain the complete set of immediately dominated children.
+ // * If children is an array and this property is false: the array contains
+ // the complete set of immediately dominated children.
+ // * If children is undefined and this property is true: there exist
+ // immediately dominated children for this node, but they have not been
+ // loaded yet.
+ // * If children is undefined and this property is false: this node does not
+ // dominate any others and therefore has no children.
+ this.moreChildrenAvailable = true;
+}
+
+DominatorTreeNode.prototype = null;
+
+module.exports = DominatorTreeNode;
+
+/**
+ * Add `child` to the `parent`'s set of children.
+ *
+ * @param {DominatorTreeNode} parent
+ * @param {DominatorTreeNode} child
+ */
+DominatorTreeNode.addChild = function (parent, child) {
+ if (parent.children === undefined) {
+ parent.children = [];
+ }
+
+ parent.children.push(child);
+ child.parentId = parent.nodeId;
+};
+
+/**
+ * A Visitor that is used to generate a label for a node in the heap snapshot
+ * and get its shallow size as well while we are at it.
+ */
+function LabelAndShallowSizeVisitor() {
+ // As we walk the description, we accumulate edges in this array.
+ this._labelPieces = [];
+
+ // Once we reach the non-zero count leaf node in the description, we move the
+ // labelPieces here to signify that we no longer need to accumulate edges.
+ this._label = undefined;
+
+ // Once we reach the non-zero count leaf node in the description, we grab the
+ // shallow size and place it here.
+ this._shallowSize = 0;
+}
+
+DominatorTreeNode.LabelAndShallowSizeVisitor = LabelAndShallowSizeVisitor;
+
+LabelAndShallowSizeVisitor.prototype = Object.create(Visitor);
+
+/**
+ * @overrides Visitor.prototype.enter
+ */
+LabelAndShallowSizeVisitor.prototype.enter = function (
+ breakdown,
+ report,
+ edge
+) {
+ if (this._labelPieces && edge) {
+ this._labelPieces.push(edge);
+ }
+};
+
+/**
+ * @overrides Visitor.prototype.exit
+ */
+LabelAndShallowSizeVisitor.prototype.exit = function (breakdown, report, edge) {
+ if (this._labelPieces && edge) {
+ this._labelPieces.pop();
+ }
+};
+
+/**
+ * @overrides Visitor.prototype.count
+ */
+LabelAndShallowSizeVisitor.prototype.count = function (
+ breakdown,
+ report,
+ edge
+) {
+ if (report.count === 0) {
+ return;
+ }
+
+ this._label = this._labelPieces;
+ this._labelPieces = undefined;
+
+ this._shallowSize = report.bytes;
+};
+
+/**
+ * Get the generated label structure accumulated by this visitor.
+ *
+ * @returns {Object}
+ */
+LabelAndShallowSizeVisitor.prototype.label = function () {
+ return this._label;
+};
+
+/**
+ * Get the shallow size of the node this visitor visited.
+ *
+ * @returns {Number}
+ */
+LabelAndShallowSizeVisitor.prototype.shallowSize = function () {
+ return this._shallowSize;
+};
+
+/**
+ * Generate a label structure for the node with the given id and grab its
+ * shallow size.
+ *
+ * What is a "label" structure? HeapSnapshot.describeNode essentially takes a
+ * census of a single node rather than the whole heap graph. The resulting
+ * report has only one count leaf that is non-zero. The label structure is the
+ * path in this report from the root to the non-zero count leaf.
+ *
+ * @param {Number} nodeId
+ * @param {HeapSnapshot} snapshot
+ * @param {Object} breakdown
+ *
+ * @returns {Object}
+ * An object with the following properties:
+ * - {Number} shallowSize
+ * - {Object} label
+ */
+DominatorTreeNode.getLabelAndShallowSize = function (
+ nodeId,
+ snapshot,
+ breakdown
+) {
+ const description = snapshot.describeNode(breakdown, nodeId);
+
+ const visitor = new LabelAndShallowSizeVisitor();
+ walk(breakdown, description, visitor);
+
+ return {
+ label: visitor.label(),
+ shallowSize: visitor.shallowSize(),
+ };
+};
+
+/**
+ * Do a partial traversal of the given dominator tree and convert it into a tree
+ * of `DominatorTreeNode`s. Dominator trees have a node for every node in the
+ * snapshot's heap graph, so we must not allocate a JS object for every node. It
+ * would be way too many and the node count is effectively unbounded.
+ *
+ * Go no deeper down the tree than `maxDepth` and only consider at most
+ * `maxSiblings` within any single node's children.
+ *
+ * @param {DominatorTree} dominatorTree
+ * @param {HeapSnapshot} snapshot
+ * @param {Object} breakdown
+ * @param {Number} maxDepth
+ * @param {Number} maxSiblings
+ *
+ * @returns {DominatorTreeNode}
+ */
+DominatorTreeNode.partialTraversal = function (
+ dominatorTree,
+ snapshot,
+ breakdown,
+ maxDepth = DEFAULT_MAX_DEPTH,
+ maxSiblings = DEFAULT_MAX_SIBLINGS
+) {
+ function dfs(nodeId, depth) {
+ const { label, shallowSize } = DominatorTreeNode.getLabelAndShallowSize(
+ nodeId,
+ snapshot,
+ breakdown
+ );
+ const retainedSize = dominatorTree.getRetainedSize(nodeId);
+ const node = new DominatorTreeNode(
+ nodeId,
+ label,
+ shallowSize,
+ retainedSize
+ );
+ const childNodeIds = dominatorTree.getImmediatelyDominated(nodeId);
+
+ const newDepth = depth + 1;
+ if (newDepth < maxDepth) {
+ const endIdx = Math.min(childNodeIds.length, maxSiblings);
+ for (let i = 0; i < endIdx; i++) {
+ DominatorTreeNode.addChild(node, dfs(childNodeIds[i], newDepth));
+ }
+ node.moreChildrenAvailable = endIdx < childNodeIds.length;
+ } else {
+ node.moreChildrenAvailable = !!childNodeIds.length;
+ }
+
+ return node;
+ }
+
+ return dfs(dominatorTree.root, 0);
+};
+
+/**
+ * Insert more children into the given (partially complete) dominator tree.
+ *
+ * The tree is updated in an immutable and persistent manner: a new tree is
+ * returned, but all unmodified subtrees (which is most) are shared with the
+ * original tree. Only the modified nodes are re-allocated.
+ *
+ * @param {DominatorTreeNode} tree
+ * @param {Array<NodeId>} path
+ * @param {Array<DominatorTreeNode>} newChildren
+ * @param {Boolean} moreChildrenAvailable
+ *
+ * @returns {DominatorTreeNode}
+ */
+DominatorTreeNode.insert = function (
+ nodeTree,
+ path,
+ newChildren,
+ moreChildrenAvailable
+) {
+ function insert(tree, i) {
+ if (tree.nodeId !== path[i]) {
+ return tree;
+ }
+
+ if (i == path.length - 1) {
+ return immutableUpdate(tree, {
+ children: (tree.children || []).concat(newChildren),
+ moreChildrenAvailable,
+ });
+ }
+
+ return tree.children
+ ? immutableUpdate(tree, {
+ children: tree.children.map(c => insert(c, i + 1)),
+ })
+ : tree;
+ }
+
+ return insert(nodeTree, 0);
+};
+
+/**
+ * Get the new canonical node with the given `id` in `tree` that exists along
+ * `path`. If there is no such node along `path`, return null.
+ *
+ * This is useful if we have a reference to a now-outdated DominatorTreeNode due
+ * to a recent call to DominatorTreeNode.insert and want to get the up-to-date
+ * version. We don't have to walk the whole tree: if there is an updated version
+ * of the node then it *must* be along the path.
+ *
+ * @param {NodeId} id
+ * @param {DominatorTreeNode} tree
+ * @param {Array<NodeId>} path
+ *
+ * @returns {DominatorTreeNode|null}
+ */
+DominatorTreeNode.getNodeByIdAlongPath = function (id, tree, path) {
+ function find(node, i) {
+ if (!node || node.nodeId !== path[i]) {
+ return null;
+ }
+
+ if (node.nodeId === id) {
+ return node;
+ }
+
+ if (i === path.length - 1 || !node.children) {
+ return null;
+ }
+
+ const nextId = path[i + 1];
+ return find(
+ node.children.find(c => c.nodeId === nextId),
+ i + 1
+ );
+ }
+
+ return find(tree, 0);
+};
+
+/**
+ * Find the shortest retaining paths for the given set of DominatorTreeNodes,
+ * and populate each node's `shortestPaths` property with them in place.
+ *
+ * @param {HeapSnapshot} snapshot
+ * @param {Object} breakdown
+ * @param {NodeId} start
+ * @param {Array<DominatorTreeNode>} treeNodes
+ * @param {Number} maxNumPaths
+ */
+DominatorTreeNode.attachShortestPaths = function (
+ snapshot,
+ breakdown,
+ start,
+ treeNodes,
+ maxNumPaths = DEFAULT_MAX_NUM_PATHS
+) {
+ const idToTreeNode = new Map();
+ const targets = [];
+ for (const node of treeNodes) {
+ const id = node.nodeId;
+ idToTreeNode.set(id, node);
+ targets.push(id);
+ }
+
+ const shortestPaths = snapshot.computeShortestPaths(
+ start,
+ targets,
+ maxNumPaths
+ );
+
+ for (const [target, paths] of shortestPaths) {
+ const deduped = deduplicatePaths(target, paths);
+ deduped.nodes = deduped.nodes.map(id => {
+ const { label } = DominatorTreeNode.getLabelAndShallowSize(
+ id,
+ snapshot,
+ breakdown
+ );
+ return { id, label };
+ });
+
+ idToTreeNode.get(target).shortestPaths = deduped;
+ }
+};
diff --git a/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp
new file mode 100644
index 0000000000..651d7b9496
--- /dev/null
+++ b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/FileDescriptorOutputStream.h"
+#include "private/pprio.h"
+
+namespace mozilla {
+namespace devtools {
+
+/* static */
+already_AddRefed<FileDescriptorOutputStream> FileDescriptorOutputStream::Create(
+ const ipc::FileDescriptor& fileDescriptor) {
+ if (NS_WARN_IF(!fileDescriptor.IsValid())) return nullptr;
+
+ auto rawFD = fileDescriptor.ClonePlatformHandle();
+ PRFileDesc* prfd = PR_ImportFile(PROsfd(rawFD.release()));
+ if (NS_WARN_IF(!prfd)) return nullptr;
+
+ RefPtr<FileDescriptorOutputStream> stream =
+ new FileDescriptorOutputStream(prfd);
+ return stream.forget();
+}
+
+NS_IMPL_ISUPPORTS(FileDescriptorOutputStream, nsIOutputStream);
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::Close() {
+ // Repeatedly closing is idempotent.
+ if (!fd) return NS_OK;
+
+ if (PR_Close(fd) != PR_SUCCESS) return NS_ERROR_FAILURE;
+ fd = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::StreamStatus() {
+ return fd ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::Write(const char* buf, uint32_t count,
+ uint32_t* retval) {
+ if (NS_WARN_IF(!fd)) return NS_ERROR_FAILURE;
+
+ auto written = PR_Write(fd, buf, count);
+ if (written < 0) return NS_ERROR_FAILURE;
+ *retval = written;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::Flush() {
+ if (NS_WARN_IF(!fd)) return NS_ERROR_FAILURE;
+
+ return PR_Sync(fd) == PR_SUCCESS ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::WriteFrom(nsIInputStream* fromStream,
+ uint32_t count, uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::WriteSegments(nsReadSegmentFun reader,
+ void* closure, uint32_t count,
+ uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::IsNonBlocking(bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h
new file mode 100644
index 0000000000..5d8f31424c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_FileDescriptorOutputStream_h
+#define mozilla_devtools_FileDescriptorOutputStream_h
+
+#include <prio.h>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "nsIOutputStream.h"
+
+namespace mozilla {
+namespace devtools {
+
+class FileDescriptorOutputStream final : public nsIOutputStream {
+ private:
+ PRFileDesc* fd;
+
+ public:
+ static already_AddRefed<FileDescriptorOutputStream> Create(
+ const ipc::FileDescriptor& fileDescriptor);
+
+ private:
+ explicit FileDescriptorOutputStream(PRFileDesc* prfd) : fd(prfd) {}
+
+ virtual ~FileDescriptorOutputStream() {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOUTPUTSTREAM
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_FileDescriptorOutputStream_h
diff --git a/devtools/shared/heapsnapshot/HeapAnalyses.worker.js b/devtools/shared/heapsnapshot/HeapAnalyses.worker.js
new file mode 100644
index 0000000000..0f61d99a94
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapAnalyses.worker.js
@@ -0,0 +1,336 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is a worker which reads offline heap snapshots into memory and performs
+// heavyweight analyses on them without blocking the main thread. A
+// HeapAnalysesWorker is owned and communicated with by a HeapAnalysesClient
+// instance. See HeapAnalysesClient.js.
+
+"use strict";
+
+/* import-globals-from /toolkit/components/workerloader/require.js */
+importScripts("resource://gre/modules/workers/require.js");
+/* import-globals-from ../worker/helper.js */
+importScripts("resource://devtools/shared/worker/helper.js");
+const {
+ censusReportToCensusTreeNode,
+} = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
+const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
+const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+
+const DEFAULT_START_INDEX = 0;
+const DEFAULT_MAX_COUNT = 50;
+
+/**
+ * The set of HeapSnapshot instances this worker has read into memory. Keyed by
+ * snapshot file path.
+ */
+const snapshots = Object.create(null);
+
+/**
+ * The set of `DominatorTree`s that have been computed, mapped by their id (aka
+ * the index into this array).
+ *
+ * @see /dom/webidl/DominatorTree.webidl
+ */
+const dominatorTrees = [];
+
+/**
+ * The i^th HeapSnapshot in this array is the snapshot used to generate the i^th
+ * dominator tree in `dominatorTrees` above. This lets us map from a dominator
+ * tree id to the snapshot it came from.
+ */
+const dominatorTreeSnapshots = [];
+
+/**
+ * @see HeapAnalysesClient.prototype.readHeapSnapshot
+ */
+workerHelper.createTask(self, "readHeapSnapshot", ({ snapshotFilePath }) => {
+ snapshots[snapshotFilePath] = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ return true;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.deleteHeapSnapshot
+ */
+workerHelper.createTask(self, "deleteHeapSnapshot", ({ snapshotFilePath }) => {
+ const snapshot = snapshots[snapshotFilePath];
+ if (!snapshot) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+
+ snapshots[snapshotFilePath] = undefined;
+
+ const dominatorTreeId = dominatorTreeSnapshots.indexOf(snapshot);
+ if (dominatorTreeId != -1) {
+ dominatorTreeSnapshots[dominatorTreeId] = undefined;
+ dominatorTrees[dominatorTreeId] = undefined;
+ }
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.takeCensus
+ */
+workerHelper.createTask(
+ self,
+ "takeCensus",
+ ({ snapshotFilePath, censusOptions, requestOptions }) => {
+ if (!snapshots[snapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+
+ let report = snapshots[snapshotFilePath].takeCensus(censusOptions);
+ let parentMap;
+
+ if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
+ const opts = { filter: requestOptions.filter || null };
+ if (requestOptions.asInvertedTreeNode) {
+ opts.invert = true;
+ }
+ report = censusReportToCensusTreeNode(
+ censusOptions.breakdown,
+ report,
+ opts
+ );
+ parentMap = CensusUtils.createParentMap(report);
+ }
+
+ return { report, parentMap };
+ }
+);
+
+/**
+ * @see HeapAnalysesClient.prototype.getCensusIndividuals
+ */
+workerHelper.createTask(self, "getCensusIndividuals", request => {
+ const {
+ dominatorTreeId,
+ indices,
+ censusBreakdown,
+ labelBreakdown,
+ maxRetainingPaths,
+ maxIndividuals,
+ } = request;
+
+ const dominatorTree = dominatorTrees[dominatorTreeId];
+ if (!dominatorTree) {
+ throw new Error(
+ `There does not exist a DominatorTree with the id ${dominatorTreeId}`
+ );
+ }
+
+ const snapshot = dominatorTreeSnapshots[dominatorTreeId];
+ const nodeIds = CensusUtils.getCensusIndividuals(
+ indices,
+ censusBreakdown,
+ snapshot
+ );
+
+ const nodes = nodeIds
+ .sort(
+ (a, b) =>
+ dominatorTree.getRetainedSize(b) - dominatorTree.getRetainedSize(a)
+ )
+ .slice(0, maxIndividuals)
+ .map(id => {
+ const { label, shallowSize } = DominatorTreeNode.getLabelAndShallowSize(
+ id,
+ snapshot,
+ labelBreakdown
+ );
+ const retainedSize = dominatorTree.getRetainedSize(id);
+ const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
+ node.moreChildrenAvailable = false;
+ return node;
+ });
+
+ DominatorTreeNode.attachShortestPaths(
+ snapshot,
+ labelBreakdown,
+ dominatorTree.root,
+ nodes,
+ maxRetainingPaths
+ );
+
+ return { nodes };
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.takeCensusDiff
+ */
+workerHelper.createTask(self, "takeCensusDiff", request => {
+ const {
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ censusOptions,
+ requestOptions,
+ } = request;
+
+ if (!snapshots[firstSnapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${firstSnapshotFilePath}'`);
+ }
+
+ if (!snapshots[secondSnapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${secondSnapshotFilePath}'`);
+ }
+
+ const first = snapshots[firstSnapshotFilePath].takeCensus(censusOptions);
+ const second = snapshots[secondSnapshotFilePath].takeCensus(censusOptions);
+ let delta = CensusUtils.diff(censusOptions.breakdown, first, second);
+ let parentMap;
+
+ if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
+ const opts = { filter: requestOptions.filter || null };
+ if (requestOptions.asInvertedTreeNode) {
+ opts.invert = true;
+ }
+ delta = censusReportToCensusTreeNode(censusOptions.breakdown, delta, opts);
+ parentMap = CensusUtils.createParentMap(delta);
+ }
+
+ return { delta, parentMap };
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getCreationTime
+ */
+workerHelper.createTask(self, "getCreationTime", snapshotFilePath => {
+ if (!snapshots[snapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+ return snapshots[snapshotFilePath].creationTime;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.computeDominatorTree
+ */
+workerHelper.createTask(self, "computeDominatorTree", snapshotFilePath => {
+ const snapshot = snapshots[snapshotFilePath];
+ if (!snapshot) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+
+ const id = dominatorTrees.length;
+ dominatorTrees.push(snapshot.computeDominatorTree());
+ dominatorTreeSnapshots.push(snapshot);
+ return id;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getDominatorTree
+ */
+workerHelper.createTask(self, "getDominatorTree", request => {
+ const {
+ dominatorTreeId,
+ breakdown,
+ maxDepth,
+ maxSiblings,
+ maxRetainingPaths,
+ } = request;
+
+ if (!(dominatorTreeId >= 0 && dominatorTreeId < dominatorTrees.length)) {
+ throw new Error(
+ `There does not exist a DominatorTree with the id ${dominatorTreeId}`
+ );
+ }
+
+ const dominatorTree = dominatorTrees[dominatorTreeId];
+ const snapshot = dominatorTreeSnapshots[dominatorTreeId];
+
+ const tree = DominatorTreeNode.partialTraversal(
+ dominatorTree,
+ snapshot,
+ breakdown,
+ maxDepth,
+ maxSiblings
+ );
+
+ const nodes = [];
+ (function getNodes(node) {
+ nodes.push(node);
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ getNodes(node.children[i]);
+ }
+ }
+ })(tree);
+
+ DominatorTreeNode.attachShortestPaths(
+ snapshot,
+ breakdown,
+ dominatorTree.root,
+ nodes,
+ maxRetainingPaths
+ );
+
+ return tree;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getImmediatelyDominated
+ */
+workerHelper.createTask(self, "getImmediatelyDominated", request => {
+ const {
+ dominatorTreeId,
+ nodeId,
+ breakdown,
+ startIndex,
+ maxCount,
+ maxRetainingPaths,
+ } = request;
+
+ if (!(dominatorTreeId >= 0 && dominatorTreeId < dominatorTrees.length)) {
+ throw new Error(
+ `There does not exist a DominatorTree with the id ${dominatorTreeId}`
+ );
+ }
+
+ const dominatorTree = dominatorTrees[dominatorTreeId];
+ const snapshot = dominatorTreeSnapshots[dominatorTreeId];
+
+ const childIds = dominatorTree.getImmediatelyDominated(nodeId);
+ if (!childIds) {
+ throw new Error(`${nodeId} is not a node id in the dominator tree`);
+ }
+
+ const start = startIndex || DEFAULT_START_INDEX;
+ const count = maxCount || DEFAULT_MAX_COUNT;
+ const end = start + count;
+
+ const nodes = childIds.slice(start, end).map(id => {
+ const { label, shallowSize } = DominatorTreeNode.getLabelAndShallowSize(
+ id,
+ snapshot,
+ breakdown
+ );
+ const retainedSize = dominatorTree.getRetainedSize(id);
+ const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
+ node.parentId = nodeId;
+ // DominatorTree.getImmediatelyDominated will always return non-null here
+ // because we got the id directly from the dominator tree.
+ node.moreChildrenAvailable =
+ !!dominatorTree.getImmediatelyDominated(id).length;
+ return node;
+ });
+
+ const path = [];
+ let id = nodeId;
+ do {
+ path.push(id);
+ id = dominatorTree.getImmediateDominator(id);
+ } while (id !== null);
+ path.reverse();
+
+ const moreChildrenAvailable = childIds.length > end;
+
+ DominatorTreeNode.attachShortestPaths(
+ snapshot,
+ breakdown,
+ dominatorTree.root,
+ nodes,
+ maxRetainingPaths
+ );
+
+ return { nodes, moreChildrenAvailable, path };
+});
diff --git a/devtools/shared/heapsnapshot/HeapAnalysesClient.js b/devtools/shared/heapsnapshot/HeapAnalysesClient.js
new file mode 100644
index 0000000000..a77d9397a5
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapAnalysesClient.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/. */
+
+"use strict";
+
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ DevToolsWorker,
+} = require("resource://devtools/shared/worker/worker.js");
+
+const WORKER_URL =
+ "resource://devtools/shared/heapsnapshot/HeapAnalyses.worker.js";
+var workerCounter = 0;
+
+/**
+ * A HeapAnalysesClient instance provides a developer-friendly interface for
+ * interacting with a HeapAnalysesWorker. This enables users to be ignorant of
+ * the message passing protocol used to communicate with the worker. The
+ * HeapAnalysesClient owns the worker, and terminating the worker is done by
+ * terminating the client (see the `destroy` method).
+ */
+const HeapAnalysesClient = (module.exports = function () {
+ this._worker = new DevToolsWorker(WORKER_URL, {
+ name: `HeapAnalyses-${workerCounter++}`,
+ verbose: DevToolsUtils.dumpv.wantVerbose,
+ });
+});
+
+/**
+ * Destroy the worker, causing it to release its resources (such as heap
+ * snapshots it has deserialized and read into memory). The client is no longer
+ * usable after calling this method.
+ */
+HeapAnalysesClient.prototype.destroy = function () {
+ this._worker.destroy();
+ this._worker = null;
+};
+
+/**
+ * Tell the worker to read into memory the heap snapshot at the given file
+ * path. This is a prerequisite for asking the worker to perform various
+ * analyses on a heap snapshot.
+ *
+ * @param {String} snapshotFilePath
+ *
+ * @returns Promise
+ * The promise is fulfilled if the heap snapshot is successfully
+ * deserialized and read into memory. The promise is rejected if that
+ * does not happen, eg due to a bad file path or malformed heap
+ * snapshot file.
+ */
+HeapAnalysesClient.prototype.readHeapSnapshot = function (snapshotFilePath) {
+ return this._worker.performTask("readHeapSnapshot", { snapshotFilePath });
+};
+
+/**
+ * Tell the worker to delete all references to the snapshot and dominator trees
+ * linked to the provided snapshot file path.
+ *
+ * @param {String} snapshotFilePath
+ * @return Promise<undefined>
+ */
+HeapAnalysesClient.prototype.deleteHeapSnapshot = function (snapshotFilePath) {
+ return this._worker.performTask("deleteHeapSnapshot", { snapshotFilePath });
+};
+
+/**
+ * Request the creation time given a snapshot file path. Returns `null`
+ * if snapshot does not exist.
+ *
+ * @param {String} snapshotFilePath
+ * The path to the snapshot.
+ * @return {Number?}
+ * The unix timestamp of the creation time of the snapshot, or null if
+ * snapshot does not exist.
+ */
+HeapAnalysesClient.prototype.getCreationTime = function (snapshotFilePath) {
+ return this._worker.performTask("getCreationTime", snapshotFilePath);
+};
+
+/** * Censuses *****************************************************************/
+
+/**
+ * Ask the worker to perform a census analysis on the heap snapshot with the
+ * given path. The heap snapshot at the given path must have already been read
+ * into memory by the worker (see `readHeapSnapshot`).
+ *
+ * @param {String} snapshotFilePath
+ *
+ * @param {Object} censusOptions
+ * A structured-cloneable object specifying the requested census's
+ * breakdown. See the "takeCensus" section of
+ * `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
+ *
+ * @param {Object} requestOptions
+ * An object specifying options of this worker request.
+ * - {Boolean} asTreeNode
+ * Whether or not the census is returned as a CensusTreeNode,
+ * or just a breakdown report. Defaults to false.
+ * @see `devtools/shared/heapsnapshot/census-tree-node.js`
+ * - {Boolean} asInvertedTreeNode
+ * Whether or not the census is returned as an inverted
+ * CensusTreeNode. Defaults to false.
+ * - {String} filter
+ * A filter string to prune the resulting tree with. Only applies if
+ * either asTreeNode or asInvertedTreeNode is true.
+ *
+ * @returns Promise<Object>
+ * An object with the following properties:
+ * - report:
+ * The report generated by the given census breakdown, or a
+ * CensusTreeNode generated by the given census breakdown if
+ * `asTreeNode` is true.
+ * - parentMap:
+ * The result of calling CensusUtils.createParentMap on the generated
+ * report. Only exists if asTreeNode or asInvertedTreeNode are set.
+ */
+HeapAnalysesClient.prototype.takeCensus = function (
+ snapshotFilePath,
+ censusOptions,
+ requestOptions = {}
+) {
+ return this._worker.performTask("takeCensus", {
+ snapshotFilePath,
+ censusOptions,
+ requestOptions,
+ });
+};
+
+/**
+ * Get the individual nodes that correspond to the given census report leaf
+ * indices.
+ *
+ * @param {Object} opts
+ * An object with the following properties:
+ * - {DominatorTreeId} dominatorTreeId: The id of the dominator tree.
+ * - {Set<Number>} indices: The indices of the census report leaves we
+ * would like to get the individuals for.
+ * - {Object} censusBreakdown: The breakdown used to generate the census.
+ * - {Object} labelBreakdown: The breakdown we would like to use when
+ * labeling the resulting nodes.
+ * - {Number} maxRetainingPaths: The maximum number of retaining paths to
+ * compute for each node.
+ * - {Number} maxIndividuals: The maximum number of individual nodes to
+ * return.
+ *
+ * @returns {Promise<Object>}
+ * A promise of an object with the following properties:
+ * - {Array<DominatorTreeNode>} nodes: An array of `DominatorTreeNode`s
+ * with their shortest paths attached, and without any dominator tree
+ * child/parent information attached. The results are sorted by
+ * retained size.
+ *
+ */
+HeapAnalysesClient.prototype.getCensusIndividuals = function (opts) {
+ return this._worker.performTask("getCensusIndividuals", opts);
+};
+
+/**
+ * Request that the worker take a census on the heap snapshots with the given
+ * paths and then return the difference between them. Both heap snapshots must
+ * have already been read into memory by the worker (see `readHeapSnapshot`).
+ *
+ * @param {String} firstSnapshotFilePath
+ * The first snapshot file path.
+ *
+ * @param {String} secondSnapshotFilePath
+ * The second snapshot file path.
+ *
+ * @param {Object} censusOptions
+ * A structured-cloneable object specifying the requested census's
+ * breakdown. See the "takeCensus" section of
+ * `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
+ *
+ * @param {Object} requestOptions
+ * An object specifying options for this request.
+ * - {Boolean} asTreeNode
+ * Whether the resulting delta report should be converted to a census
+ * tree node before returned. Defaults to false.
+ * - {Boolean} asInvertedTreeNode
+ * Whether or not the census is returned as an inverted
+ * CensusTreeNode. Defaults to false.
+ * - {String} filter
+ * A filter string to prune the resulting tree with. Only applies if
+ * either asTreeNode or asInvertedTreeNode is true.
+ *
+ * @returns Promise<Object>
+ * - delta:
+ * The delta report generated by diffing the two census reports, or a
+ * CensusTreeNode generated from the delta report if
+ * `requestOptions.asTreeNode` was true.
+ * - parentMap:
+ * The result of calling CensusUtils.createParentMap on the generated
+ * delta. Only exists if asTreeNode or asInvertedTreeNode are set.
+ */
+HeapAnalysesClient.prototype.takeCensusDiff = function (
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ censusOptions,
+ requestOptions = {}
+) {
+ return this._worker.performTask("takeCensusDiff", {
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ censusOptions,
+ requestOptions,
+ });
+};
+
+/** * Dominator Trees **********************************************************/
+
+/**
+ * Compute the dominator tree of the heap snapshot loaded from the given file
+ * path. Returns the id of the computed dominator tree.
+ *
+ * @param {String} snapshotFilePath
+ *
+ * @returns {Promise<DominatorTreeId>}
+ */
+HeapAnalysesClient.prototype.computeDominatorTree = function (
+ snapshotFilePath
+) {
+ return this._worker.performTask("computeDominatorTree", snapshotFilePath);
+};
+
+/**
+ * Get the initial, partial view of the dominator tree with the given id.
+ *
+ * @param {Object} opts
+ * An object specifying options for this request.
+ * - {DominatorTreeId} dominatorTreeId
+ * The id of the dominator tree.
+ * - {Object} breakdown
+ * The breakdown used to generate node labels.
+ * - {Number} maxDepth
+ * The maximum depth to traverse down the tree to create this initial
+ * view.
+ * - {Number} maxSiblings
+ * The maximum number of siblings to visit within each traversed node's
+ * children.
+ * - {Number} maxRetainingPaths
+ * The maximum number of retaining paths to find for each node.
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+HeapAnalysesClient.prototype.getDominatorTree = function (opts) {
+ return this._worker.performTask("getDominatorTree", opts);
+};
+
+/**
+ * Get a subset of a nodes children in the dominator tree.
+ *
+ * @param {Object} opts
+ * An object specifying options for this request.
+ * - {DominatorTreeId} dominatorTreeId
+ * The id of the dominator tree.
+ * - {NodeId} nodeId
+ * The id of the node whose children are being found.
+ * - {Object} breakdown
+ * The breakdown used to generate node labels.
+ * - {Number} startIndex
+ * The starting index within the full set of immediately dominated
+ * children of the children being requested. Children are always sorted
+ * by greatest to least retained size.
+ * - {Number} maxCount
+ * The maximum number of children to return.
+ * - {Number} maxRetainingPaths
+ * The maximum number of retaining paths to find for each node.
+ *
+ * @returns {Promise<Object>}
+ * A promise of an object with the following properties:
+ * - {Array<DominatorTreeNode>} nodes
+ * The requested nodes that are immediately dominated by the node
+ * identified by `opts.nodeId`.
+ * - {Boolean} moreChildrenAvailable
+ * True iff there are more children available after the returned
+ * nodes.
+ * - {Array<NodeId>} path
+ * The path through the tree from the root to these node's parent, eg
+ * [root's id, child of root's id, child of child of root's id, ..., `nodeId`].
+ */
+HeapAnalysesClient.prototype.getImmediatelyDominated = function (opts) {
+ return this._worker.performTask("getImmediatelyDominated", opts);
+};
diff --git a/devtools/shared/heapsnapshot/HeapSnapshot.cpp b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
new file mode 100644
index 0000000000..ce0eec2d5c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -0,0 +1,1581 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HeapSnapshot.h"
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/gzip_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin
+#include "js/Debug.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "js/TypeDecls.h"
+#include "js/UbiNodeBreadthFirst.h"
+#include "js/UbiNodeCensus.h"
+#include "js/UbiNodeDominatorTree.h"
+#include "js/UbiNodeShortestPaths.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/devtools/AutoMemMap.h"
+#include "mozilla/devtools/CoreDump.pb.h"
+#include "mozilla/devtools/DeserializedNode.h"
+#include "mozilla/devtools/DominatorTree.h"
+#include "mozilla/devtools/FileDescriptorOutputStream.h"
+#include "mozilla/devtools/HeapSnapshotTempFileHelperChild.h"
+#include "mozilla/devtools/ZeroCopyNSIOutputStream.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/HeapSnapshotBinding.h"
+#include "mozilla/RangedPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/MapAndSet.h"
+#include "js/Object.h" // JS::GetCompartment
+#include "nsComponentManagerUtils.h" // do_CreateInstance
+#include "nsCycleCollectionParticipant.h"
+#include "nsCRTGlue.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+#include "nsISupportsImpl.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "prerror.h"
+#include "prio.h"
+#include "prtypes.h"
+#include "SpecialSystemDirectory.h"
+
+namespace mozilla {
+namespace devtools {
+
+using namespace JS;
+using namespace dom;
+
+using ::google::protobuf::io::ArrayInputStream;
+using ::google::protobuf::io::CodedInputStream;
+using ::google::protobuf::io::GzipInputStream;
+using ::google::protobuf::io::ZeroCopyInputStream;
+
+using JS::ubi::AtomOrTwoByteChars;
+using JS::ubi::ShortestPaths;
+
+MallocSizeOf GetCurrentThreadDebuggerMallocSizeOf() {
+ auto ccjscx = CycleCollectedJSContext::Get();
+ MOZ_ASSERT(ccjscx);
+ auto cx = ccjscx->Context();
+ MOZ_ASSERT(cx);
+ auto mallocSizeOf = JS::dbg::GetDebuggerMallocSizeOf(cx);
+ MOZ_ASSERT(mallocSizeOf);
+ return mallocSizeOf;
+}
+
+/*** Cycle Collection Boilerplate *********************************************/
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HeapSnapshot, mParent)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HeapSnapshot)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HeapSnapshot)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HeapSnapshot)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* virtual */
+JSObject* HeapSnapshot::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HeapSnapshot_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/*** Reading Heap Snapshots ***************************************************/
+
+/* static */
+already_AddRefed<HeapSnapshot> HeapSnapshot::Create(JSContext* cx,
+ GlobalObject& global,
+ const uint8_t* buffer,
+ uint32_t size,
+ ErrorResult& rv) {
+ RefPtr<HeapSnapshot> snapshot = new HeapSnapshot(cx, global.GetAsSupports());
+ if (!snapshot->init(cx, buffer, size)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ return snapshot.forget();
+}
+
+template <typename MessageType>
+static bool parseMessage(ZeroCopyInputStream& stream, uint32_t sizeOfMessage,
+ MessageType& message) {
+ // We need to create a new `CodedInputStream` for each message so that the
+ // 64MB limit is applied per-message rather than to the whole stream.
+ CodedInputStream codedStream(&stream);
+
+ // The protobuf message nesting that core dumps exhibit is dominated by
+ // allocation stacks' frames. In the most deeply nested case, each frame has
+ // two messages: a StackFrame message and a StackFrame::Data message. These
+ // frames are on top of a small constant of other messages. There are a
+ // MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for
+ // the two messages per frame plus some head room for the constant number of
+ // non-dominating messages.
+ codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3);
+
+ auto limit = codedStream.PushLimit(sizeOfMessage);
+ if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) ||
+ NS_WARN_IF(!codedStream.ConsumedEntireMessage()) ||
+ NS_WARN_IF(codedStream.BytesUntilLimit() != 0)) {
+ return false;
+ }
+
+ codedStream.PopLimit(limit);
+ return true;
+}
+
+template <typename CharT, typename InternedStringSet>
+struct GetOrInternStringMatcher {
+ InternedStringSet& internedStrings;
+
+ explicit GetOrInternStringMatcher(InternedStringSet& strings)
+ : internedStrings(strings) {}
+
+ const CharT* operator()(const std::string* str) {
+ MOZ_ASSERT(str);
+ size_t length = str->length() / sizeof(CharT);
+ auto tempString = reinterpret_cast<const CharT*>(str->data());
+
+ UniqueFreePtr<CharT[]> owned(NS_xstrndup(tempString, length));
+ if (!internedStrings.append(std::move(owned))) return nullptr;
+
+ return internedStrings.back().get();
+ }
+
+ const CharT* operator()(uint64_t ref) {
+ if (MOZ_LIKELY(ref < internedStrings.length())) {
+ auto& string = internedStrings[ref];
+ MOZ_ASSERT(string);
+ return string.get();
+ }
+
+ return nullptr;
+ }
+};
+
+template <
+ // Either char or char16_t.
+ typename CharT,
+ // A reference to either `internedOneByteStrings` or
+ // `internedTwoByteStrings` if CharT is char or char16_t respectively.
+ typename InternedStringSet>
+const CharT* HeapSnapshot::getOrInternString(
+ InternedStringSet& internedStrings, Maybe<StringOrRef>& maybeStrOrRef) {
+ // Incomplete message: has neither a string nor a reference to an already
+ // interned string.
+ if (MOZ_UNLIKELY(maybeStrOrRef.isNothing())) return nullptr;
+
+ GetOrInternStringMatcher<CharT, InternedStringSet> m(internedStrings);
+ return maybeStrOrRef->match(m);
+}
+
+// Get a de-duplicated string as a Maybe<StringOrRef> from the given `msg`.
+#define GET_STRING_OR_REF_WITH_PROP_NAMES(msg, strPropertyName, \
+ refPropertyName) \
+ (msg.has_##refPropertyName() ? Some(StringOrRef(msg.refPropertyName())) \
+ : msg.has_##strPropertyName() ? Some(StringOrRef(&msg.strPropertyName())) \
+ : Nothing())
+
+#define GET_STRING_OR_REF(msg, property) \
+ (msg.has_##property##ref() ? Some(StringOrRef(msg.property##ref())) \
+ : msg.has_##property() ? Some(StringOrRef(&msg.property())) \
+ : Nothing())
+
+bool HeapSnapshot::saveNode(const protobuf::Node& node,
+ NodeIdSet& edgeReferents) {
+ // NB: de-duplicated string properties must be read back and interned in the
+ // same order here as they are written and serialized in
+ // `CoreDumpWriter::writeNode` or else indices in references to already
+ // serialized strings will be off.
+
+ if (NS_WARN_IF(!node.has_id())) return false;
+ NodeId id = node.id();
+
+ // NodeIds are derived from pointers (at most 48 bits) and we rely on them
+ // fitting into JS numbers (IEEE 754 doubles, can precisely store 53 bit
+ // integers) despite storing them on disk as 64 bit integers.
+ if (NS_WARN_IF(!JS::Value::isNumberRepresentable(id))) return false;
+
+ // Should only deserialize each node once.
+ if (NS_WARN_IF(nodes.has(id))) return false;
+
+ if (NS_WARN_IF(!JS::ubi::Uint32IsValidCoarseType(node.coarsetype())))
+ return false;
+ auto coarseType = JS::ubi::Uint32ToCoarseType(node.coarsetype());
+
+ Maybe<StringOrRef> typeNameOrRef =
+ GET_STRING_OR_REF_WITH_PROP_NAMES(node, typename_, typenameref);
+ auto typeName =
+ getOrInternString<char16_t>(internedTwoByteStrings, typeNameOrRef);
+ if (NS_WARN_IF(!typeName)) return false;
+
+ if (NS_WARN_IF(!node.has_size())) return false;
+ uint64_t size = node.size();
+
+ auto edgesLength = node.edges_size();
+ DeserializedNode::EdgeVector edges;
+ if (NS_WARN_IF(!edges.reserve(edgesLength))) return false;
+ for (decltype(edgesLength) i = 0; i < edgesLength; i++) {
+ auto& protoEdge = node.edges(i);
+
+ if (NS_WARN_IF(!protoEdge.has_referent())) return false;
+ NodeId referent = protoEdge.referent();
+
+ if (NS_WARN_IF(!edgeReferents.put(referent))) return false;
+
+ const char16_t* edgeName = nullptr;
+ if (protoEdge.EdgeNameOrRef_case() !=
+ protobuf::Edge::EDGENAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> edgeNameOrRef = GET_STRING_OR_REF(protoEdge, name);
+ edgeName =
+ getOrInternString<char16_t>(internedTwoByteStrings, edgeNameOrRef);
+ if (NS_WARN_IF(!edgeName)) return false;
+ }
+
+ edges.infallibleAppend(DeserializedEdge(referent, edgeName));
+ }
+
+ Maybe<StackFrameId> allocationStack;
+ if (node.has_allocationstack()) {
+ StackFrameId id = 0;
+ if (NS_WARN_IF(!saveStackFrame(node.allocationstack(), id))) return false;
+ allocationStack.emplace(id);
+ }
+ MOZ_ASSERT(allocationStack.isSome() == node.has_allocationstack());
+
+ const char* jsObjectClassName = nullptr;
+ if (node.JSObjectClassNameOrRef_case() !=
+ protobuf::Node::JSOBJECTCLASSNAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> clsNameOrRef =
+ GET_STRING_OR_REF(node, jsobjectclassname);
+ jsObjectClassName =
+ getOrInternString<char>(internedOneByteStrings, clsNameOrRef);
+ if (NS_WARN_IF(!jsObjectClassName)) return false;
+ }
+
+ const char* scriptFilename = nullptr;
+ if (node.ScriptFilenameOrRef_case() !=
+ protobuf::Node::SCRIPTFILENAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> scriptFilenameOrRef =
+ GET_STRING_OR_REF(node, scriptfilename);
+ scriptFilename =
+ getOrInternString<char>(internedOneByteStrings, scriptFilenameOrRef);
+ if (NS_WARN_IF(!scriptFilename)) return false;
+ }
+
+ const char16_t* descriptiveTypeName = nullptr;
+ if (node.descriptiveTypeNameOrRef_case() !=
+ protobuf::Node::DESCRIPTIVETYPENAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> descriptiveTypeNameOrRef =
+ GET_STRING_OR_REF(node, descriptivetypename);
+ descriptiveTypeName = getOrInternString<char16_t>(internedTwoByteStrings,
+ descriptiveTypeNameOrRef);
+ if (NS_WARN_IF(!descriptiveTypeName)) return false;
+ }
+
+ if (NS_WARN_IF(!nodes.putNew(
+ id, DeserializedNode(id, coarseType, typeName, size, std::move(edges),
+ allocationStack, jsObjectClassName,
+ scriptFilename, descriptiveTypeName, *this)))) {
+ return false;
+ };
+
+ return true;
+}
+
+bool HeapSnapshot::saveStackFrame(const protobuf::StackFrame& frame,
+ StackFrameId& outFrameId) {
+ // NB: de-duplicated string properties must be read in the same order here as
+ // they are written in `CoreDumpWriter::getProtobufStackFrame` or else indices
+ // in references to already serialized strings will be off.
+
+ if (frame.has_ref()) {
+ // We should only get a reference to the previous frame if we have already
+ // seen the previous frame.
+ if (!frames.has(frame.ref())) return false;
+
+ outFrameId = frame.ref();
+ return true;
+ }
+
+ // Incomplete message.
+ if (!frame.has_data()) return false;
+
+ auto data = frame.data();
+
+ if (!data.has_id()) return false;
+ StackFrameId id = data.id();
+
+ // This should be the first and only time we see this frame.
+ if (frames.has(id)) return false;
+
+ if (!data.has_line()) return false;
+ uint32_t line = data.line();
+
+ if (!data.has_column()) return false;
+ JS::TaggedColumnNumberOneOrigin column(
+ JS::LimitedColumnNumberOneOrigin(data.column()));
+
+ if (!data.has_issystem()) return false;
+ bool isSystem = data.issystem();
+
+ if (!data.has_isselfhosted()) return false;
+ bool isSelfHosted = data.isselfhosted();
+
+ Maybe<StringOrRef> sourceOrRef = GET_STRING_OR_REF(data, source);
+ auto source =
+ getOrInternString<char16_t>(internedTwoByteStrings, sourceOrRef);
+ if (!source) return false;
+
+ const char16_t* functionDisplayName = nullptr;
+ if (data.FunctionDisplayNameOrRef_case() !=
+ protobuf::StackFrame_Data::FUNCTIONDISPLAYNAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> nameOrRef = GET_STRING_OR_REF(data, functiondisplayname);
+ functionDisplayName =
+ getOrInternString<char16_t>(internedTwoByteStrings, nameOrRef);
+ if (!functionDisplayName) return false;
+ }
+
+ Maybe<StackFrameId> parent;
+ if (data.has_parent()) {
+ StackFrameId parentId = 0;
+ if (!saveStackFrame(data.parent(), parentId)) return false;
+ parent = Some(parentId);
+ }
+
+ if (!frames.putNew(id,
+ DeserializedStackFrame(id, parent, line, column, source,
+ functionDisplayName, isSystem,
+ isSelfHosted, *this))) {
+ return false;
+ }
+
+ outFrameId = id;
+ return true;
+}
+
+#undef GET_STRING_OR_REF_WITH_PROP_NAMES
+#undef GET_STRING_OR_REF
+
+// Because protobuf messages aren't self-delimiting, we serialize each message
+// preceded by its size in bytes. When deserializing, we read this size and then
+// limit reading from the stream to the given byte size. If we didn't, then the
+// first message would consume the entire stream.
+static bool readSizeOfNextMessage(ZeroCopyInputStream& stream,
+ uint32_t* sizep) {
+ MOZ_ASSERT(sizep);
+ CodedInputStream codedStream(&stream);
+ return codedStream.ReadVarint32(sizep) && *sizep > 0;
+}
+
+bool HeapSnapshot::init(JSContext* cx, const uint8_t* buffer, uint32_t size) {
+ ArrayInputStream stream(buffer, size);
+ GzipInputStream gzipStream(&stream);
+ uint32_t sizeOfMessage = 0;
+
+ // First is the metadata.
+
+ protobuf::Metadata metadata;
+ if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage)))
+ return false;
+ if (!parseMessage(gzipStream, sizeOfMessage, metadata)) return false;
+ if (metadata.has_timestamp()) timestamp.emplace(metadata.timestamp());
+
+ // Next is the root node.
+
+ protobuf::Node root;
+ if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage)))
+ return false;
+ if (!parseMessage(gzipStream, sizeOfMessage, root)) return false;
+
+ // Although the id is optional in the protobuf format for future proofing, we
+ // can't currently do anything without it.
+ if (NS_WARN_IF(!root.has_id())) return false;
+ rootId = root.id();
+
+ // The set of all node ids we've found edges pointing to.
+ NodeIdSet edgeReferents(cx);
+
+ if (NS_WARN_IF(!saveNode(root, edgeReferents))) return false;
+
+ // Finally, the rest of the nodes in the core dump.
+
+ // Test for the end of the stream. The protobuf library gives no way to tell
+ // the difference between an underlying read error and the stream being
+ // done. All we can do is attempt to read the size of the next message and
+ // extrapolate guestimations from the result of that operation.
+ while (readSizeOfNextMessage(gzipStream, &sizeOfMessage)) {
+ protobuf::Node node;
+ if (!parseMessage(gzipStream, sizeOfMessage, node)) return false;
+ if (NS_WARN_IF(!saveNode(node, edgeReferents))) return false;
+ }
+
+ // Check the set of node ids referred to by edges we found and ensure that we
+ // have the node corresponding to each id. If we don't have all of them, it is
+ // unsafe to perform analyses of this heap snapshot.
+ for (auto iter = edgeReferents.iter(); !iter.done(); iter.next()) {
+ if (NS_WARN_IF(!nodes.has(iter.get()))) return false;
+ }
+
+ return true;
+}
+
+/*** Heap Snapshot Analyses ***************************************************/
+
+void HeapSnapshot::TakeCensus(JSContext* cx, JS::Handle<JSObject*> options,
+ JS::MutableHandle<JS::Value> rval,
+ ErrorResult& rv) {
+ JS::ubi::Census census(cx);
+
+ JS::ubi::CountTypePtr rootType;
+ if (NS_WARN_IF(!JS::ubi::ParseCensusOptions(cx, census, options, rootType))) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ JS::ubi::RootedCount rootCount(cx, rootType->makeCount());
+ if (NS_WARN_IF(!rootCount)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::ubi::CensusHandler handler(census, rootCount,
+ GetCurrentThreadDebuggerMallocSizeOf());
+
+ {
+ JS::AutoCheckCannotGC nogc;
+
+ JS::ubi::CensusTraversal traversal(cx, handler, nogc);
+
+ if (NS_WARN_IF(!traversal.addStart(getRoot()))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ if (NS_WARN_IF(!traversal.traverse())) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ }
+
+ if (NS_WARN_IF(!handler.report(cx, rval))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+}
+
+void HeapSnapshot::DescribeNode(JSContext* cx, JS::Handle<JSObject*> breakdown,
+ uint64_t nodeId,
+ JS::MutableHandle<JS::Value> rval,
+ ErrorResult& rv) {
+ MOZ_ASSERT(breakdown);
+ JS::Rooted<JS::Value> breakdownVal(cx, JS::ObjectValue(*breakdown));
+ JS::ubi::CountTypePtr rootType = JS::ubi::ParseBreakdown(cx, breakdownVal);
+ if (NS_WARN_IF(!rootType)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ JS::ubi::RootedCount rootCount(cx, rootType->makeCount());
+ if (NS_WARN_IF(!rootCount)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::ubi::Node::Id id(nodeId);
+ Maybe<JS::ubi::Node> node = getNodeById(id);
+ if (NS_WARN_IF(node.isNothing())) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
+ if (NS_WARN_IF(!rootCount->count(mallocSizeOf, *node))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ if (NS_WARN_IF(!rootCount->report(cx, rval))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+}
+
+already_AddRefed<DominatorTree> HeapSnapshot::ComputeDominatorTree(
+ ErrorResult& rv) {
+ Maybe<JS::ubi::DominatorTree> maybeTree;
+ {
+ auto ccjscx = CycleCollectedJSContext::Get();
+ MOZ_ASSERT(ccjscx);
+ auto cx = ccjscx->Context();
+ MOZ_ASSERT(cx);
+ JS::AutoCheckCannotGC nogc(cx);
+ maybeTree = JS::ubi::DominatorTree::Create(cx, nogc, getRoot());
+ }
+
+ if (NS_WARN_IF(maybeTree.isNothing())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ return MakeAndAddRef<DominatorTree>(std::move(*maybeTree), this, mParent);
+}
+
+void HeapSnapshot::ComputeShortestPaths(JSContext* cx, uint64_t start,
+ const Sequence<uint64_t>& targets,
+ uint64_t maxNumPaths,
+ JS::MutableHandle<JSObject*> results,
+ ErrorResult& rv) {
+ // First ensure that our inputs are valid.
+
+ if (NS_WARN_IF(maxNumPaths == 0)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Maybe<JS::ubi::Node> startNode = getNodeById(start);
+ if (NS_WARN_IF(startNode.isNothing())) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ if (NS_WARN_IF(targets.Length() == 0)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ // Aggregate the targets into a set and make sure that they exist in the heap
+ // snapshot.
+
+ JS::ubi::NodeSet targetsSet;
+
+ for (const auto& target : targets) {
+ Maybe<JS::ubi::Node> targetNode = getNodeById(target);
+ if (NS_WARN_IF(targetNode.isNothing())) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ if (NS_WARN_IF(!targetsSet.put(*targetNode))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+
+ // Walk the heap graph and find the shortest paths.
+
+ Maybe<ShortestPaths> maybeShortestPaths;
+ {
+ JS::AutoCheckCannotGC nogc(cx);
+ maybeShortestPaths = ShortestPaths::Create(
+ cx, nogc, maxNumPaths, *startNode, std::move(targetsSet));
+ }
+
+ if (NS_WARN_IF(maybeShortestPaths.isNothing())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto& shortestPaths = *maybeShortestPaths;
+
+ // Convert the results into a Map object mapping target node IDs to arrays of
+ // paths found.
+
+ JS::Rooted<JSObject*> resultsMap(cx, JS::NewMapObject(cx));
+ if (NS_WARN_IF(!resultsMap)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (auto iter = shortestPaths.targetIter(); !iter.done(); iter.next()) {
+ JS::Rooted<JS::Value> key(cx, JS::NumberValue(iter.get().identifier()));
+ JS::RootedVector<JS::Value> paths(cx);
+
+ bool ok = shortestPaths.forEachPath(iter.get(), [&](JS::ubi::Path& path) {
+ JS::RootedVector<JS::Value> pathValues(cx);
+
+ for (JS::ubi::BackEdge* edge : path) {
+ JS::Rooted<JSObject*> pathPart(cx, JS_NewPlainObject(cx));
+ if (!pathPart) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> predecessor(
+ cx, NumberValue(edge->predecessor().identifier()));
+ if (!JS_DefineProperty(cx, pathPart, "predecessor", predecessor,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> edgeNameVal(cx, NullValue());
+ if (edge->name()) {
+ JS::Rooted<JSString*> edgeName(
+ cx, JS_AtomizeUCString(cx, edge->name().get()));
+ if (!edgeName) {
+ return false;
+ }
+ edgeNameVal = StringValue(edgeName);
+ }
+
+ if (!JS_DefineProperty(cx, pathPart, "edge", edgeNameVal,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ if (!pathValues.append(ObjectValue(*pathPart))) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JSObject*> pathObj(cx, JS::NewArrayObject(cx, pathValues));
+ return pathObj && paths.append(ObjectValue(*pathObj));
+ });
+
+ if (NS_WARN_IF(!ok)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::Rooted<JSObject*> pathsArray(cx, JS::NewArrayObject(cx, paths));
+ if (NS_WARN_IF(!pathsArray)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::Rooted<JS::Value> pathsVal(cx, ObjectValue(*pathsArray));
+ if (NS_WARN_IF(!JS::MapSet(cx, resultsMap, key, pathsVal))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+
+ results.set(resultsMap);
+}
+
+/*** Saving Heap Snapshots ****************************************************/
+
+// If we are only taking a snapshot of the heap affected by the given set of
+// globals, find the set of compartments the globals are allocated
+// within. Returns false on OOM failure.
+static bool PopulateCompartmentsWithGlobals(
+ CompartmentSet& compartments, JS::HandleVector<JSObject*> globals) {
+ unsigned length = globals.length();
+ for (unsigned i = 0; i < length; i++) {
+ if (!compartments.put(JS::GetCompartment(globals[i]))) return false;
+ }
+
+ return true;
+}
+
+// Add the given set of globals as explicit roots in the given roots
+// list. Returns false on OOM failure.
+static bool AddGlobalsAsRoots(JS::HandleVector<JSObject*> globals,
+ ubi::RootList& roots) {
+ unsigned length = globals.length();
+ for (unsigned i = 0; i < length; i++) {
+ if (!roots.addRoot(ubi::Node(globals[i].get()), u"heap snapshot global")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Choose roots and limits for a traversal, given `boundaries`. Set `roots` to
+// the set of nodes within the boundaries that are referred to by nodes
+// outside. If `boundaries` does not include all JS compartments, initialize
+// `compartments` to the set of included compartments; otherwise, leave
+// `compartments` uninitialized. (You can use compartments.initialized() to
+// check.)
+//
+// If `boundaries` is incoherent, or we encounter an error while trying to
+// handle it, or we run out of memory, set `rv` appropriately and return
+// `false`.
+//
+// Return value is a pair of the status and an AutoCheckCannotGC token,
+// forwarded from ubi::RootList::init(), to ensure that the caller does
+// not GC while the RootList is live and initialized.
+static std::pair<bool, AutoCheckCannotGC> EstablishBoundaries(
+ JSContext* cx, ErrorResult& rv, const HeapSnapshotBoundaries& boundaries,
+ ubi::RootList& roots, CompartmentSet& compartments) {
+ MOZ_ASSERT(!roots.initialized());
+ MOZ_ASSERT(compartments.empty());
+
+ bool foundBoundaryProperty = false;
+
+ if (boundaries.mRuntime.WasPassed()) {
+ foundBoundaryProperty = true;
+
+ if (!boundaries.mRuntime.Value()) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+
+ auto [ok, nogc] = roots.init();
+ if (!ok) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return {false, nogc};
+ }
+ }
+
+ if (boundaries.mDebugger.WasPassed()) {
+ if (foundBoundaryProperty) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+ foundBoundaryProperty = true;
+
+ JSObject* dbgObj = boundaries.mDebugger.Value();
+ if (!dbgObj || !dbg::IsDebugger(*dbgObj)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+
+ JS::RootedVector<JSObject*> globals(cx);
+ if (!dbg::GetDebuggeeGlobals(cx, *dbgObj, &globals) ||
+ !PopulateCompartmentsWithGlobals(compartments, globals) ||
+ !roots.init(compartments).first || !AddGlobalsAsRoots(globals, roots)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+ }
+
+ if (boundaries.mGlobals.WasPassed()) {
+ if (foundBoundaryProperty) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+ foundBoundaryProperty = true;
+
+ uint32_t length = boundaries.mGlobals.Value().Length();
+ if (length == 0) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+
+ JS::RootedVector<JSObject*> globals(cx);
+ for (uint32_t i = 0; i < length; i++) {
+ JSObject* global = boundaries.mGlobals.Value().ElementAt(i);
+ if (!JS_IsGlobalObject(global)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+ if (!globals.append(global)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+ }
+
+ if (!PopulateCompartmentsWithGlobals(compartments, globals) ||
+ !roots.init(compartments).first || !AddGlobalsAsRoots(globals, roots)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return {false, AutoCheckCannotGC(cx)};
+ }
+ }
+ AutoCheckCannotGC nogc(cx);
+
+ if (!foundBoundaryProperty) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return {false, nogc};
+ }
+
+ MOZ_ASSERT(roots.initialized());
+ return {true, nogc};
+}
+
+// A variant covering all the various two-byte strings that we can get from the
+// ubi::Node API.
+class TwoByteString
+ : public Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName> {
+ using Base = Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName>;
+
+ struct CopyToBufferMatcher {
+ RangedPtr<char16_t> destination;
+ size_t maxLength;
+
+ CopyToBufferMatcher(RangedPtr<char16_t> destination, size_t maxLength)
+ : destination(destination), maxLength(maxLength) {}
+
+ size_t operator()(JS::ubi::EdgeName& ptr) {
+ return ptr ? operator()(ptr.get()) : 0;
+ }
+
+ size_t operator()(JSAtom* atom) {
+ MOZ_ASSERT(atom);
+ JS::ubi::AtomOrTwoByteChars s(atom);
+ return s.copyToBuffer(destination, maxLength);
+ }
+
+ size_t operator()(const char16_t* chars) {
+ MOZ_ASSERT(chars);
+ JS::ubi::AtomOrTwoByteChars s(chars);
+ return s.copyToBuffer(destination, maxLength);
+ }
+ };
+
+ public:
+ template <typename T>
+ MOZ_IMPLICIT TwoByteString(T&& rhs) : Base(std::forward<T>(rhs)) {}
+
+ template <typename T>
+ TwoByteString& operator=(T&& rhs) {
+ MOZ_ASSERT(this != &rhs, "self-move disallowed");
+ this->~TwoByteString();
+ new (this) TwoByteString(std::forward<T>(rhs));
+ return *this;
+ }
+
+ TwoByteString(const TwoByteString&) = delete;
+ TwoByteString& operator=(const TwoByteString&) = delete;
+
+ // Rewrap the inner value of a JS::ubi::AtomOrTwoByteChars as a TwoByteString.
+ static TwoByteString from(JS::ubi::AtomOrTwoByteChars&& s) {
+ return s.match([](auto* a) { return TwoByteString(a); });
+ }
+
+ // Returns true if the given TwoByteString is non-null, false otherwise.
+ bool isNonNull() const {
+ return match([](auto& t) { return t != nullptr; });
+ }
+
+ // Return the length of the string, 0 if it is null.
+ size_t length() const {
+ return match(
+ [](JSAtom* atom) -> size_t {
+ MOZ_ASSERT(atom);
+ JS::ubi::AtomOrTwoByteChars s(atom);
+ return s.length();
+ },
+ [](const char16_t* chars) -> size_t {
+ MOZ_ASSERT(chars);
+ return NS_strlen(chars);
+ },
+ [](const JS::ubi::EdgeName& ptr) -> size_t {
+ MOZ_ASSERT(ptr);
+ return NS_strlen(ptr.get());
+ });
+ }
+
+ // Copy the contents of a TwoByteString into the provided buffer. The buffer
+ // is NOT null terminated. The number of characters written is returned.
+ size_t copyToBuffer(RangedPtr<char16_t> destination, size_t maxLength) {
+ CopyToBufferMatcher m(destination, maxLength);
+ return match(m);
+ }
+
+ struct HashPolicy;
+};
+
+// A hashing policy for TwoByteString.
+//
+// Atoms are pointer hashed and use pointer equality, which means that we
+// tolerate some duplication across atoms and the other two types of two-byte
+// strings. In practice, we expect the amount of this duplication to be very low
+// because each type is generally a different semantic thing in addition to
+// having a slightly different representation. For example, the set of edge
+// names and the set stack frames' source names naturally tend not to overlap
+// very much if at all.
+struct TwoByteString::HashPolicy {
+ using Lookup = TwoByteString;
+
+ static js::HashNumber hash(const Lookup& l) {
+ return l.match(
+ [](const JSAtom* atom) {
+ return js::DefaultHasher<const JSAtom*>::hash(atom);
+ },
+ [](const char16_t* chars) {
+ MOZ_ASSERT(chars);
+ auto length = NS_strlen(chars);
+ return HashString(chars, length);
+ },
+ [](const JS::ubi::EdgeName& ptr) {
+ const char16_t* chars = ptr.get();
+ MOZ_ASSERT(chars);
+ auto length = NS_strlen(chars);
+ return HashString(chars, length);
+ });
+ }
+
+ struct EqualityMatcher {
+ const TwoByteString& rhs;
+ explicit EqualityMatcher(const TwoByteString& rhs) : rhs(rhs) {}
+
+ bool operator()(const JSAtom* atom) {
+ return rhs.is<JSAtom*>() && rhs.as<JSAtom*>() == atom;
+ }
+
+ bool operator()(const char16_t* chars) {
+ MOZ_ASSERT(chars);
+
+ const char16_t* rhsChars = nullptr;
+ if (rhs.is<const char16_t*>())
+ rhsChars = rhs.as<const char16_t*>();
+ else if (rhs.is<JS::ubi::EdgeName>())
+ rhsChars = rhs.as<JS::ubi::EdgeName>().get();
+ else
+ return false;
+ MOZ_ASSERT(rhsChars);
+
+ auto length = NS_strlen(chars);
+ if (NS_strlen(rhsChars) != length) return false;
+
+ return memcmp(chars, rhsChars, length * sizeof(char16_t)) == 0;
+ }
+
+ bool operator()(const JS::ubi::EdgeName& ptr) {
+ MOZ_ASSERT(ptr);
+ return operator()(ptr.get());
+ }
+ };
+
+ static bool match(const TwoByteString& k, const Lookup& l) {
+ EqualityMatcher eq(l);
+ return k.match(eq);
+ }
+
+ static void rekey(TwoByteString& k, TwoByteString&& newKey) {
+ k = std::move(newKey);
+ }
+};
+
+// Returns whether `edge` should be included in a heap snapshot of
+// `compartments`. The optional `policy` out-param is set to INCLUDE_EDGES
+// if we want to include the referent's edges, or EXCLUDE_EDGES if we don't
+// want to include them.
+static bool ShouldIncludeEdge(JS::CompartmentSet* compartments,
+ const ubi::Node& origin, const ubi::Edge& edge,
+ CoreDumpWriter::EdgePolicy* policy = nullptr) {
+ if (policy) {
+ *policy = CoreDumpWriter::INCLUDE_EDGES;
+ }
+
+ if (!compartments) {
+ // We aren't targeting a particular set of compartments, so serialize all
+ // the things!
+ return true;
+ }
+
+ // We are targeting a particular set of compartments. If this node is in our
+ // target set, serialize it and all of its edges. If this node is _not_ in our
+ // target set, we also serialize under the assumption that it is a shared
+ // resource being used by something in our target compartments since we
+ // reached it by traversing the heap graph. However, we do not serialize its
+ // outgoing edges and we abandon further traversal from this node.
+ //
+ // If the node does not belong to any compartment, we also serialize its
+ // outgoing edges. This case is relevant for Shapes: they don't belong to a
+ // specific compartment and contain edges to parent/kids Shapes we want to
+ // include. Note that these Shapes may contain pointers into our target
+ // compartment (the Shape's getter/setter JSObjects). However, we do not
+ // serialize nodes in other compartments that are reachable from these
+ // non-compartment nodes.
+
+ JS::Compartment* compartment = edge.referent.compartment();
+
+ if (!compartment || compartments->has(compartment)) {
+ return true;
+ }
+
+ if (policy) {
+ *policy = CoreDumpWriter::EXCLUDE_EDGES;
+ }
+
+ return !!origin.compartment();
+}
+
+// A `CoreDumpWriter` that serializes nodes to protobufs and writes them to the
+// given `ZeroCopyOutputStream`.
+class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter {
+ using FrameSet = js::HashSet<uint64_t>;
+ using TwoByteStringMap =
+ js::HashMap<TwoByteString, uint64_t, TwoByteString::HashPolicy>;
+ using OneByteStringMap = js::HashMap<const char*, uint64_t>;
+
+ JSContext* cx;
+ bool wantNames;
+ // The set of |JS::ubi::StackFrame::identifier()|s that have already been
+ // serialized and written to the core dump.
+ FrameSet framesAlreadySerialized;
+ // The set of two-byte strings that have already been serialized and written
+ // to the core dump.
+ TwoByteStringMap twoByteStringsAlreadySerialized;
+ // The set of one-byte strings that have already been serialized and written
+ // to the core dump.
+ OneByteStringMap oneByteStringsAlreadySerialized;
+
+ ::google::protobuf::io::ZeroCopyOutputStream& stream;
+
+ JS::CompartmentSet* compartments;
+
+ bool writeMessage(const ::google::protobuf::MessageLite& message) {
+ // We have to create a new CodedOutputStream when writing each message so
+ // that the 64MB size limit used by Coded{Output,Input}Stream to prevent
+ // integer overflow is enforced per message rather than on the whole stream.
+ ::google::protobuf::io::CodedOutputStream codedStream(&stream);
+ codedStream.WriteVarint32(message.ByteSizeLong());
+ message.SerializeWithCachedSizes(&codedStream);
+ return !codedStream.HadError();
+ }
+
+ // Attach the full two-byte string or a reference to a two-byte string that
+ // has already been serialized to a protobuf message.
+ template <typename SetStringFunction, typename SetRefFunction>
+ bool attachTwoByteString(TwoByteString& string, SetStringFunction setString,
+ SetRefFunction setRef) {
+ auto ptr = twoByteStringsAlreadySerialized.lookupForAdd(string);
+ if (ptr) {
+ setRef(ptr->value());
+ return true;
+ }
+
+ auto length = string.length();
+ auto stringData = MakeUnique<std::string>(length * sizeof(char16_t), '\0');
+ if (!stringData) return false;
+
+ auto buf = const_cast<char16_t*>(
+ reinterpret_cast<const char16_t*>(stringData->data()));
+ string.copyToBuffer(RangedPtr<char16_t>(buf, length), length);
+
+ uint64_t ref = twoByteStringsAlreadySerialized.count();
+ if (!twoByteStringsAlreadySerialized.add(ptr, std::move(string), ref))
+ return false;
+
+ setString(stringData.release());
+ return true;
+ }
+
+ // Attach the full one-byte string or a reference to a one-byte string that
+ // has already been serialized to a protobuf message.
+ template <typename SetStringFunction, typename SetRefFunction>
+ bool attachOneByteString(const char* string, SetStringFunction setString,
+ SetRefFunction setRef) {
+ auto ptr = oneByteStringsAlreadySerialized.lookupForAdd(string);
+ if (ptr) {
+ setRef(ptr->value());
+ return true;
+ }
+
+ auto length = strlen(string);
+ auto stringData = MakeUnique<std::string>(string, length);
+ if (!stringData) return false;
+
+ uint64_t ref = oneByteStringsAlreadySerialized.count();
+ if (!oneByteStringsAlreadySerialized.add(ptr, string, ref)) return false;
+
+ setString(stringData.release());
+ return true;
+ }
+
+ protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame,
+ size_t depth = 1) {
+ // NB: de-duplicated string properties must be written in the same order
+ // here as they are read in `HeapSnapshot::saveStackFrame` or else indices
+ // in references to already serialized strings will be off.
+
+ MOZ_ASSERT(frame,
+ "null frames should be represented as the lack of a serialized "
+ "stack frame");
+
+ auto id = frame.identifier();
+ auto protobufStackFrame = MakeUnique<protobuf::StackFrame>();
+ if (!protobufStackFrame) return nullptr;
+
+ if (framesAlreadySerialized.has(id)) {
+ protobufStackFrame->set_ref(id);
+ return protobufStackFrame.release();
+ }
+
+ auto data = MakeUnique<protobuf::StackFrame_Data>();
+ if (!data) return nullptr;
+
+ data->set_id(id);
+ data->set_line(frame.line());
+ data->set_column(frame.column().oneOriginValue());
+ data->set_issystem(frame.isSystem());
+ data->set_isselfhosted(frame.isSelfHosted(cx));
+
+ auto dupeSource = TwoByteString::from(frame.source());
+ if (!attachTwoByteString(
+ dupeSource,
+ [&](std::string* source) { data->set_allocated_source(source); },
+ [&](uint64_t ref) { data->set_sourceref(ref); })) {
+ return nullptr;
+ }
+
+ auto dupeName = TwoByteString::from(frame.functionDisplayName());
+ if (dupeName.isNonNull()) {
+ if (!attachTwoByteString(
+ dupeName,
+ [&](std::string* name) {
+ data->set_allocated_functiondisplayname(name);
+ },
+ [&](uint64_t ref) { data->set_functiondisplaynameref(ref); })) {
+ return nullptr;
+ }
+ }
+
+ auto parent = frame.parent();
+ if (parent && depth < HeapSnapshot::MAX_STACK_DEPTH) {
+ auto protobufParent = getProtobufStackFrame(parent, depth + 1);
+ if (!protobufParent) return nullptr;
+ data->set_allocated_parent(protobufParent);
+ }
+
+ protobufStackFrame->set_allocated_data(data.release());
+
+ if (!framesAlreadySerialized.put(id)) return nullptr;
+
+ return protobufStackFrame.release();
+ }
+
+ public:
+ StreamWriter(JSContext* cx,
+ ::google::protobuf::io::ZeroCopyOutputStream& stream,
+ bool wantNames, JS::CompartmentSet* compartments)
+ : cx(cx),
+ wantNames(wantNames),
+ framesAlreadySerialized(cx),
+ twoByteStringsAlreadySerialized(cx),
+ oneByteStringsAlreadySerialized(cx),
+ stream(stream),
+ compartments(compartments) {}
+
+ ~StreamWriter() override {}
+
+ bool writeMetadata(uint64_t timestamp) final {
+ protobuf::Metadata metadata;
+ metadata.set_timestamp(timestamp);
+ return writeMessage(metadata);
+ }
+
+ bool writeNode(const JS::ubi::Node& ubiNode, EdgePolicy includeEdges) final {
+ // NB: de-duplicated string properties must be written in the same order
+ // here as they are read in `HeapSnapshot::saveNode` or else indices in
+ // references to already serialized strings will be off.
+
+ protobuf::Node protobufNode;
+ protobufNode.set_id(ubiNode.identifier());
+
+ protobufNode.set_coarsetype(
+ JS::ubi::CoarseTypeToUint32(ubiNode.coarseType()));
+
+ auto typeName = TwoByteString(ubiNode.typeName());
+ if (NS_WARN_IF(!attachTwoByteString(
+ typeName,
+ [&](std::string* name) {
+ protobufNode.set_allocated_typename_(name);
+ },
+ [&](uint64_t ref) { protobufNode.set_typenameref(ref); }))) {
+ return false;
+ }
+
+ mozilla::MallocSizeOf mallocSizeOf = dbg::GetDebuggerMallocSizeOf(cx);
+ MOZ_ASSERT(mallocSizeOf);
+ protobufNode.set_size(ubiNode.size(mallocSizeOf));
+
+ if (includeEdges) {
+ auto edges = ubiNode.edges(cx, wantNames);
+ if (NS_WARN_IF(!edges)) return false;
+
+ for (; !edges->empty(); edges->popFront()) {
+ ubi::Edge& ubiEdge = edges->front();
+ if (!ShouldIncludeEdge(compartments, ubiNode, ubiEdge)) {
+ continue;
+ }
+
+ protobuf::Edge* protobufEdge = protobufNode.add_edges();
+ if (NS_WARN_IF(!protobufEdge)) {
+ return false;
+ }
+
+ protobufEdge->set_referent(ubiEdge.referent.identifier());
+
+ if (wantNames && ubiEdge.name) {
+ TwoByteString edgeName(std::move(ubiEdge.name));
+ if (NS_WARN_IF(!attachTwoByteString(
+ edgeName,
+ [&](std::string* name) {
+ protobufEdge->set_allocated_name(name);
+ },
+ [&](uint64_t ref) { protobufEdge->set_nameref(ref); }))) {
+ return false;
+ }
+ }
+ }
+ }
+
+ if (ubiNode.hasAllocationStack()) {
+ auto ubiStackFrame = ubiNode.allocationStack();
+ auto protoStackFrame = getProtobufStackFrame(ubiStackFrame);
+ if (NS_WARN_IF(!protoStackFrame)) return false;
+ protobufNode.set_allocated_allocationstack(protoStackFrame);
+ }
+
+ if (auto className = ubiNode.jsObjectClassName()) {
+ if (NS_WARN_IF(!attachOneByteString(
+ className,
+ [&](std::string* name) {
+ protobufNode.set_allocated_jsobjectclassname(name);
+ },
+ [&](uint64_t ref) {
+ protobufNode.set_jsobjectclassnameref(ref);
+ }))) {
+ return false;
+ }
+ }
+
+ if (auto scriptFilename = ubiNode.scriptFilename()) {
+ if (NS_WARN_IF(!attachOneByteString(
+ scriptFilename,
+ [&](std::string* name) {
+ protobufNode.set_allocated_scriptfilename(name);
+ },
+ [&](uint64_t ref) {
+ protobufNode.set_scriptfilenameref(ref);
+ }))) {
+ return false;
+ }
+ }
+
+ if (ubiNode.descriptiveTypeName()) {
+ auto descriptiveTypeName = TwoByteString(ubiNode.descriptiveTypeName());
+ if (NS_WARN_IF(!attachTwoByteString(
+ descriptiveTypeName,
+ [&](std::string* name) {
+ protobufNode.set_allocated_descriptivetypename(name);
+ },
+ [&](uint64_t ref) {
+ protobufNode.set_descriptivetypenameref(ref);
+ }))) {
+ return false;
+ }
+ }
+
+ return writeMessage(protobufNode);
+ }
+};
+
+// A JS::ubi::BreadthFirst handler that serializes a snapshot of the heap into a
+// core dump.
+class MOZ_STACK_CLASS HeapSnapshotHandler {
+ CoreDumpWriter& writer;
+ JS::CompartmentSet* compartments;
+
+ public:
+ // For telemetry.
+ uint32_t nodeCount;
+ uint32_t edgeCount;
+
+ HeapSnapshotHandler(CoreDumpWriter& writer, JS::CompartmentSet* compartments)
+ : writer(writer),
+ compartments(compartments),
+ nodeCount(0),
+ edgeCount(0) {}
+
+ // JS::ubi::BreadthFirst handler interface.
+
+ class NodeData {};
+ typedef JS::ubi::BreadthFirst<HeapSnapshotHandler> Traversal;
+ bool operator()(Traversal& traversal, JS::ubi::Node origin,
+ const JS::ubi::Edge& edge, NodeData*, bool first) {
+ edgeCount++;
+
+ // We're only interested in the first time we reach edge.referent, not in
+ // every edge arriving at that node. "But, don't we want to serialize every
+ // edge in the heap graph?" you ask. Don't worry! This edge is still
+ // serialized into the core dump. Serializing a node also serializes each of
+ // its edges, and if we are traversing a given edge, we must have already
+ // visited and serialized the origin node and its edges.
+ if (!first) return true;
+
+ CoreDumpWriter::EdgePolicy policy;
+ if (!ShouldIncludeEdge(compartments, origin, edge, &policy)) {
+ // Because ShouldIncludeEdge considers the |origin| node as well, we don't
+ // want to consider this node 'visited' until we write it to the core
+ // dump.
+ traversal.doNotMarkReferentAsVisited();
+ return true;
+ }
+
+ nodeCount++;
+
+ if (policy == CoreDumpWriter::EXCLUDE_EDGES) traversal.abandonReferent();
+
+ return writer.writeNode(edge.referent, policy);
+ }
+};
+
+bool WriteHeapGraph(JSContext* cx, const JS::ubi::Node& node,
+ CoreDumpWriter& writer, bool wantNames,
+ JS::CompartmentSet* compartments,
+ JS::AutoCheckCannotGC& noGC, uint32_t& outNodeCount,
+ uint32_t& outEdgeCount) {
+ // Serialize the starting node to the core dump.
+
+ if (NS_WARN_IF(!writer.writeNode(node, CoreDumpWriter::INCLUDE_EDGES))) {
+ return false;
+ }
+
+ // Walk the heap graph starting from the given node and serialize it into the
+ // core dump.
+
+ HeapSnapshotHandler handler(writer, compartments);
+ HeapSnapshotHandler::Traversal traversal(cx, handler, noGC);
+ traversal.wantNames = wantNames;
+
+ bool ok = traversal.addStartVisited(node) && traversal.traverse();
+
+ if (ok) {
+ outNodeCount = handler.nodeCount;
+ outEdgeCount = handler.edgeCount;
+ }
+
+ return ok;
+}
+
+static unsigned long msSinceProcessCreation(const TimeStamp& now) {
+ auto duration = now - TimeStamp::ProcessCreation();
+ return (unsigned long)duration.ToMilliseconds();
+}
+
+/* static */
+already_AddRefed<nsIFile> HeapSnapshot::CreateUniqueCoreDumpFile(
+ ErrorResult& rv, const TimeStamp& now, nsAString& outFilePath,
+ nsAString& outSnapshotId) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIFile> file;
+ rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(file));
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ nsAutoString tempPath;
+ rv = file->GetPath(tempPath);
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ auto ms = msSinceProcessCreation(now);
+ rv = file->AppendNative(nsPrintfCString("%lu.fxsnapshot", ms));
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666);
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ rv = file->GetPath(outFilePath);
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ // The snapshot ID must be computed in the process that created the
+ // temp file, because TmpD may not be the same in all processes.
+ outSnapshotId.Assign(Substring(
+ outFilePath, tempPath.Length() + 1,
+ outFilePath.Length() - tempPath.Length() - sizeof(".fxsnapshot")));
+
+ return file.forget();
+}
+
+// Deletion policy for cleaning up PHeapSnapshotTempFileHelperChild pointers.
+class DeleteHeapSnapshotTempFileHelperChild {
+ public:
+ constexpr DeleteHeapSnapshotTempFileHelperChild() {}
+
+ void operator()(PHeapSnapshotTempFileHelperChild* ptr) const {
+ Unused << NS_WARN_IF(!HeapSnapshotTempFileHelperChild::Send__delete__(ptr));
+ }
+};
+
+// A UniquePtr alias to automatically manage PHeapSnapshotTempFileHelperChild
+// pointers.
+using UniqueHeapSnapshotTempFileHelperChild =
+ UniquePtr<PHeapSnapshotTempFileHelperChild,
+ DeleteHeapSnapshotTempFileHelperChild>;
+
+// Get an nsIOutputStream that we can write the heap snapshot to. In non-e10s
+// and in the e10s parent process, open a file directly and create an output
+// stream for it. In e10s child processes, we are sandboxed without access to
+// the filesystem. Use IPDL to request a file descriptor from the parent
+// process.
+static already_AddRefed<nsIOutputStream> getCoreDumpOutputStream(
+ ErrorResult& rv, TimeStamp& start, nsAString& outFilePath,
+ nsAString& outSnapshotId) {
+ if (XRE_IsParentProcess()) {
+ // Create the file and open the output stream directly.
+
+ nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(
+ rv, start, outFilePath, outSnapshotId);
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file,
+ PR_WRONLY, -1, 0);
+ if (NS_WARN_IF(rv.Failed())) return nullptr;
+
+ return outputStream.forget();
+ }
+ // Request a file descriptor from the parent process over IPDL.
+
+ auto cc = ContentChild::GetSingleton();
+ if (!cc) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ UniqueHeapSnapshotTempFileHelperChild helper(
+ cc->SendPHeapSnapshotTempFileHelperConstructor());
+ if (NS_WARN_IF(!helper)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ OpenHeapSnapshotTempFileResponse response;
+ if (!helper->SendOpenHeapSnapshotTempFile(&response)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ if (response.type() == OpenHeapSnapshotTempFileResponse::Tnsresult) {
+ rv.Throw(response.get_nsresult());
+ return nullptr;
+ }
+
+ auto opened = response.get_OpenedFile();
+ outFilePath = opened.path();
+ outSnapshotId = opened.snapshotId();
+ nsCOMPtr<nsIOutputStream> outputStream =
+ FileDescriptorOutputStream::Create(opened.descriptor());
+ if (NS_WARN_IF(!outputStream)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ return outputStream.forget();
+}
+
+} // namespace devtools
+
+namespace dom {
+
+using namespace JS;
+using namespace devtools;
+
+/* static */
+void ChromeUtils::SaveHeapSnapshotShared(
+ GlobalObject& global, const HeapSnapshotBoundaries& boundaries,
+ nsAString& outFilePath, nsAString& outSnapshotId, ErrorResult& rv) {
+ auto start = TimeStamp::Now();
+
+ bool wantNames = true;
+ CompartmentSet compartments;
+ uint32_t nodeCount = 0;
+ uint32_t edgeCount = 0;
+
+ nsCOMPtr<nsIOutputStream> outputStream =
+ getCoreDumpOutputStream(rv, start, outFilePath, outSnapshotId);
+ if (NS_WARN_IF(rv.Failed())) return;
+
+ ZeroCopyNSIOutputStream zeroCopyStream(outputStream);
+ ::google::protobuf::io::GzipOutputStream gzipStream(&zeroCopyStream);
+
+ JSContext* cx = global.Context();
+
+ {
+ ubi::RootList rootList(cx, wantNames);
+ auto [ok, nogc] =
+ EstablishBoundaries(cx, rv, boundaries, rootList, compartments);
+ if (!ok) {
+ return;
+ }
+
+ StreamWriter writer(cx, gzipStream, wantNames,
+ !compartments.empty() ? &compartments : nullptr);
+
+ ubi::Node roots(&rootList);
+
+ // Serialize the initial heap snapshot metadata to the core dump.
+ if (!writer.writeMetadata(PR_Now()) ||
+ // Serialize the heap graph to the core dump, starting from our list of
+ // roots.
+ !WriteHeapGraph(cx, roots, writer, wantNames,
+ !compartments.empty() ? &compartments : nullptr, nogc,
+ nodeCount, edgeCount)) {
+ rv.Throw(zeroCopyStream.failed() ? zeroCopyStream.result()
+ : NS_ERROR_UNEXPECTED);
+ return;
+ }
+ }
+
+ Telemetry::AccumulateTimeDelta(Telemetry::DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS,
+ start);
+ Telemetry::Accumulate(Telemetry::DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT,
+ nodeCount);
+ Telemetry::Accumulate(Telemetry::DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT,
+ edgeCount);
+}
+
+/* static */
+uint64_t ChromeUtils::GetObjectNodeId(GlobalObject& global,
+ JS::Handle<JSObject*> val) {
+ JS::Rooted<JSObject*> obj(global.Context(), val);
+
+ JS::ubi::Node node(obj);
+ return node.identifier();
+}
+
+/* static */
+void ChromeUtils::SaveHeapSnapshot(GlobalObject& global,
+ const HeapSnapshotBoundaries& boundaries,
+ nsAString& outFilePath, ErrorResult& rv) {
+ nsAutoString snapshotId;
+ SaveHeapSnapshotShared(global, boundaries, outFilePath, snapshotId, rv);
+}
+
+/* static */
+void ChromeUtils::SaveHeapSnapshotGetId(
+ GlobalObject& global, const HeapSnapshotBoundaries& boundaries,
+ nsAString& outSnapshotId, ErrorResult& rv) {
+ nsAutoString filePath;
+ SaveHeapSnapshotShared(global, boundaries, filePath, outSnapshotId, rv);
+}
+
+/* static */
+already_AddRefed<HeapSnapshot> ChromeUtils::ReadHeapSnapshot(
+ GlobalObject& global, const nsAString& filePath, ErrorResult& rv) {
+ auto start = TimeStamp::Now();
+
+ nsresult nsrv;
+ nsCOMPtr<nsIFile> snapshotFile =
+ do_CreateInstance("@mozilla.org/file/local;1", &nsrv);
+
+ if (NS_FAILED(nsrv)) {
+ rv = nsrv;
+ return nullptr;
+ }
+
+ rv = snapshotFile->InitWithPath(filePath);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+
+ AutoMemMap mm;
+ rv = mm.init(snapshotFile);
+ if (rv.Failed()) return nullptr;
+
+ RefPtr<HeapSnapshot> snapshot = HeapSnapshot::Create(
+ global.Context(), global, reinterpret_cast<const uint8_t*>(mm.address()),
+ mm.size(), rv);
+
+ if (!rv.Failed())
+ Telemetry::AccumulateTimeDelta(Telemetry::DEVTOOLS_READ_HEAP_SNAPSHOT_MS,
+ start);
+
+ return snapshot.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/HeapSnapshot.h b/devtools/shared/heapsnapshot/HeapSnapshot.h
new file mode 100644
index 0000000000..a23fdc2c86
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.h
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_HeapSnapshot__
+#define mozilla_devtools_HeapSnapshot__
+
+#include "js/HashTable.h"
+#include "mozilla/devtools/DeserializedNode.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefCounted.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+#include "CoreDump.pb.h"
+#include "nsCOMPtr.h"
+#include "nsCRTGlue.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "nsXPCOM.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace devtools {
+
+class DominatorTree;
+
+using UniqueTwoByteString = UniqueFreePtr<char16_t[]>;
+using UniqueOneByteString = UniqueFreePtr<char[]>;
+
+class HeapSnapshot final : public nsISupports, public nsWrapperCache {
+ friend struct DeserializedNode;
+ friend struct DeserializedEdge;
+ friend struct DeserializedStackFrame;
+ friend class JS::ubi::Concrete<JS::ubi::DeserializedNode>;
+
+ explicit HeapSnapshot(JSContext* cx, nsISupports* aParent)
+ : timestamp(Nothing()),
+ rootId(0),
+ nodes(cx),
+ frames(cx),
+ mParent(aParent) {
+ MOZ_ASSERT(aParent);
+ };
+
+ // Initialize this HeapSnapshot from the given buffer that contains a
+ // serialized core dump. Do NOT take ownership of the buffer, only borrow it
+ // for the duration of the call. Return false on failure.
+ bool init(JSContext* cx, const uint8_t* buffer, uint32_t size);
+
+ using NodeIdSet = js::HashSet<NodeId>;
+
+ // Save the given `protobuf::Node` message in this `HeapSnapshot` as a
+ // `DeserializedNode`.
+ bool saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents);
+
+ // Save the given `protobuf::StackFrame` message in this `HeapSnapshot` as a
+ // `DeserializedStackFrame`. The saved stack frame's id is returned via the
+ // out parameter.
+ bool saveStackFrame(const protobuf::StackFrame& frame,
+ StackFrameId& outFrameId);
+
+ public:
+ // The maximum number of stack frames that we will serialize into a core
+ // dump. This helps prevent over-recursion in the protobuf library when
+ // deserializing stacks.
+ static const size_t MAX_STACK_DEPTH = 60;
+
+ private:
+ // If present, a timestamp in the same units that `PR_Now` gives.
+ Maybe<uint64_t> timestamp;
+
+ // The id of the root node for this deserialized heap graph.
+ NodeId rootId;
+
+ // The set of nodes in this deserialized heap graph, keyed by id.
+ using NodeSet = js::HashSet<DeserializedNode, DeserializedNode::HashPolicy>;
+ NodeSet nodes;
+
+ // The set of stack frames in this deserialized heap graph, keyed by id.
+ using FrameSet =
+ js::HashSet<DeserializedStackFrame, DeserializedStackFrame::HashPolicy>;
+ FrameSet frames;
+
+ Vector<UniqueTwoByteString> internedTwoByteStrings;
+ Vector<UniqueOneByteString> internedOneByteStrings;
+
+ using StringOrRef = Variant<const std::string*, uint64_t>;
+
+ template <typename CharT, typename InternedStringSet>
+ const CharT* getOrInternString(InternedStringSet& internedStrings,
+ Maybe<StringOrRef>& maybeStrOrRef);
+
+ protected:
+ nsCOMPtr<nsISupports> mParent;
+
+ virtual ~HeapSnapshot() {}
+
+ public:
+ // Create a `HeapSnapshot` from the given buffer that contains a serialized
+ // core dump. Do NOT take ownership of the buffer, only borrow it for the
+ // duration of the call.
+ static already_AddRefed<HeapSnapshot> Create(JSContext* cx,
+ dom::GlobalObject& global,
+ const uint8_t* buffer,
+ uint32_t size, ErrorResult& rv);
+
+ // Creates the `$TEMP_DIR/XXXXXX-XXX.fxsnapshot` core dump file that heap
+ // snapshots are serialized into.
+ static already_AddRefed<nsIFile> CreateUniqueCoreDumpFile(
+ ErrorResult& rv, const TimeStamp& now, nsAString& outFilePath,
+ nsAString& outSnapshotId);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(HeapSnapshot)
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(HeapSnapshot)
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ const char16_t* borrowUniqueString(const char16_t* duplicateString,
+ size_t length);
+
+ // Get the root node of this heap snapshot's graph.
+ JS::ubi::Node getRoot() {
+ auto p = nodes.lookup(rootId);
+ MOZ_ASSERT(p);
+ const DeserializedNode& node = *p;
+ return JS::ubi::Node(const_cast<DeserializedNode*>(&node));
+ }
+
+ Maybe<JS::ubi::Node> getNodeById(JS::ubi::Node::Id nodeId) {
+ auto p = nodes.lookup(nodeId);
+ if (!p) return Nothing();
+ return Some(JS::ubi::Node(const_cast<DeserializedNode*>(&*p)));
+ }
+
+ void TakeCensus(JSContext* cx, JS::Handle<JSObject*> options,
+ JS::MutableHandle<JS::Value> rval, ErrorResult& rv);
+
+ void DescribeNode(JSContext* cx, JS::Handle<JSObject*> breakdown,
+ uint64_t nodeId, JS::MutableHandle<JS::Value> rval,
+ ErrorResult& rv);
+
+ already_AddRefed<DominatorTree> ComputeDominatorTree(ErrorResult& rv);
+
+ void ComputeShortestPaths(JSContext* cx, uint64_t start,
+ const dom::Sequence<uint64_t>& targets,
+ uint64_t maxNumPaths,
+ JS::MutableHandle<JSObject*> results,
+ ErrorResult& rv);
+
+ dom::Nullable<uint64_t> GetCreationTime() {
+ static const uint64_t maxTime = uint64_t(1) << 53;
+ if (timestamp.isSome() && timestamp.ref() <= maxTime) {
+ return dom::Nullable<uint64_t>(timestamp.ref());
+ }
+
+ return dom::Nullable<uint64_t>();
+ }
+};
+
+// A `CoreDumpWriter` is given the data we wish to save in a core dump and
+// serializes it to disk, or memory, or a socket, etc.
+class CoreDumpWriter {
+ public:
+ virtual ~CoreDumpWriter(){};
+
+ // Write the given bits of metadata we would like to associate with this core
+ // dump.
+ virtual bool writeMetadata(uint64_t timestamp) = 0;
+
+ enum EdgePolicy : bool { INCLUDE_EDGES = true, EXCLUDE_EDGES = false };
+
+ // Write the given `JS::ubi::Node` to the core dump. The given `EdgePolicy`
+ // dictates whether its outgoing edges should also be written to the core
+ // dump, or excluded.
+ virtual bool writeNode(const JS::ubi::Node& node,
+ EdgePolicy includeEdges) = 0;
+};
+
+// Serialize the heap graph as seen from `node` with the given `CoreDumpWriter`.
+// If `wantNames` is true, capture edge names. If `zones` is non-null, only
+// capture the sub-graph within the zone set, otherwise capture the whole heap
+// graph. Returns false on failure.
+bool WriteHeapGraph(JSContext* cx, const JS::ubi::Node& node,
+ CoreDumpWriter& writer, bool wantNames,
+ JS::CompartmentSet* compartments,
+ JS::AutoCheckCannotGC& noGC, uint32_t& outNodeCount,
+ uint32_t& outEdgeCount);
+inline bool WriteHeapGraph(JSContext* cx, const JS::ubi::Node& node,
+ CoreDumpWriter& writer, bool wantNames,
+ JS::CompartmentSet* compartments,
+ JS::AutoCheckCannotGC& noGC) {
+ uint32_t ignoreNodeCount;
+ uint32_t ignoreEdgeCount;
+ return WriteHeapGraph(cx, node, writer, wantNames, compartments, noGC,
+ ignoreNodeCount, ignoreEdgeCount);
+}
+
+// Get the mozilla::MallocSizeOf for the current thread's JSRuntime.
+MallocSizeOf GetCurrentThreadDebuggerMallocSizeOf();
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_HeapSnapshot__
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js b/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js
new file mode 100644
index 0000000000..ab804eded1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.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/. */
+
+// Heap snapshots are always saved in the temp directory, and have a regular
+// naming convention. This module provides helpers for working with heap
+// snapshot files in a safe manner. Because we attempt to avoid unnecessary
+// copies of the heap snapshot files by checking the local filesystem for a heap
+// snapshot file with the given snapshot id, we want to ensure that we are only
+// attempting to open heap snapshot files and not `~/.ssh/id_rsa`, for
+// example. Therefore, the RDP only talks about snapshot ids, or transfering the
+// bulk file data. A file path can be recovered from a snapshot id, which allows
+// one to check for the presence of the heap snapshot file on the local file
+// system, but we don't have to worry about opening arbitrary files.
+//
+// The heap snapshot file path conventions permits the following forms:
+//
+// $TEMP_DIRECTORY/XXXXXXXXXX.fxsnapshot
+// $TEMP_DIRECTORY/XXXXXXXXXX-XXXXX.fxsnapshot
+//
+// Where the strings of "X" are zero or more digits.
+
+"use strict";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+function getHeapSnapshotFileTemplate() {
+ return PathUtils.join(PathUtils.tempDir, `${Date.now()}.fxsnapshot`);
+}
+
+/**
+ * Get a unique temp file path for a new heap snapshot. The file is guaranteed
+ * not to exist before this call.
+ *
+ * @returns String
+ */
+exports.getNewUniqueHeapSnapshotTempFilePath = function () {
+ const file = new lazy.FileUtils.File(getHeapSnapshotFileTemplate());
+ // The call to createUnique will append "-N" after the leaf name (but before
+ // the extension) until a new file is found and create it. This guarantees we
+ // won't accidentally choose the same file twice.
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ return file.path;
+};
+
+function isValidSnapshotFileId(snapshotId) {
+ return /^\d+(\-\d+)?$/.test(snapshotId);
+}
+
+/**
+ * Get the file path for the given snapshot id.
+ *
+ * @param {String} snapshotId
+ *
+ * @returns String | null
+ */
+exports.getHeapSnapshotTempFilePath = function (snapshotId) {
+ // Don't want anyone sneaking "../../../.." strings into the snapshot id and
+ // trying to make us open arbitrary files.
+ if (!isValidSnapshotFileId(snapshotId)) {
+ return null;
+ }
+ return PathUtils.join(PathUtils.tempDir, snapshotId + ".fxsnapshot");
+};
+
+/**
+ * Return true if we have the heap snapshot file for the given snapshot id on
+ * the local file system. False is returned otherwise.
+ *
+ * @returns Promise<Boolean>
+ */
+exports.haveHeapSnapshotTempFile = function (snapshotId) {
+ const path = exports.getHeapSnapshotTempFilePath(snapshotId);
+ if (!path) {
+ return Promise.resolve(false);
+ }
+
+ return IOUtils.stat(path).then(
+ () => true,
+ () => false
+ );
+};
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h
new file mode 100644
index 0000000000..becf00eb85
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_HeapSnapshotTempFileHelperChild_h
+#define mozilla_devtools_HeapSnapshotTempFileHelperChild_h
+
+#include "mozilla/devtools/PHeapSnapshotTempFileHelperChild.h"
+
+namespace mozilla {
+namespace devtools {
+
+class HeapSnapshotTempFileHelperChild
+ : public PHeapSnapshotTempFileHelperChild {
+ explicit HeapSnapshotTempFileHelperChild() {}
+
+ public:
+ static inline PHeapSnapshotTempFileHelperChild* Create();
+};
+
+/* static */ inline PHeapSnapshotTempFileHelperChild*
+HeapSnapshotTempFileHelperChild::Create() {
+ return new HeapSnapshotTempFileHelperChild();
+}
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_HeapSnapshotTempFileHelperChild_h
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp
new file mode 100644
index 0000000000..ea1cf8378a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/devtools/HeapSnapshotTempFileHelperParent.h"
+#include "mozilla/ErrorResult.h"
+#include "private/pprio.h"
+
+#include "nsIFile.h"
+
+namespace mozilla {
+namespace devtools {
+
+static bool openFileFailure(ErrorResult& rv,
+ OpenHeapSnapshotTempFileResponse* outResponse) {
+ *outResponse = rv.StealNSResult();
+ return true;
+}
+
+mozilla::ipc::IPCResult
+HeapSnapshotTempFileHelperParent::RecvOpenHeapSnapshotTempFile(
+ OpenHeapSnapshotTempFileResponse* outResponse) {
+ auto start = TimeStamp::Now();
+ ErrorResult rv;
+ nsAutoString filePath;
+ nsAutoString snapshotId;
+ nsCOMPtr<nsIFile> file =
+ HeapSnapshot::CreateUniqueCoreDumpFile(rv, start, filePath, snapshotId);
+ if (NS_WARN_IF(rv.Failed())) {
+ if (!openFileFailure(rv, outResponse)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+ }
+
+ PRFileDesc* prfd;
+ rv = file->OpenNSPRFileDesc(PR_WRONLY, 0, &prfd);
+ if (NS_WARN_IF(rv.Failed())) {
+ if (!openFileFailure(rv, outResponse)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+ }
+
+ FileDescriptor::PlatformHandleType handle =
+ FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(prfd));
+ FileDescriptor fd(handle);
+ *outResponse = OpenedFile(filePath, snapshotId, fd);
+ return IPC_OK();
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h
new file mode 100644
index 0000000000..058f0c6eea
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_HeapSnapshotTempFileHelperParent_h
+#define mozilla_devtools_HeapSnapshotTempFileHelperParent_h
+
+#include "mozilla/devtools/PHeapSnapshotTempFileHelperParent.h"
+
+namespace mozilla {
+namespace devtools {
+
+class HeapSnapshotTempFileHelperParent
+ : public PHeapSnapshotTempFileHelperParent {
+ friend class PHeapSnapshotTempFileHelperParent;
+
+ explicit HeapSnapshotTempFileHelperParent() {}
+ void ActorDestroy(ActorDestroyReason why) override {}
+ mozilla::ipc::IPCResult RecvOpenHeapSnapshotTempFile(
+ OpenHeapSnapshotTempFileResponse* outResponse);
+
+ public:
+ static inline PHeapSnapshotTempFileHelperParent* Create();
+};
+
+/* static */ inline PHeapSnapshotTempFileHelperParent*
+HeapSnapshotTempFileHelperParent::Create() {
+ return new HeapSnapshotTempFileHelperParent();
+}
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_HeapSnapshotTempFileHelperParent_h
diff --git a/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl b/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl
new file mode 100644
index 0000000000..7257e43088
--- /dev/null
+++ b/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=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/. */
+
+include protocol PContent;
+
+namespace mozilla {
+namespace devtools {
+
+struct OpenedFile
+{
+ nsString path;
+ nsString snapshotId;
+ FileDescriptor descriptor;
+};
+
+union OpenHeapSnapshotTempFileResponse
+{
+ nsresult;
+ OpenedFile;
+};
+
+[ManualDealloc]
+sync protocol PHeapSnapshotTempFileHelper
+{
+ manager PContent;
+
+parent:
+ sync OpenHeapSnapshotTempFile() returns (OpenHeapSnapshotTempFileResponse response);
+
+ async __delete__();
+};
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp
new file mode 100644
index 0000000000..8edde20635
--- /dev/null
+++ b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/ZeroCopyNSIOutputStream.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+namespace devtools {
+
+ZeroCopyNSIOutputStream::ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out)
+ : out(out), result_(NS_OK), amountUsed(0), writtenCount(0) {
+ DebugOnly<bool> nonBlocking = false;
+ MOZ_ASSERT(out->IsNonBlocking(&nonBlocking) == NS_OK);
+ MOZ_ASSERT(!nonBlocking);
+}
+
+ZeroCopyNSIOutputStream::~ZeroCopyNSIOutputStream() {
+ if (!failed()) Unused << NS_WARN_IF(NS_FAILED(writeBuffer()));
+}
+
+nsresult ZeroCopyNSIOutputStream::writeBuffer() {
+ if (failed()) return result_;
+
+ if (amountUsed == 0) return NS_OK;
+
+ int32_t amountWritten = 0;
+ while (amountWritten < amountUsed) {
+ uint32_t justWritten = 0;
+
+ result_ = out->Write(buffer + amountWritten, amountUsed - amountWritten,
+ &justWritten);
+ if (NS_WARN_IF(NS_FAILED(result_))) return result_;
+
+ amountWritten += justWritten;
+ }
+
+ writtenCount += amountUsed;
+ amountUsed = 0;
+ return NS_OK;
+}
+
+// ZeroCopyOutputStream Interface
+
+bool ZeroCopyNSIOutputStream::Next(void** data, int* size) {
+ MOZ_ASSERT(data != nullptr);
+ MOZ_ASSERT(size != nullptr);
+
+ if (failed()) return false;
+
+ if (amountUsed == BUFFER_SIZE) {
+ if (NS_FAILED(writeBuffer())) return false;
+ }
+
+ *data = buffer + amountUsed;
+ *size = BUFFER_SIZE - amountUsed;
+ amountUsed = BUFFER_SIZE;
+ return true;
+}
+
+void ZeroCopyNSIOutputStream::BackUp(int count) {
+ MOZ_ASSERT(count >= 0, "Cannot back up a negative amount of bytes.");
+ MOZ_ASSERT(amountUsed == BUFFER_SIZE,
+ "Can only call BackUp directly after calling Next.");
+ MOZ_ASSERT(count <= amountUsed,
+ "Can't back up further than we've given out.");
+
+ amountUsed -= count;
+}
+
+::google::protobuf::int64 ZeroCopyNSIOutputStream::ByteCount() const {
+ return writtenCount + amountUsed;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h
new file mode 100644
index 0000000000..96158e389e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_ZeroCopyNSIOutputStream__
+#define mozilla_devtools_ZeroCopyNSIOutputStream__
+
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/stubs/common.h>
+
+#include "nsCOMPtr.h"
+#include "nsIOutputStream.h"
+
+namespace mozilla {
+namespace devtools {
+
+// A `google::protobuf::io::ZeroCopyOutputStream` implementation that uses an
+// `nsIOutputStream` under the covers.
+//
+// This class will automatically write and flush its data to the
+// `nsIOutputStream` in its destructor, but if you care whether that call
+// succeeds or fails, then you should call the `flush` method yourself. Errors
+// will be logged, however.
+class MOZ_STACK_CLASS ZeroCopyNSIOutputStream
+ : public ::google::protobuf::io::ZeroCopyOutputStream {
+ static const int BUFFER_SIZE = 8192;
+
+ // The nsIOutputStream we are streaming to.
+ nsCOMPtr<nsIOutputStream>& out;
+
+ // The buffer we write data to before passing it to the output stream.
+ char buffer[BUFFER_SIZE];
+
+ // The status of writing to the underlying output stream.
+ nsresult result_;
+
+ // The number of bytes in the buffer that have been used thus far.
+ int amountUsed;
+
+ // Excluding the amount of the buffer currently used (which hasn't been
+ // written and flushed yet), this is the number of bytes written to the output
+ // stream.
+ int64_t writtenCount;
+
+ // Write the internal buffer to the output stream and flush it.
+ nsresult writeBuffer();
+
+ public:
+ explicit ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out);
+
+ nsresult flush() { return writeBuffer(); }
+
+ // Return true if writing to the underlying output stream ever failed.
+ bool failed() const { return NS_FAILED(result_); }
+
+ nsresult result() const { return result_; }
+
+ // ZeroCopyOutputStream Interface
+ virtual ~ZeroCopyNSIOutputStream() override;
+ virtual bool Next(void** data, int* size) override;
+ virtual void BackUp(int count) override;
+ virtual ::google::protobuf::int64 ByteCount() const override;
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_ZeroCopyNSIOutputStream__
diff --git a/devtools/shared/heapsnapshot/census-tree-node.js b/devtools/shared/heapsnapshot/census-tree-node.js
new file mode 100644
index 0000000000..82f5929370
--- /dev/null
+++ b/devtools/shared/heapsnapshot/census-tree-node.js
@@ -0,0 +1,764 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// CensusTreeNode is an intermediate representation of a census report that
+// exists between after a report is generated by taking a census and before the
+// report is rendered in the DOM. It must be dead simple to render, with no
+// further data processing or massaging needed before rendering DOM nodes. Our
+// goal is to do the census report to CensusTreeNode transformation in the
+// HeapAnalysesWorker, and ensure that the **only** work that the main thread
+// has to do is strictly DOM rendering work.
+
+const {
+ Visitor,
+ walk,
+ basisTotalBytes,
+ basisTotalCount,
+} = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+
+// Monotonically increasing integer for CensusTreeNode `id`s.
+let censusTreeNodeIdCounter = 0;
+
+/**
+ * Return true if the given object is a SavedFrame stack object, false otherwise.
+ *
+ * @param {any} obj
+ * @returns {Boolean}
+ */
+function isSavedFrame(obj) {
+ return Object.prototype.toString.call(obj) === "[object SavedFrame]";
+}
+
+/**
+ * A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when
+ * aggregating multiple SavedFrame allocation stack keys into a tree of many
+ * CensusTreeNodes. Each stack may share older frames, and we want to preserve
+ * this sharing when converting to CensusTreeNode, so before creating a new
+ * CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches.
+ */
+function CensusTreeNodeCache() {}
+CensusTreeNodeCache.prototype = null;
+
+/**
+ * The value of a single entry stored in a CensusTreeNodeCache. It is a pair of
+ * the CensusTreeNode for this cache value, and the subsequent
+ * CensusTreeNodeCache for this node's children.
+ *
+ * @param {SavedFrame} frame
+ * The frame being cached.
+ */
+function CensusTreeNodeCacheValue() {
+ // The CensusTreeNode for this cache value.
+ this.node = undefined;
+ // The CensusTreeNodeCache for this frame's children.
+ this.children = undefined;
+}
+
+CensusTreeNodeCacheValue.prototype = null;
+
+/**
+ * Create a unique string for the given SavedFrame (ignoring the frame's parent
+ * chain) that can be used as a hash to key this frame within a CensusTreeNodeCache.
+ *
+ * NB: We manually hash rather than using an ES6 Map because we are purposely
+ * ignoring the parent chain and wish to consider frames with everything the
+ * same except their parents as the same.
+ *
+ * @param {SavedFrame} frame
+ * The SavedFrame object we would like to lookup in or insert into a
+ * CensusTreeNodeCache.
+ *
+ * @returns {String}
+ * The unique string that can be used as a key in a CensusTreeNodeCache.
+ */
+CensusTreeNodeCache.hashFrame = function (frame) {
+ // eslint-disable-next-line max-len
+ return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
+};
+
+/**
+ * Create a unique string for the given CensusTreeNode **with regards to
+ * siblings at the current depth of the tree, not within the whole tree.** It
+ * can be used as a hash to key this node within a CensusTreeNodeCache.
+ *
+ * @param {CensusTreeNode} node
+ * The node we would like to lookup in or insert into a cache.
+ *
+ * @returns {String}
+ * The unique string that can be used as a key in a CensusTreeNodeCache.
+ */
+CensusTreeNodeCache.hashNode = function (node) {
+ return isSavedFrame(node.name)
+ ? CensusTreeNodeCache.hashFrame(node.name)
+ : `NODE,${node.name}`;
+};
+
+/**
+ * Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame
+ * object in the given cache.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNodeCacheValue} value
+ */
+CensusTreeNodeCache.insertFrame = function (cache, value) {
+ cache[CensusTreeNodeCache.hashFrame(value.node.name)] = value;
+};
+
+/**
+ * Insert the given value in the cache.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNodeCacheValue} value
+ */
+CensusTreeNodeCache.insertNode = function (cache, value) {
+ if (isSavedFrame(value.node.name)) {
+ CensusTreeNodeCache.insertFrame(cache, value);
+ } else {
+ cache[CensusTreeNodeCache.hashNode(value.node)] = value;
+ }
+};
+
+/**
+ * Lookup `frame` in `cache` and return its value if it exists.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {SavedFrame} frame
+ *
+ * @returns {undefined|CensusTreeNodeCacheValue}
+ */
+CensusTreeNodeCache.lookupFrame = function (cache, frame) {
+ return cache[CensusTreeNodeCache.hashFrame(frame)];
+};
+
+/**
+ * Lookup `node` in `cache` and return its value if it exists.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNode} node
+ *
+ * @returns {undefined|CensusTreeNodeCacheValue}
+ */
+CensusTreeNodeCache.lookupNode = function (cache, node) {
+ return isSavedFrame(node.name)
+ ? CensusTreeNodeCache.lookupFrame(cache, node.name)
+ : cache[CensusTreeNodeCache.hashNode(node)];
+};
+
+/**
+ * Add `child` to `parent`'s set of children and store the parent ID
+ * on the child.
+ *
+ * @param {CensusTreeNode} parent
+ * @param {CensusTreeNode} child
+ */
+function addChild(parent, child) {
+ if (!parent.children) {
+ parent.children = [];
+ }
+ child.parent = parent.id;
+ parent.children.push(child);
+}
+
+/**
+ * Get an array of each frame in the provided stack.
+ *
+ * @param {SavedFrame} stack
+ * @returns {Array<SavedFrame>}
+ */
+function getArrayOfFrames(stack) {
+ const frames = [];
+ let frame = stack;
+ while (frame) {
+ frames.push(frame);
+ frame = frame.parent;
+ }
+ frames.reverse();
+ return frames;
+}
+
+/**
+ * Given an `edge` to a sub-`report` whose structure is described by
+ * `breakdown`, create a CensusTreeNode tree.
+ *
+ * @param {Object} breakdown
+ * The breakdown specifying the structure of the given report.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {null|String|SavedFrame} edge
+ * The edge leading to this report from the parent report.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * The cache of CensusTreeNodes we have already made for the siblings of
+ * the node being created. The existing nodes are reused when possible.
+ *
+ * @param {Object} outParams
+ * The return values are attached to this object after this function
+ * returns. Because we create a CensusTreeNode for each frame in a
+ * SavedFrame stack edge, there may multiple nodes per sub-report.
+ *
+ * - top: The deepest node in the CensusTreeNode subtree created.
+ *
+ * - bottom: The shallowest node in the CensusTreeNode subtree created.
+ * This is null if the shallowest node in the subtree was
+ * found in the `cache` and reused.
+ *
+ * Note that top and bottom are not necessarily different. In the case
+ * where there is a 1:1 correspondence between an edge in the report and
+ * a CensusTreeNode, top and bottom refer to the same node.
+ */
+function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) {
+ if (!isSavedFrame(edge)) {
+ const node = new CensusTreeNode(edge);
+ outParams.top = outParams.bottom = node;
+ return;
+ }
+
+ const frames = getArrayOfFrames(edge);
+ let currentCache = cache;
+ let prevNode;
+ for (let i = 0, length = frames.length; i < length; i++) {
+ const frame = frames[i];
+
+ // Get or create the CensusTreeNodeCacheValue for this frame. If we already
+ // have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this
+ // frame, we don't need to add the node to the previous node's children as
+ // we have already done that. If we don't have a CensusTreeNodeCacheValue
+ // and CensusTreeNode for this frame, then create one and make sure to hook
+ // it up as a child of the previous node.
+ let isNewNode = false;
+ let val = CensusTreeNodeCache.lookupFrame(currentCache, frame);
+ if (!val) {
+ isNewNode = true;
+ val = new CensusTreeNodeCacheValue();
+ val.node = new CensusTreeNode(frame);
+
+ CensusTreeNodeCache.insertFrame(currentCache, val);
+ if (prevNode) {
+ addChild(prevNode, val.node);
+ }
+ }
+
+ if (i === 0) {
+ outParams.bottom = isNewNode ? val.node : null;
+ }
+ if (i === length - 1) {
+ outParams.top = val.node;
+ }
+
+ prevNode = val.node;
+
+ if (i !== length - 1 && !val.children) {
+ // This is not the last frame and therefore this node will have
+ // children, which we must cache.
+ val.children = new CensusTreeNodeCache();
+ }
+
+ currentCache = val.children;
+ }
+}
+
+/**
+ * A Visitor that walks a census report and creates the corresponding
+ * CensusTreeNode tree.
+ */
+function CensusTreeNodeVisitor() {
+ // The root of the resulting CensusTreeNode tree.
+ this._root = null;
+
+ // The stack of CensusTreeNodes that we are in the process of building while
+ // walking the census report.
+ this._nodeStack = [];
+
+ // To avoid unnecessary allocations, we reuse the same out parameter object
+ // passed to `makeCensusTreeNodeSubTree` every time we call it.
+ this._outParams = {
+ top: null,
+ bottom: null,
+ };
+
+ // The stack of `CensusTreeNodeCache`s that we use to aggregate many
+ // SavedFrame stacks into a single CensusTreeNode tree.
+ this._cacheStack = [new CensusTreeNodeCache()];
+
+ // The current index in the DFS of the census report tree.
+ this._index = -1;
+}
+
+CensusTreeNodeVisitor.prototype = Object.create(Visitor);
+
+/**
+ * Create the CensusTreeNode subtree for this sub-report and link it to the
+ * parent CensusTreeNode.
+ *
+ * @overrides Visitor.prototype.enter
+ */
+CensusTreeNodeVisitor.prototype.enter = function (breakdown, report, edge) {
+ this._index++;
+
+ const cache = this._cacheStack[this._cacheStack.length - 1];
+ makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams);
+ const { top, bottom } = this._outParams;
+
+ if (!this._root) {
+ this._root = bottom;
+ } else if (bottom) {
+ addChild(this._nodeStack[this._nodeStack.length - 1], bottom);
+ }
+
+ this._cacheStack.push(new CensusTreeNodeCache());
+ this._nodeStack.push(top);
+};
+
+function values(cache) {
+ return Object.keys(cache).map(k => cache[k]);
+}
+
+function isNonEmpty(node) {
+ return (
+ (node.children !== undefined && node.children.length) ||
+ node.bytes !== 0 ||
+ node.count !== 0
+ );
+}
+
+/**
+ * We have finished adding children to the CensusTreeNode subtree for the
+ * current sub-report. Make sure that the children are sorted for every node in
+ * the subtree.
+ *
+ * @overrides Visitor.prototype.exit
+ */
+CensusTreeNodeVisitor.prototype.exit = function (breakdown, report, edge) {
+ // Ensure all children are sorted and have their counts/bytes aggregated. We
+ // only need to consider cache children here, because other children
+ // correspond to other sub-reports and we already fixed them up in an earlier
+ // invocation of `exit`.
+
+ function dfs(node, childrenCache) {
+ if (childrenCache) {
+ const childValues = values(childrenCache);
+ for (let i = 0, length = childValues.length; i < length; i++) {
+ dfs(childValues[i].node, childValues[i].children);
+ }
+ }
+
+ node.totalCount = node.count;
+ node.totalBytes = node.bytes;
+
+ if (node.children) {
+ // Prune empty leaves.
+ node.children = node.children.filter(isNonEmpty);
+
+ node.children.sort(compareByTotal);
+
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ node.totalCount += node.children[i].totalCount;
+ node.totalBytes += node.children[i].totalBytes;
+ }
+ }
+ }
+
+ const top = this._nodeStack.pop();
+ const cache = this._cacheStack.pop();
+ dfs(top, cache);
+};
+
+/**
+ * @overrides Visitor.prototype.count
+ */
+CensusTreeNodeVisitor.prototype.count = function (breakdown, report, edge) {
+ const node = this._nodeStack[this._nodeStack.length - 1];
+ node.reportLeafIndex = this._index;
+
+ if (breakdown.count) {
+ node.count = report.count;
+ }
+
+ if (breakdown.bytes) {
+ node.bytes = report.bytes;
+ }
+};
+
+/**
+ * Get the root of the resulting CensusTreeNode tree.
+ *
+ * @returns {CensusTreeNode}
+ */
+CensusTreeNodeVisitor.prototype.root = function () {
+ if (!this._root) {
+ throw new Error(
+ "Attempt to get the root before walking the census report!"
+ );
+ }
+
+ if (this._nodeStack.length) {
+ throw new Error("Attempt to get the root while walking the census report!");
+ }
+
+ return this._root;
+};
+
+/**
+ * Create a single, uninitialized CensusTreeNode.
+ *
+ * @param {null|String|SavedFrame} name
+ */
+function CensusTreeNode(name) {
+ // Display name for this CensusTreeNode. Either null, a string, or a
+ // SavedFrame.
+ this.name = name;
+
+ // The number of bytes occupied by matching things in the heap snapshot.
+ this.bytes = 0;
+
+ // The sum of `this.bytes` and `child.totalBytes` for each child in
+ // `this.children`.
+ this.totalBytes = 0;
+
+ // The number of things in the heap snapshot that match this node in the
+ // census tree.
+ this.count = 0;
+
+ // The sum of `this.count` and `child.totalCount` for each child in
+ // `this.children`.
+ this.totalCount = 0;
+
+ // An array of this node's children, or undefined if it has no children.
+ this.children = undefined;
+
+ // The unique ID of this node.
+ this.id = ++censusTreeNodeIdCounter;
+
+ // If present, the unique ID of this node's parent. If this node does not have
+ // a parent, then undefined.
+ this.parent = undefined;
+
+ // The `reportLeafIndex` property allows mapping a CensusTreeNode node back to
+ // a leaf in the census report it was generated from. It is always one of the
+ // following variants:
+ //
+ // * A `Number` index pointing a leaf report in a pre-order DFS traversal of
+ // this CensusTreeNode's census report.
+ //
+ // * A `Set` object containing such indices, when this is part of an inverted
+ // CensusTreeNode tree and multiple leaves in the report map onto this node.
+ //
+ // * Finally, `undefined` when no leaves in the census report correspond with
+ // this node.
+ //
+ // The first and third cases are the common cases. The second case is rather
+ // uncommon, and to avoid doubling the number of allocations when creating
+ // CensusTreeNode trees, and objects that get structured cloned when sending
+ // such trees from the HeapAnalysesWorker to the main thread, we only allocate
+ // a Set object once a node actually does have multiple leaves it corresponds
+ // to.
+ this.reportLeafIndex = undefined;
+}
+
+CensusTreeNode.prototype = null;
+
+/**
+ * Compare the given nodes by their `totalBytes` properties, and breaking ties
+ * with the `totalCount`, `bytes`, and `count` properties (in that order).
+ *
+ * @param {CensusTreeNode} node1
+ * @param {CensusTreeNode} node2
+ *
+ * @returns {Number}
+ * A number suitable for using with Array.prototype.sort.
+ */
+function compareByTotal(node1, node2) {
+ return (
+ Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes) ||
+ Math.abs(node2.totalCount) - Math.abs(node1.totalCount) ||
+ Math.abs(node2.bytes) - Math.abs(node1.bytes) ||
+ Math.abs(node2.count) - Math.abs(node1.count)
+ );
+}
+
+/**
+ * Compare the given nodes by their `bytes` properties, and breaking ties with
+ * the `count`, `totalBytes`, and `totalCount` properties (in that order).
+ *
+ * @param {CensusTreeNode} node1
+ * @param {CensusTreeNode} node2
+ *
+ * @returns {Number}
+ * A number suitable for using with Array.prototype.sort.
+ */
+function compareBySelf(node1, node2) {
+ return (
+ Math.abs(node2.bytes) - Math.abs(node1.bytes) ||
+ Math.abs(node2.count) - Math.abs(node1.count) ||
+ Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes) ||
+ Math.abs(node2.totalCount) - Math.abs(node1.totalCount)
+ );
+}
+
+/**
+ * Given a parent cache value from a tree we are building and a child node from
+ * a tree we are basing the new tree off of, if we already have a corresponding
+ * node in the parent's children cache, merge this node's counts with
+ * it. Otherwise, create the corresponding node, add it to the parent's children
+ * cache, and create the parent->child edge.
+ *
+ * @param {CensusTreeNodeCacheValue} parentCachevalue
+ * @param {CensusTreeNode} node
+ *
+ * @returns {CensusTreeNodeCacheValue}
+ * The new or extant child node's corresponding cache value.
+ */
+function insertOrMergeNode(parentCacheValue, node) {
+ if (!parentCacheValue.children) {
+ parentCacheValue.children = new CensusTreeNodeCache();
+ }
+
+ let val = CensusTreeNodeCache.lookupNode(parentCacheValue.children, node);
+
+ if (val) {
+ // When inverting, it is possible that multiple leaves in the census report
+ // get merged into a single CensusTreeNode node. When this occurs, switch
+ // from a single index to a set of indices.
+ if (
+ val.node.reportLeafIndex !== undefined &&
+ val.node.reportLeafIndex !== node.reportLeafIndex
+ ) {
+ if (typeof val.node.reportLeafIndex === "number") {
+ const oldIndex = val.node.reportLeafIndex;
+ val.node.reportLeafIndex = new Set();
+ val.node.reportLeafIndex.add(oldIndex);
+ val.node.reportLeafIndex.add(node.reportLeafIndex);
+ } else {
+ val.node.reportLeafIndex.add(node.reportLeafIndex);
+ }
+ }
+
+ val.node.count += node.count;
+ val.node.bytes += node.bytes;
+ } else {
+ val = new CensusTreeNodeCacheValue();
+
+ val.node = new CensusTreeNode(node.name);
+ val.node.reportLeafIndex = node.reportLeafIndex;
+ val.node.count = node.count;
+ val.node.totalCount = node.totalCount;
+ val.node.bytes = node.bytes;
+ val.node.totalBytes = node.totalBytes;
+
+ addChild(parentCacheValue.node, val.node);
+ CensusTreeNodeCache.insertNode(parentCacheValue.children, val);
+ }
+
+ return val;
+}
+
+/**
+ * Given an un-inverted CensusTreeNode tree, return the corresponding inverted
+ * CensusTreeNode tree. The input tree is not modified. The resulting inverted
+ * tree is sorted by self bytes rather than by total bytes.
+ *
+ * @param {CensusTreeNode} tree
+ * The un-inverted tree.
+ *
+ * @returns {CensusTreeNode}
+ * The corresponding inverted tree.
+ */
+function invert(tree) {
+ const inverted = new CensusTreeNodeCacheValue();
+ inverted.node = new CensusTreeNode(null);
+
+ // Do a depth-first search of the un-inverted tree. As we reach each leaf,
+ // take the path from the old root to the leaf, reverse that path, and add it
+ // to the new, inverted tree's root.
+
+ const path = [];
+ (function addInvertedPaths(node) {
+ path.push(node);
+
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ addInvertedPaths(node.children[i]);
+ }
+ } else {
+ // We found a leaf node, add the reverse path to the inverted tree.
+ let currentCacheValue = inverted;
+ for (let i = path.length - 1; i >= 0; i--) {
+ currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
+ }
+ }
+
+ path.pop();
+ })(tree);
+
+ // Ensure that the root node always has the totals.
+ inverted.node.totalBytes = tree.totalBytes;
+ inverted.node.totalCount = tree.totalCount;
+
+ return inverted.node;
+}
+
+/**
+ * Given a CensusTreeNode tree and predicate function, create the tree
+ * containing only the nodes in any path `(node_0, node_1, ..., node_n-1)` in
+ * the given tree where `predicate(node_j)` is true for `0 <= j < n`, `node_0`
+ * is the given tree's root, and `node_n-1` is a leaf in the given tree. The
+ * given tree is left unmodified.
+ *
+ * @param {CensusTreeNode} tree
+ * @param {Function} predicate
+ *
+ * @returns {CensusTreeNode}
+ */
+function filter(tree, predicate) {
+ const filtered = new CensusTreeNodeCacheValue();
+ filtered.node = new CensusTreeNode(null);
+
+ // Do a DFS over the given tree. If the predicate returns true for any node,
+ // add that node and its whole subtree to the filtered tree.
+
+ const path = [];
+ let match = false;
+
+ function addMatchingNodes(node) {
+ path.push(node);
+
+ const oldMatch = match;
+ if (!match && predicate(node)) {
+ match = true;
+ }
+
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ addMatchingNodes(node.children[i]);
+ }
+ } else if (match) {
+ // We found a matching leaf node, add it to the filtered tree.
+ let currentCacheValue = filtered;
+ for (let i = 0, length = path.length; i < length; i++) {
+ currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
+ }
+ }
+
+ match = oldMatch;
+ path.pop();
+ }
+
+ if (tree.children) {
+ for (let i = 0, length = tree.children.length; i < length; i++) {
+ addMatchingNodes(tree.children[i]);
+ }
+ }
+
+ filtered.node.count = tree.count;
+ filtered.node.totalCount = tree.totalCount;
+ filtered.node.bytes = tree.bytes;
+ filtered.node.totalBytes = tree.totalBytes;
+
+ return filtered.node;
+}
+
+/**
+ * Given a filter string, return a predicate function that takes a node and
+ * returns true iff the node matches the filter.
+ *
+ * @param {String} filterString
+ * @returns {Function}
+ */
+function makeFilterPredicate(filterString) {
+ return function (node) {
+ if (!node.name) {
+ return false;
+ }
+
+ if (isSavedFrame(node.name)) {
+ return (
+ node.name.source.includes(filterString) ||
+ (node.name.functionDisplayName || "").includes(filterString) ||
+ (node.name.asyncCause || "").includes(filterString)
+ );
+ }
+
+ return String(node.name).includes(filterString);
+ };
+}
+
+/**
+ * Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
+ * used to generate the census and returns a structure used to render
+ * a tree to display the data.
+ *
+ * Returns a recursive "CensusTreeNode" object, looking like:
+ *
+ * CensusTreeNode = {
+ * // `children` if it exists, is sorted by `bytes`, if they are leaf nodes.
+ * children: ?[<CensusTreeNode...>],
+ * name: <?String>
+ * count: <?Number>
+ * bytes: <?Number>
+ * id: <?Number>
+ * parent: <?Number>
+ * }
+ *
+ * @param {Object} breakdown
+ * The breakdown used to generate the census report.
+ *
+ * @param {Object} report
+ * The census report generated with the specified breakdown.
+ *
+ * @param {Object} options
+ * Configuration options.
+ * - invert: Whether to invert the resulting tree or not. Defaults to
+ * false, ie uninverted.
+ *
+ * @returns {CensusTreeNode}
+ */
+exports.censusReportToCensusTreeNode = function (
+ breakdown,
+ report,
+ options = {
+ invert: false,
+ filter: null,
+ }
+) {
+ // Reset the counter so that turning the same census report into a
+ // CensusTreeNode tree repeatedly is idempotent.
+ censusTreeNodeIdCounter = 0;
+
+ const visitor = new CensusTreeNodeVisitor();
+ walk(breakdown, report, visitor);
+ let result = visitor.root();
+
+ if (options.invert) {
+ result = invert(result);
+ }
+
+ if (typeof options.filter === "string") {
+ result = filter(result, makeFilterPredicate(options.filter));
+ }
+
+ // If the report is a delta report that was generated by diffing two other
+ // reports, make sure to use the basis totals rather than the totals of the
+ // difference.
+ if (typeof report[basisTotalBytes] === "number") {
+ result.totalBytes = report[basisTotalBytes];
+ result.totalCount = report[basisTotalCount];
+ }
+
+ // Inverting and filtering could have messed up the sort order, so do a
+ // depth-first search of the tree and ensure that siblings are sorted.
+ const comparator = options.invert ? compareBySelf : compareByTotal;
+ (function ensureSorted(node) {
+ if (node.children) {
+ node.children.sort(comparator);
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ ensureSorted(node.children[i]);
+ }
+ }
+ })(result);
+
+ return result;
+};
diff --git a/devtools/shared/heapsnapshot/generate-core-dump-sources.sh b/devtools/shared/heapsnapshot/generate-core-dump-sources.sh
new file mode 100755
index 0000000000..97e492ff05
--- /dev/null
+++ b/devtools/shared/heapsnapshot/generate-core-dump-sources.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+# A script to generate devtools/server/CoreDump.pb.{h,cc} from
+# devtools/server/CoreDump.proto. This script assumes you have
+# downloaded and installed the protocol buffer compiler, and that it is either
+# on your $PATH or located at $PROTOC_PATH.
+#
+# These files were last compiled with libprotoc 2.4.1.
+
+set -e
+
+cd $(dirname $0)
+
+if [ -n $PROTOC_PATH ]; then
+ PROTOC_PATH=`which protoc`
+fi
+
+if [ ! -e $PROTOC_PATH ]; then
+ echo You must install the protocol compiler from
+ echo https://code.google.com/p/protobuf/downloads/list
+ exit 1
+fi
+
+echo Using $PROTOC_PATH as the protocol compiler
+
+$PROTOC_PATH --cpp_out="." CoreDump.proto
diff --git a/devtools/shared/heapsnapshot/moz.build b/devtools/shared/heapsnapshot/moz.build
new file mode 100644
index 0000000000..41bc0fb4aa
--- /dev/null
+++ b/devtools/shared/heapsnapshot/moz.build
@@ -0,0 +1,59 @@
+# -*- 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Memory")
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["tests/gtest"]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+EXPORTS.mozilla.devtools += [
+ "AutoMemMap.h",
+ "CoreDump.pb.h",
+ "DeserializedNode.h",
+ "DominatorTree.h",
+ "FileDescriptorOutputStream.h",
+ "HeapSnapshot.h",
+ "HeapSnapshotTempFileHelperChild.h",
+ "HeapSnapshotTempFileHelperParent.h",
+ "ZeroCopyNSIOutputStream.h",
+]
+
+IPDL_SOURCES += [
+ "PHeapSnapshotTempFileHelper.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+SOURCES += [
+ "AutoMemMap.cpp",
+ "CoreDump.pb.cc",
+ "DeserializedNode.cpp",
+ "DominatorTree.cpp",
+ "FileDescriptorOutputStream.cpp",
+ "HeapSnapshot.cpp",
+ "HeapSnapshotTempFileHelperParent.cpp",
+ "ZeroCopyNSIOutputStream.cpp",
+]
+
+# Disable RTTI in google protocol buffer
+DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True
+
+FINAL_LIBRARY = "xul"
+
+DevToolsModules(
+ "census-tree-node.js",
+ "CensusUtils.js",
+ "DominatorTreeNode.js",
+ "HeapAnalyses.worker.js",
+ "HeapAnalysesClient.js",
+ "HeapSnapshotFileUtils.js",
+ "shortest-paths.js",
+)
diff --git a/devtools/shared/heapsnapshot/shortest-paths.js b/devtools/shared/heapsnapshot/shortest-paths.js
new file mode 100644
index 0000000000..eca1ab9735
--- /dev/null
+++ b/devtools/shared/heapsnapshot/shortest-paths.js
@@ -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/. */
+"use strict";
+
+/**
+ * Compress a set of paths leading to `target` into a single graph, returned as
+ * a set of nodes and a set of edges.
+ *
+ * @param {NodeId} target
+ * The target node passed to `HeapSnapshot.computeShortestPaths`.
+ *
+ * @param {Array<Path>} paths
+ * An array of paths to `target`, as returned by
+ * `HeapSnapshot.computeShortestPaths`.
+ *
+ * @returns {Object}
+ * An object with two properties:
+ * - edges: An array of unique objects of the form:
+ * {
+ * from: <node ID>,
+ * to: <node ID>,
+ * name: <string or null>
+ * }
+ * - nodes: An array of unique node IDs. Every `from` and `to` id is
+ * guaranteed to be in this array exactly once.
+ */
+exports.deduplicatePaths = function (target, paths) {
+ // Use this structure to de-duplicate edges among many retaining paths from
+ // start to target.
+ //
+ // Map<FromNodeId, Map<ToNodeId, Set<EdgeName>>>
+ const deduped = new Map();
+
+ function insert(from, to, name) {
+ let toMap = deduped.get(from);
+ if (!toMap) {
+ toMap = new Map();
+ deduped.set(from, toMap);
+ }
+
+ let nameSet = toMap.get(to);
+ if (!nameSet) {
+ nameSet = new Set();
+ toMap.set(to, nameSet);
+ }
+
+ nameSet.add(name);
+ }
+
+ // eslint-disable-next-line no-labels
+ outer: for (const path of paths) {
+ const pathLength = path.length;
+
+ // Check for duplicate predecessors in the path, and skip paths that contain
+ // them.
+ const predecessorsSeen = new Set();
+ predecessorsSeen.add(target);
+ for (let i = 0; i < pathLength; i++) {
+ if (predecessorsSeen.has(path[i].predecessor)) {
+ // eslint-disable-next-line no-labels
+ continue outer;
+ }
+ predecessorsSeen.add(path[i].predecessor);
+ }
+
+ for (let i = 0; i < pathLength - 1; i++) {
+ insert(path[i].predecessor, path[i + 1].predecessor, path[i].edge);
+ }
+
+ insert(path[pathLength - 1].predecessor, target, path[pathLength - 1].edge);
+ }
+
+ const nodes = [target];
+ const edges = [];
+
+ for (const [from, toMap] of deduped) {
+ // If the second/third/etc shortest path contains the `target` anywhere
+ // other than the very last node, we could accidentally put the `target` in
+ // `nodes` more than once.
+ if (from !== target) {
+ nodes.push(from);
+ }
+
+ for (const [to, edgeNameSet] of toMap) {
+ for (const name of edgeNameSet) {
+ edges.push({ from, to, name });
+ }
+ }
+ }
+
+ return { nodes, edges };
+};
diff --git a/devtools/shared/heapsnapshot/tests/browser/browser.toml b/devtools/shared/heapsnapshot/tests/browser/browser.toml
new file mode 100644
index 0000000000..dc0042b957
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/browser/browser.toml
@@ -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_saveHeapSnapshot_e10s_01.js"]
diff --git a/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js
new file mode 100644
index 0000000000..1fc25341b8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1201597 - Test to verify that we can take a heap snapshot in an e10s child process.
+ */
+
+"use strict";
+
+add_task(async function () {
+ // Create a minimal browser
+ const browser = document.createXULElement("browser");
+ browser.setAttribute("type", "content");
+ document.body.appendChild(browser);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Save heap snapshot");
+ const result = await SpecialPowers.spawn(browser, [], () => {
+ try {
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ } catch (err) {
+ return err.toString();
+ }
+
+ return "";
+ });
+ is(result, "", "result of saveHeapSnapshot");
+
+ browser.remove();
+});
diff --git a/devtools/shared/heapsnapshot/tests/chrome/chrome.toml b/devtools/shared/heapsnapshot/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..afe21d17fa
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/chrome/chrome.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = "devtools devtools-memory"
+skip-if = ["os == 'android'"]
+
+["test_DominatorTree_01.html"]
+
+["test_SaveHeapSnapshot.html"]
diff --git a/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html
new file mode 100644
index 0000000000..61e60ae209
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Sanity test that we can compute dominator trees from a heap snapshot in a web window.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ChromeUtils.saveHeapSnapshot 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>
+"use strict";
+/* global window, ChromeUtils, DominatorTree */
+
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(DominatorTree.isInstance(dominatorTree));
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html
new file mode 100644
index 0000000000..53831bfaa2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1024774 - Sanity test that we can take a heap snapshot in a web window.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ChromeUtils.saveHeapSnapshot 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>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+ ok(ChromeUtils, "The ChromeUtils interface should be exposed in chrome windows.");
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should save a heap snapshot and shouldn't throw.");
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
new file mode 100644
index 0000000000..dc24d13e98
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that the `JS::ubi::Node`s we create from
+// `mozilla::devtools::DeserializedNode` instances look and behave as we would
+// like.
+
+#include "DevTools.h"
+#include "js/TypeDecls.h"
+#include "mozilla/devtools/DeserializedNode.h"
+
+using testing::Field;
+using testing::ReturnRef;
+
+// A mock DeserializedNode for testing.
+struct MockDeserializedNode : public DeserializedNode {
+ MockDeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
+ : DeserializedNode(id, typeName, size) {}
+
+ bool addEdge(DeserializedEdge&& edge) {
+ return edges.append(std::move(edge));
+ }
+
+ MOCK_METHOD1(getEdgeReferent, JS::ubi::Node(const DeserializedEdge&));
+};
+
+size_t fakeMallocSizeOf(const void*) {
+ EXPECT_TRUE(false);
+ MOZ_ASSERT_UNREACHABLE(
+ "fakeMallocSizeOf should never be called because "
+ "DeserializedNodes report the deserialized size.");
+ return 0;
+}
+
+DEF_TEST(DeserializedNodeUbiNodes, {
+ const char16_t* typeName = u"TestTypeName";
+ const char* className = "MyObjectClassName";
+ const char* filename = "my-cool-filename.js";
+
+ NodeId id = uint64_t(1) << 33;
+ uint64_t size = uint64_t(1) << 60;
+ MockDeserializedNode mocked(id, typeName, size);
+ mocked.coarseType = JS::ubi::CoarseType::Script;
+ mocked.jsObjectClassName = className;
+ mocked.scriptFilename = filename;
+
+ DeserializedNode& deserialized = mocked;
+ JS::ubi::Node ubi(&deserialized);
+
+ // Test the ubi::Node accessors.
+
+ EXPECT_EQ(size, ubi.size(fakeMallocSizeOf));
+ EXPECT_EQ(typeName, ubi.typeName());
+ EXPECT_EQ(JS::ubi::CoarseType::Script, ubi.coarseType());
+ EXPECT_EQ(id, ubi.identifier());
+ EXPECT_FALSE(ubi.isLive());
+ EXPECT_EQ(ubi.jsObjectClassName(), className);
+ EXPECT_EQ(ubi.scriptFilename(), filename);
+
+ // Test the ubi::Node's edges.
+
+ UniquePtr<DeserializedNode> referent1(
+ new MockDeserializedNode(1, nullptr, 10));
+ DeserializedEdge edge1(referent1->id);
+ mocked.addEdge(std::move(edge1));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent1.get())));
+
+ UniquePtr<DeserializedNode> referent2(
+ new MockDeserializedNode(2, nullptr, 20));
+ DeserializedEdge edge2(referent2->id);
+ mocked.addEdge(std::move(edge2));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent2.get())));
+
+ UniquePtr<DeserializedNode> referent3(
+ new MockDeserializedNode(3, nullptr, 30));
+ DeserializedEdge edge3(referent3->id);
+ mocked.addEdge(std::move(edge3));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent3.get())));
+
+ auto range = ubi.edges(cx);
+ ASSERT_TRUE(!!range);
+
+ for (; !range->empty(); range->popFront()) {
+ // Nothing to do here. This loop ensures that we get each edge referent
+ // that we expect above.
+ }
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp
new file mode 100644
index 0000000000..4ce2c8968f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that the `JS::ubi::StackFrame`s we create from
+// `mozilla::devtools::DeserializedStackFrame` instances look and behave as we
+// would like.
+
+#include "DevTools.h"
+#include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin
+#include "js/SavedFrameAPI.h"
+#include "js/TypeDecls.h"
+#include "mozilla/devtools/DeserializedNode.h"
+
+using testing::Field;
+using testing::ReturnRef;
+
+// A mock DeserializedStackFrame for testing.
+struct MockDeserializedStackFrame : public DeserializedStackFrame {
+ MockDeserializedStackFrame() = default;
+};
+
+DEF_TEST(DeserializedStackFrameUbiStackFrames, {
+ StackFrameId id = uint64_t(1) << 42;
+ uint32_t line = 1337;
+ JS::TaggedColumnNumberOneOrigin column(
+ JS::LimitedColumnNumberOneOrigin(9)); // 3 space tabs!?
+ const char16_t* source = u"my-javascript-file.js";
+ const char16_t* functionDisplayName = u"myFunctionName";
+
+ MockDeserializedStackFrame mocked;
+ mocked.id = id;
+ mocked.line = line;
+ mocked.column = column;
+ mocked.source = source;
+ mocked.functionDisplayName = functionDisplayName;
+
+ DeserializedStackFrame& deserialized = mocked;
+ JS::ubi::StackFrame ubiFrame(&deserialized);
+
+ // Test the JS::ubi::StackFrame accessors.
+
+ EXPECT_EQ(id, ubiFrame.identifier());
+ EXPECT_EQ(JS::ubi::StackFrame(), ubiFrame.parent());
+ EXPECT_EQ(line, ubiFrame.line());
+ EXPECT_EQ(column, ubiFrame.column());
+ EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(source), ubiFrame.source());
+ EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(functionDisplayName),
+ ubiFrame.functionDisplayName());
+ EXPECT_FALSE(ubiFrame.isSelfHosted(cx));
+ EXPECT_FALSE(ubiFrame.isSystem());
+
+ JS::Rooted<JSObject*> savedFrame(cx);
+ EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame));
+
+ JSPrincipals* principals = JS::GetRealmPrincipals(js::GetContextRealm(cx));
+
+ uint32_t frameLine;
+ ASSERT_EQ(JS::SavedFrameResult::Ok,
+ JS::GetSavedFrameLine(cx, principals, savedFrame, &frameLine));
+ EXPECT_EQ(line, frameLine);
+
+ JS::TaggedColumnNumberOneOrigin frameColumn;
+ ASSERT_EQ(JS::SavedFrameResult::Ok,
+ JS::GetSavedFrameColumn(cx, principals, savedFrame, &frameColumn));
+ EXPECT_EQ(column, frameColumn);
+
+ JS::Rooted<JSObject*> parent(cx);
+ ASSERT_EQ(JS::SavedFrameResult::Ok,
+ JS::GetSavedFrameParent(cx, principals, savedFrame, &parent));
+ EXPECT_EQ(nullptr, parent);
+
+ ASSERT_EQ(NS_strlen(source), 21U);
+ char16_t sourceBuf[21] = {};
+
+ // Test when the length is shorter than the string length.
+ auto written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 3);
+ EXPECT_EQ(written, 3U);
+ for (size_t i = 0; i < 3; i++) {
+ EXPECT_EQ(sourceBuf[i], source[i]);
+ }
+
+ written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 21);
+ EXPECT_EQ(written, 21U);
+ for (size_t i = 0; i < 21; i++) {
+ EXPECT_EQ(sourceBuf[i], source[i]);
+ }
+
+ ASSERT_EQ(NS_strlen(functionDisplayName), 14U);
+ char16_t nameBuf[14] = {};
+
+ written = ubiFrame.functionDisplayName(RangedPtr<char16_t>(nameBuf), 14);
+ EXPECT_EQ(written, 14U);
+ for (size_t i = 0; i < 14; i++) {
+ EXPECT_EQ(nameBuf[i], functionDisplayName[i]);
+ }
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp
new file mode 100644
index 0000000000..8e89d5ecd7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp
@@ -0,0 +1,7 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DevTools.h"
+const char16_t JS::ubi::Concrete<FakeNode>::concreteTypeName[] = u"FakeNode";
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.h b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
new file mode 100644
index 0000000000..a3c29e87fc
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_gtest_DevTools__
+#define mozilla_devtools_gtest_DevTools__
+
+#include <utility>
+
+#include "CoreDump.pb.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "js/Principals.h"
+#include "js/UbiNode.h"
+#include "js/UniquePtr.h"
+#include "jsapi.h"
+#include "jspubtd.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "nsCRTGlue.h"
+
+using namespace mozilla;
+using namespace mozilla::devtools;
+using namespace mozilla::dom;
+using namespace testing;
+
+// GTest fixture class that all of our tests derive from.
+struct DevTools : public ::testing::Test {
+ bool _initialized;
+ JSContext* cx;
+ JS::Compartment* compartment;
+ JS::Zone* zone;
+ JS::PersistentRooted<JSObject*> global;
+
+ DevTools() : _initialized(false), cx(nullptr) {}
+
+ virtual void SetUp() {
+ MOZ_ASSERT(!_initialized);
+
+ cx = getContext();
+ if (!cx) return;
+
+ global.init(cx, createGlobal());
+ if (!global) return;
+ JS::EnterRealm(cx, global);
+
+ compartment = js::GetContextCompartment(cx);
+ zone = js::GetContextZone(cx);
+
+ _initialized = true;
+ }
+
+ JSContext* getContext() { return CycleCollectedJSContext::Get()->Context(); }
+
+ static void reportError(JSContext* cx, const char* message,
+ JSErrorReport* report) {
+ fprintf(stderr, "%s:%u:%s\n",
+ report->filename ? report->filename.c_str() : "<no filename>",
+ (unsigned int)report->lineno, message);
+ }
+
+ static const JSClass* getGlobalClass() {
+ static const JSClass globalClass = {"global", JSCLASS_GLOBAL_FLAGS,
+ &JS::DefaultGlobalClassOps};
+ return &globalClass;
+ }
+
+ JSObject* createGlobal() {
+ /* Create the global object. */
+ JS::RealmOptions options;
+ // dummy
+ options.behaviors().setReduceTimerPrecisionCallerType(
+ JS::RTPCallerTypeToken{0});
+ return JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options);
+ }
+
+ virtual void TearDown() {
+ _initialized = false;
+
+ if (global) {
+ JS::LeaveRealm(cx, nullptr);
+ global = nullptr;
+ }
+ }
+};
+
+// Helper to define a test and ensure that the fixture is initialized properly.
+#define DEF_TEST(name, body) \
+ TEST_F(DevTools, name) { \
+ ASSERT_TRUE(_initialized); \
+ body \
+ }
+
+// Fake JS::ubi::Node implementation
+class MOZ_STACK_CLASS FakeNode {
+ public:
+ JS::ubi::EdgeVector edges;
+ JS::Compartment* compartment;
+ JS::Zone* zone;
+ size_t size;
+
+ explicit FakeNode() : compartment(nullptr), zone(nullptr), size(1) {}
+};
+
+namespace JS {
+namespace ubi {
+
+template <>
+class Concrete<FakeNode> : public Base {
+ const char16_t* typeName() const override { return concreteTypeName; }
+
+ js::UniquePtr<EdgeRange> edges(JSContext*, bool) const override {
+ return js::UniquePtr<EdgeRange>(js_new<PreComputedEdgeRange>(get().edges));
+ }
+
+ Size size(mozilla::MallocSizeOf) const override { return get().size; }
+
+ JS::Zone* zone() const override { return get().zone; }
+
+ JS::Compartment* compartment() const override { return get().compartment; }
+
+ protected:
+ explicit Concrete(FakeNode* ptr) : Base(ptr) {}
+ FakeNode& get() const { return *static_cast<FakeNode*>(ptr); }
+
+ public:
+ static const char16_t concreteTypeName[];
+ static void construct(void* storage, FakeNode* ptr) {
+ new (storage) Concrete(ptr);
+ }
+};
+
+} // namespace ubi
+} // namespace JS
+
+inline void AddEdge(FakeNode& node, FakeNode& referent,
+ const char16_t* edgeName = nullptr) {
+ char16_t* ownedEdgeName = nullptr;
+ if (edgeName) {
+ ownedEdgeName = NS_xstrdup(edgeName);
+ }
+
+ JS::ubi::Edge edge(ownedEdgeName, &referent);
+ ASSERT_TRUE(node.edges.append(std::move(edge)));
+}
+
+// Custom GMock Matchers
+
+// Use the testing namespace to avoid static analysis failures in the gmock
+// matcher classes that get generated from MATCHER_P macros.
+namespace testing {
+
+// Ensure that given node has the expected number of edges.
+MATCHER_P2(EdgesLength, cx, expectedLength, "") {
+ auto edges = arg.edges(cx);
+ if (!edges) return false;
+
+ int actualLength = 0;
+ for (; !edges->empty(); edges->popFront()) actualLength++;
+
+ return Matcher<int>(Eq(expectedLength))
+ .MatchAndExplain(actualLength, result_listener);
+}
+
+// Get the nth edge and match it with the given matcher.
+MATCHER_P3(Edge, cx, n, matcher, "") {
+ auto edges = arg.edges(cx);
+ if (!edges) return false;
+
+ int i = 0;
+ for (; !edges->empty(); edges->popFront()) {
+ if (i == n) {
+ return Matcher<const JS::ubi::Edge&>(matcher).MatchAndExplain(
+ edges->front(), result_listener);
+ }
+
+ i++;
+ }
+
+ return false;
+}
+
+// Ensures that two char16_t* strings are equal.
+MATCHER_P(UTF16StrEq, str, "") { return NS_strcmp(arg, str) == 0; }
+
+MATCHER_P(UniqueUTF16StrEq, str, "") { return NS_strcmp(arg.get(), str) == 0; }
+
+MATCHER(UniqueIsNull, "") { return arg.get() == nullptr; }
+
+// Matches an edge whose referent is the node with the given id.
+MATCHER_P(EdgeTo, id, "") {
+ return Matcher<const DeserializedEdge&>(
+ Field(&DeserializedEdge::referent, id))
+ .MatchAndExplain(arg, result_listener);
+}
+
+} // namespace testing
+
+// A mock `Writer` class to be used with testing `WriteHeapGraph`.
+class MockWriter : public CoreDumpWriter {
+ public:
+ virtual ~MockWriter() override = default;
+ MOCK_METHOD2(writeNode,
+ bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy));
+ MOCK_METHOD1(writeMetadata, bool(uint64_t));
+};
+
+inline void ExpectWriteNode(MockWriter& writer, FakeNode& node) {
+ EXPECT_CALL(writer, writeNode(Eq(JS::ubi::Node(&node)), _))
+ .Times(1)
+ .WillOnce(Return(true));
+}
+
+#endif // mozilla_devtools_gtest_DevTools__
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp
new file mode 100644
index 0000000000..d264c73738
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that heap snapshots cross compartment boundaries when expected.
+
+#include "DevTools.h"
+
+DEF_TEST(DoesCrossCompartmentBoundaries, {
+ // Create a new global to get a new compartment.
+ JS::RealmOptions options;
+ // dummy
+ options.behaviors().setReduceTimerPrecisionCallerType(
+ JS::RTPCallerTypeToken{0});
+ JS::Rooted<JSObject*> newGlobal(
+ cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options));
+ ASSERT_TRUE(newGlobal);
+ JS::Compartment* newCompartment = nullptr;
+ {
+ JSAutoRealm ar(cx, newGlobal);
+ ASSERT_TRUE(JS::InitRealmStandardClasses(cx));
+ newCompartment = js::GetContextCompartment(cx);
+ }
+ ASSERT_TRUE(newCompartment);
+ ASSERT_NE(newCompartment, compartment);
+
+ // Our set of target compartments is both the old and new compartments.
+ JS::CompartmentSet targetCompartments;
+ ASSERT_TRUE(targetCompartments.put(compartment));
+ ASSERT_TRUE(targetCompartments.put(newCompartment));
+
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+ FakeNode nodeD;
+
+ nodeA.compartment = compartment;
+ nodeB.compartment = nullptr;
+ nodeC.compartment = newCompartment;
+ nodeD.compartment = nullptr;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeA, nodeC);
+ AddEdge(nodeB, nodeD);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize nodeA, because it is in one of our target compartments.
+ ExpectWriteNode(writer, nodeA);
+
+ // Should serialize nodeB, because it doesn't belong to a compartment and is
+ // therefore assumed to be shared.
+ ExpectWriteNode(writer, nodeB);
+
+ // Should also serialize nodeC, which is in our target compartments, but a
+ // different compartment than A.
+ ExpectWriteNode(writer, nodeC);
+
+ // Should serialize nodeD because it's reachable via B and both nodes B and D
+ // don't belong to a specific compartment.
+ ExpectWriteNode(writer, nodeD);
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer,
+ /* wantNames = */ false, &targetCompartments,
+ noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp
new file mode 100644
index 0000000000..6b506aabff
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that heap snapshots walk the compartment boundaries correctly.
+
+#include "DevTools.h"
+
+DEF_TEST(DoesntCrossCompartmentBoundaries, {
+ // Create a new global to get a new compartment.
+ JS::RealmOptions options;
+ // dummy
+ options.behaviors().setReduceTimerPrecisionCallerType(
+ JS::RTPCallerTypeToken{0});
+ JS::Rooted<JSObject*> newGlobal(
+ cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options));
+ ASSERT_TRUE(newGlobal);
+ JS::Compartment* newCompartment = nullptr;
+ {
+ JSAutoRealm ar(cx, newGlobal);
+ ASSERT_TRUE(JS::InitRealmStandardClasses(cx));
+ newCompartment = js::GetContextCompartment(cx);
+ }
+ ASSERT_TRUE(newCompartment);
+ ASSERT_NE(newCompartment, compartment);
+
+ // Our set of target compartments is only the pre-existing compartment and
+ // does not include the new compartment.
+ JS::CompartmentSet targetCompartments;
+ ASSERT_TRUE(targetCompartments.put(compartment));
+
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+
+ nodeA.compartment = compartment;
+ nodeB.compartment = nullptr;
+ nodeC.compartment = newCompartment;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeB, nodeC);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize nodeA, because it is in our target compartments.
+ ExpectWriteNode(writer, nodeA);
+
+ // Should serialize nodeB, because it doesn't belong to a compartment and is
+ // therefore assumed to be shared.
+ ExpectWriteNode(writer, nodeB);
+
+ // But we shouldn't ever serialize nodeC.
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer,
+ /* wantNames = */ false, &targetCompartments,
+ noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
new file mode 100644
index 0000000000..ab47941e39
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that edge names get serialized correctly.
+
+#include "DevTools.h"
+
+using testing::Field;
+using testing::IsNull;
+using testing::Property;
+using testing::Return;
+
+DEF_TEST(SerializesEdgeNames, {
+ FakeNode node;
+ FakeNode referent;
+
+ const char16_t edgeName[] = u"edge name";
+ const char16_t emptyStr[] = u"";
+
+ AddEdge(node, referent, edgeName);
+ AddEdge(node, referent, emptyStr);
+ AddEdge(node, referent, nullptr);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should get the node with edges once.
+ EXPECT_CALL(
+ writer,
+ writeNode(
+ AllOf(EdgesLength(cx, 3),
+ Edge(cx, 0,
+ Field(&JS::ubi::Edge::name, UniqueUTF16StrEq(edgeName))),
+ Edge(cx, 1,
+ Field(&JS::ubi::Edge::name, UniqueUTF16StrEq(emptyStr))),
+ Edge(cx, 2, Field(&JS::ubi::Edge::name, UniqueIsNull()))),
+ _))
+ .Times(1)
+ .WillOnce(Return(true));
+
+ // Should get the referent node that doesn't have any edges once.
+ ExpectWriteNode(writer, referent);
+
+ JS::AutoCheckCannotGC noGC(cx);
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&node), writer,
+ /* wantNames = */ true,
+ /* zones = */ nullptr, noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp
new file mode 100644
index 0000000000..d71c86703c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that everything in the heap graph gets serialized once, and only once.
+
+#include "DevTools.h"
+
+DEF_TEST(SerializesEverythingInHeapGraphOnce, {
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+ FakeNode nodeD;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeB, nodeC);
+ AddEdge(nodeC, nodeD);
+ AddEdge(nodeD, nodeA);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize each node once.
+ ExpectWriteNode(writer, nodeA);
+ ExpectWriteNode(writer, nodeB);
+ ExpectWriteNode(writer, nodeC);
+ ExpectWriteNode(writer, nodeD);
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer,
+ /* wantNames = */ false,
+ /* zones = */ nullptr, noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
new file mode 100644
index 0000000000..4c29b28832
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that a ubi::Node's typeName gets properly serialized into a core dump.
+
+#include "DevTools.h"
+
+using testing::Property;
+using testing::Return;
+
+DEF_TEST(SerializesTypeNames, {
+ FakeNode node;
+
+ ::testing::NiceMock<MockWriter> writer;
+ EXPECT_CALL(
+ writer,
+ writeNode(Property(&JS::ubi::Node::typeName, UTF16StrEq(u"FakeNode")), _))
+ .Times(1)
+ .WillOnce(Return(true));
+
+ JS::AutoCheckCannotGC noGC(cx);
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&node), writer,
+ /* wantNames = */ true,
+ /* zones = */ nullptr, noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/moz.build b/devtools/shared/heapsnapshot/tests/gtest/moz.build
new file mode 100644
index 0000000000..880d7b334e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/moz.build
@@ -0,0 +1,32 @@
+# -*- 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/.
+
+Library("devtoolstests")
+
+LOCAL_INCLUDES += [
+ "../..",
+]
+
+DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True
+DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True
+
+UNIFIED_SOURCES = [
+ "DeserializedNodeUbiNodes.cpp",
+ "DeserializedStackFrameUbiStackFrames.cpp",
+ "DevTools.cpp",
+ "DoesCrossCompartmentBoundaries.cpp",
+ "DoesntCrossCompartmentBoundaries.cpp",
+ "SerializesEdgeNames.cpp",
+ "SerializesEverythingInHeapGraphOnce.cpp",
+ "SerializesTypeNames.cpp",
+]
+
+# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard
+# to work around, so we just ignore it.
+if CONFIG["CC_TYPE"] == "clang":
+ CXXFLAGS += ["-Wno-inconsistent-missing-override"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs
new file mode 100644
index 0000000000..0e86f8b055
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs
@@ -0,0 +1,176 @@
+// Functions for checking results returned by
+// Debugger.Memory.prototype.takeCensus and
+// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js.
+
+export const Census = {};
+function dumpn(msg) {
+ dump("DBG-TEST: Census.jsm: " + msg + "\n");
+}
+
+// Census.walkCensus(subject, name, walker)
+//
+// Use |walker| to check |subject|, a census object of the sort returned by
+// Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the
+// leaves. Use |name| as the name for |subject| in diagnostic messages. Return
+// the number of leaves of |subject| we visited.
+//
+// A walker is an object with three methods:
+//
+// - enter(prop): Return the walker we should use to check the property of the
+// subject census named |prop|. This is for recursing into the subobjects of
+// the subject.
+//
+// - done(): Called after we have called 'enter' on every property of the
+// subject.
+//
+// - check(value): Check |value|, a leaf in the subject.
+//
+// Walker methods are expected to simply throw if a node we visit doesn't look
+// right.
+Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0);
+function walk(subject, name, walker, count) {
+ if (typeof subject === "object") {
+ dumpn(name);
+ for (const prop in subject) {
+ count = walk(
+ subject[prop],
+ name + "[" + uneval(prop) + "]",
+ walker.enter(prop),
+ count
+ );
+ }
+ walker.done();
+ } else {
+ dumpn(name + " = " + uneval(subject));
+ walker.check(subject);
+ count++;
+ }
+
+ return count;
+}
+
+// A walker that doesn't check anything.
+Census.walkAnything = {
+ enter: () => Census.walkAnything,
+ done: () => undefined,
+ check: () => undefined,
+};
+
+// A walker that requires all leaves to be zeros.
+Census.assertAllZeros = {
+ enter: () => Census.assertAllZeros,
+ done: () => undefined,
+ check: elt => {
+ if (elt !== 0) {
+ throw new Error("Census mismatch: expected zero, found " + elt);
+ }
+ },
+};
+
+function expectedObject() {
+ throw new Error(
+ "Census mismatch: subject has leaf where basis has nested object"
+ );
+}
+
+function expectedLeaf() {
+ throw new Error(
+ "Census mismatch: subject has nested object where basis has leaf"
+ );
+}
+
+// Return a function that, given a 'basis' census, returns a census walker that
+// compares the subject census against the basis. The returned walker calls the
+// given |compare|, |missing|, and |extra| functions as follows:
+//
+// - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the
+// corresponding leaf of the basis.
+//
+// - missing(prop, value): Called when the subject is missing a property named
+// |prop| which is present in the basis with value |value|.
+//
+// - extra(prop): Called when the subject has a property named |prop|, but the
+// basis has no such property. This should return a walker that can check
+// the subject's value.
+function makeBasisChecker({ compare, missing, extra }) {
+ return function makeWalker(basis) {
+ if (typeof basis === "object") {
+ const unvisited = new Set(Object.getOwnPropertyNames(basis));
+ return {
+ enter: prop => {
+ unvisited.delete(prop);
+ if (prop in basis) {
+ return makeWalker(basis[prop]);
+ }
+
+ return extra(prop);
+ },
+
+ done: () => unvisited.forEach(prop => missing(prop, basis[prop])),
+ check: expectedObject,
+ };
+ }
+
+ return {
+ enter: expectedLeaf,
+ done: expectedLeaf,
+ check: elt => compare(elt, basis),
+ };
+ };
+}
+
+function missingProp(prop) {
+ throw new Error(
+ "Census mismatch: subject lacks property present in basis: " + prop
+ );
+}
+
+function extraProp(prop) {
+ throw new Error(
+ "Census mismatch: subject has property not present in basis: " + prop
+ );
+}
+
+// Return a walker that checks that the subject census has counts all equal to
+// |basis|.
+Census.assertAllEqual = makeBasisChecker({
+ compare: (a, b) => {
+ if (a !== b) {
+ throw new Error("Census mismatch: expected " + a + " got " + b);
+ }
+ },
+ missing: missingProp,
+ extra: extraProp,
+});
+
+function ok(val) {
+ if (!val) {
+ throw new Error("Census mismatch: expected truthy, got " + val);
+ }
+}
+
+// Return a walker that checks that the subject census has at least as many
+// items of each category as |basis|.
+Census.assertAllNotLessThan = makeBasisChecker({
+ compare: (subject, basis) => ok(subject >= basis),
+ missing: missingProp,
+ extra: () => Census.walkAnything,
+});
+
+// Return a walker that checks that the subject census has at most as many
+// items of each category as |basis|.
+Census.assertAllNotMoreThan = makeBasisChecker({
+ compare: (subject, basis) => ok(subject <= basis),
+ missing: missingProp,
+ extra: () => Census.walkAnything,
+});
+
+// Return a walker that checks that the subject census has within |fudge|
+// items of each category of the count in |basis|.
+Census.assertAllWithin = function (fudge, basis) {
+ return makeBasisChecker({
+ compare: (subject, base) => ok(Math.abs(subject - base) <= fudge),
+ missing: missingProp,
+ extra: () => Census.walkAnything,
+ })(basis);
+};
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs
new file mode 100644
index 0000000000..76312db86a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs
@@ -0,0 +1,218 @@
+// A little pattern-matching library.
+//
+// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools
+// server xpcshell tests.
+
+export const Match = (function () {
+ function Pattern(template) {
+ // act like a constructor even as a function
+ if (!(this instanceof Pattern)) {
+ return new Pattern(template);
+ }
+
+ this.template = template;
+ }
+
+ Pattern.prototype = {
+ match(act) {
+ return match(act, this.template);
+ },
+
+ matches(act) {
+ try {
+ return this.match(act);
+ } catch (e) {
+ if (e instanceof MatchError) {
+ return false;
+ }
+ }
+ return false;
+ },
+
+ assert(act, message) {
+ try {
+ return this.match(act);
+ } catch (e) {
+ if (e instanceof MatchError) {
+ throw new Error((message || "failed match") + ": " + e.message);
+ }
+ }
+ return false;
+ },
+
+ toString: () => "[object Pattern]",
+ };
+
+ Pattern.ANY = new Pattern();
+ Pattern.ANY.template = Pattern.ANY;
+
+ Pattern.NUMBER = new Pattern();
+ Pattern.NUMBER.match = function (act) {
+ if (typeof act !== "number") {
+ throw new MatchError("Expected number, got: " + quote(act));
+ }
+ };
+
+ Pattern.NATURAL = new Pattern();
+ Pattern.NATURAL.match = function (act) {
+ if (typeof act !== "number" || act !== Math.floor(act) || act < 0) {
+ throw new MatchError("Expected natural number, got: " + quote(act));
+ }
+ };
+
+ const quote = uneval;
+
+ function MatchError(msg) {
+ this.message = msg;
+ }
+
+ MatchError.prototype = {
+ toString() {
+ return "match error: " + this.message;
+ },
+ };
+
+ function isAtom(x) {
+ return (
+ typeof x === "number" ||
+ typeof x === "string" ||
+ typeof x === "boolean" ||
+ x === null ||
+ (typeof x === "object" && x instanceof RegExp)
+ );
+ }
+
+ function isObject(x) {
+ return x !== null && typeof x === "object";
+ }
+
+ function isFunction(x) {
+ return typeof x === "function";
+ }
+
+ function isArrayLike(x) {
+ return isObject(x) && "length" in x;
+ }
+
+ function matchAtom(act, exp) {
+ if (typeof exp === "number" && isNaN(exp)) {
+ if (typeof act !== "number" || !isNaN(act)) {
+ throw new MatchError("expected NaN, got: " + quote(act));
+ }
+ return true;
+ }
+
+ if (exp === null) {
+ if (act !== null) {
+ throw new MatchError("expected null, got: " + quote(act));
+ }
+ return true;
+ }
+
+ if (exp instanceof RegExp) {
+ if (!(act instanceof RegExp) || exp.source !== act.source) {
+ throw new MatchError("expected " + quote(exp) + ", got: " + quote(act));
+ }
+ return true;
+ }
+
+ switch (typeof exp) {
+ case "string":
+ if (act !== exp) {
+ throw new MatchError(
+ "expected " + quote(exp) + ", got " + quote(act)
+ );
+ }
+ return true;
+ case "boolean":
+ case "number":
+ if (exp !== act) {
+ throw new MatchError("expected " + exp + ", got " + quote(act));
+ }
+ return true;
+ }
+
+ throw new Error("bad pattern: " + exp.toSource());
+ }
+
+ function matchObject(act, exp) {
+ if (!isObject(act)) {
+ throw new MatchError("expected object, got " + quote(act));
+ }
+
+ for (const key in exp) {
+ if (!(key in act)) {
+ throw new MatchError(
+ "expected property " + quote(key) + " not found in " + quote(act)
+ );
+ }
+ match(act[key], exp[key]);
+ }
+
+ return true;
+ }
+
+ function matchFunction(act, exp) {
+ if (!isFunction(act)) {
+ throw new MatchError("expected function, got " + quote(act));
+ }
+
+ if (act !== exp) {
+ throw new MatchError(
+ "expected function: " + exp + "\nbut got different function: " + act
+ );
+ }
+ }
+
+ function matchArray(act, exp) {
+ if (!isObject(act) || !("length" in act)) {
+ throw new MatchError("expected array-like object, got " + quote(act));
+ }
+
+ const length = exp.length;
+ if (act.length !== exp.length) {
+ throw new MatchError(
+ "expected array-like object of length " + length + ", got " + quote(act)
+ );
+ }
+
+ for (let i = 0; i < length; i++) {
+ if (i in exp) {
+ if (!(i in act)) {
+ throw new MatchError(
+ "expected array property " + i + " not found in " + quote(act)
+ );
+ }
+ match(act[i], exp[i]);
+ }
+ }
+
+ return true;
+ }
+
+ function match(act, exp) {
+ if (exp === Pattern.ANY) {
+ return true;
+ }
+
+ if (exp instanceof Pattern) {
+ return exp.match(act);
+ }
+
+ if (isAtom(exp)) {
+ return matchAtom(act, exp);
+ }
+
+ if (isArrayLike(exp)) {
+ return matchArray(act, exp);
+ }
+
+ if (isFunction(exp)) {
+ return matchFunction(act, exp);
+ }
+
+ return matchObject(act, exp);
+ }
+
+ return { Pattern, MatchError };
+})();
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js
new file mode 100644
index 0000000000..c636226101
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+console.log("Initializing worker.");
+
+self.onmessage = e => {
+ console.log("Starting test.");
+ try {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(DominatorTree.isInstance(dominatorTree));
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (excp) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+ } catch (ex) {
+ ok(
+ false,
+ "Unexpected error inside worker:\n" + ex.toString() + "\n" + ex.stack
+ );
+ } finally {
+ done();
+ }
+};
+
+// Proxy assertions to the main thread.
+function ok(val, msg) {
+ console.log("ok(" + !!val + ', "' + msg + '")');
+ self.postMessage({
+ type: "assertion",
+ passed: !!val,
+ msg,
+ stack: Error().stack,
+ });
+}
+
+// Tell the main thread we are done with the tests.
+function done() {
+ console.log("done()");
+ self.postMessage({
+ type: "done",
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js
new file mode 100644
index 0000000000..d51decb104
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js
@@ -0,0 +1,554 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+/* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient,
+ assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus,
+ saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff,
+ assertLabelAndShallowSize, makeTestDominatorTreeNode,
+ assertDominatorTreeNodeInsertion, assertDeduplicatedPaths,
+ assertCountToBucketBreakdown, pathEntry */
+
+var CC = Components.Constructor;
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs");
+const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs");
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js");
+const {
+ censusReportToCensusTreeNode,
+} = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
+const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
+const {
+ deduplicatePaths,
+} = require("resource://devtools/shared/heapsnapshot/shortest-paths.js");
+const { LabelAndShallowSizeVisitor } = DominatorTreeNode;
+
+// Always log packets when running tests. runxpcshelltests.py will throw
+// the output away anyway, unless you give it the --verbose flag.
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.prefs.setBoolPref("devtools.debugger.log", true);
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ });
+}
+
+const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+);
+
+function dumpn(msg) {
+ dump("HEAPSNAPSHOT-TEST: " + msg + "\n");
+}
+
+function addTestingFunctionsToGlobal(global) {
+ global.eval(
+ `
+ const testingFunctions = Components.utils.getJSTestingFunctions();
+ for (let k in testingFunctions) {
+ this[k] = testingFunctions[k];
+ }
+ `
+ );
+ if (!global.print) {
+ global.print = info;
+ }
+ if (!global.newGlobal) {
+ global.newGlobal = newGlobal;
+ }
+ if (!global.Debugger) {
+ addDebuggerToGlobal(global);
+ }
+}
+
+addTestingFunctionsToGlobal(this);
+
+/**
+ * Create a new global, with all the JS shell testing functions. Similar to the
+ * newGlobal function exposed to JS shells, and useful for porting JS shell
+ * tests to xpcshell tests.
+ */
+function newGlobal() {
+ const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+ addTestingFunctionsToGlobal(global);
+ return global;
+}
+
+function assertThrows(f, val, msg) {
+ let fullmsg;
+ try {
+ f();
+ } catch (exc) {
+ if (exc === val && (val !== 0 || 1 / exc === 1 / val)) {
+ return;
+ } else if (exc instanceof Error && exc.message === val) {
+ return;
+ }
+ fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
+ }
+ if (fullmsg === undefined) {
+ fullmsg =
+ "Assertion failed: expected exception " + val + ", no exception thrown";
+ }
+ if (msg !== undefined) {
+ fullmsg += " - " + msg;
+ }
+ throw new Error(fullmsg);
+}
+
+/**
+ * Returns the full path of the file with the specified name in a
+ * platform-independent and URL-like form.
+ */
+function getFilePath(
+ name,
+ allowMissing = false,
+ usePlatformPathSeparator = false
+) {
+ const file = do_get_file(name, allowMissing);
+ let path = Services.io.newFileURI(file).spec;
+ let filePrePath = "file://";
+ if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) {
+ filePrePath += "/";
+ }
+
+ path = path.slice(filePrePath.length);
+
+ if (usePlatformPathSeparator && path.match(/^\w:/)) {
+ path = path.replace(/\//g, "\\");
+ }
+
+ return path;
+}
+
+function saveNewHeapSnapshot(opts = { runtime: true }) {
+ const filePath = ChromeUtils.saveHeapSnapshot(opts);
+ ok(filePath, "Should get a file path to save the core dump to.");
+ ok(true, "Saved a heap snapshot to " + filePath);
+ return filePath;
+}
+
+function readHeapSnapshot(filePath) {
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should have read a heap snapshot back from " + filePath);
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "snapshot should be an instance of HeapSnapshot"
+ );
+ return snapshot;
+}
+
+/**
+ * Save a heap snapshot to the file with the given name in the current
+ * directory, read it back as a HeapSnapshot instance, and then take a census of
+ * the heap snapshot's serialized heap graph with the provided census options.
+ *
+ * @param {Object|undefined} censusOptions
+ * Options that should be passed through to the takeCensus method. See
+ * js/src/doc/Debugger/Debugger.Memory.md for details.
+ *
+ * @param {Debugger|null} dbg
+ * If a Debugger object is given, only serialize the subgraph covered by
+ * the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @param {String} fileName
+ * The file name to save the heap snapshot's core dump file to, within
+ * the current directory.
+ *
+ * @returns Census
+ */
+function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) {
+ const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+ const filePath = saveNewHeapSnapshot(snapshotOptions);
+ const snapshot = readHeapSnapshot(filePath);
+
+ equal(
+ typeof snapshot.takeCensus,
+ "function",
+ "snapshot should have a takeCensus method"
+ );
+
+ return snapshot.takeCensus(censusOptions);
+}
+
+/**
+ * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and
+ * then compute its dominator tree.
+ *
+ * @param {Debugger|null} dbg
+ * If a Debugger object is given, only serialize the subgraph covered by
+ * the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @returns {DominatorTree}
+ */
+function saveHeapSnapshotAndComputeDominatorTree(dbg = null) {
+ const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+ const filePath = saveNewHeapSnapshot(snapshotOptions);
+ const snapshot = readHeapSnapshot(filePath);
+
+ equal(
+ typeof snapshot.computeDominatorTree,
+ "function",
+ "snapshot should have a `computeDominatorTree` method"
+ );
+
+ const dominatorTree = snapshot.computeDominatorTree();
+
+ ok(dominatorTree, "Should be able to compute a dominator tree");
+ ok(
+ DominatorTree.isInstance(dominatorTree),
+ "Should be an instance of DominatorTree"
+ );
+
+ return dominatorTree;
+}
+
+function isSavedFrame(obj) {
+ return Object.prototype.toString.call(obj) === "[object SavedFrame]";
+}
+
+function savedFrameReplacer(key, val) {
+ if (isSavedFrame(val)) {
+ return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`;
+ }
+ return val;
+}
+
+/**
+ * Assert that creating a CensusTreeNode from the given `report` with the
+ * specified `breakdown` creates the given `expected` CensusTreeNode.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {Object} expected
+ * The expected CensusTreeNode result.
+ *
+ * @param {Object} options
+ * The options to pass through to `censusReportToCensusTreeNode`.
+ */
+function compareCensusViewData(breakdown, report, expected, options) {
+ dumpn("Generating CensusTreeNode from report:");
+ dumpn("breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("report: " + JSON.stringify(report, null, 4));
+ dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4));
+
+ const actual = censusReportToCensusTreeNode(breakdown, report, options);
+ dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+// Deep structural equivalence that can handle Map objects in addition to plain
+// objects.
+function assertStructurallyEquivalent(actual, expected, path = "root") {
+ if (actual === expected) {
+ equal(actual, expected, "actual and expected are the same");
+ return;
+ }
+
+ equal(typeof actual, typeof expected, `${path}: typeof should be the same`);
+
+ if (actual && typeof actual === "object") {
+ const actualProtoString = Object.prototype.toString.call(actual);
+ const expectedProtoString = Object.prototype.toString.call(expected);
+ equal(
+ actualProtoString,
+ expectedProtoString,
+ `${path}: Object.prototype.toString.call() should be the same`
+ );
+
+ if (actualProtoString === "[object Map]") {
+ const expectedKeys = new Set([...expected.keys()]);
+
+ for (const key of actual.keys()) {
+ ok(
+ expectedKeys.has(key),
+ `${path}: every key in actual is expected: ${String(key).slice(
+ 0,
+ 10
+ )}`
+ );
+ expectedKeys.delete(key);
+
+ assertStructurallyEquivalent(
+ actual.get(key),
+ expected.get(key),
+ path + ".get(" + String(key).slice(0, 20) + ")"
+ );
+ }
+
+ equal(
+ expectedKeys.size,
+ 0,
+ `${path}: every key in expected should also exist in actual,\
+ did not see ${[...expectedKeys]}`
+ );
+ } else if (actualProtoString === "[object Set]") {
+ const expectedItems = new Set([...expected]);
+
+ for (const item of actual) {
+ ok(
+ expectedItems.has(item),
+ `${path}: every set item in actual should exist in expected: ${item}`
+ );
+ expectedItems.delete(item);
+ }
+
+ equal(
+ expectedItems.size,
+ 0,
+ `${path}: every set item in expected should also exist in actual,\
+ did not see ${[...expectedItems]}`
+ );
+ } else {
+ const expectedKeys = new Set(Object.keys(expected));
+
+ for (const key of Object.keys(actual)) {
+ ok(
+ expectedKeys.has(key),
+ `${path}: every key in actual should exist in expected: ${key}`
+ );
+ expectedKeys.delete(key);
+
+ assertStructurallyEquivalent(
+ actual[key],
+ expected[key],
+ path + "." + key
+ );
+ }
+
+ equal(
+ expectedKeys.size,
+ 0,
+ `${path}: every key in expected should also exist in actual,\
+ did not see ${[...expectedKeys]}`
+ );
+ }
+ } else {
+ equal(actual, expected, `${path}: primitives should be equal`);
+ }
+}
+
+/**
+ * Assert that creating a diff of the `first` and `second` census reports
+ * creates the `expected` delta-report.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} first
+ * The first census report.
+ *
+ * @param {Object} second
+ * The second census report.
+ *
+ * @param {Object} expected
+ * The expected delta-report.
+ */
+function assertDiff(breakdown, first, second, expected) {
+ dumpn("Diffing census reports:");
+ dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("First census report: " + JSON.stringify(first, null, 4));
+ dumpn("Second census report: " + JSON.stringify(second, null, 4));
+ dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4));
+
+ const actual = CensusUtils.diff(breakdown, first, second);
+ dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+/**
+ * Assert that creating a label and getting a shallow size from the given node
+ * description with the specified breakdown is as expected.
+ *
+ * @param {Object} breakdown
+ * @param {Object} givenDescription
+ * @param {Number} expectedShallowSize
+ * @param {Object} expectedLabel
+ */
+function assertLabelAndShallowSize(
+ breakdown,
+ givenDescription,
+ expectedShallowSize,
+ expectedLabel
+) {
+ dumpn("Computing label and shallow size from node description:");
+ dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("Given description: " + JSON.stringify(givenDescription, null, 4));
+
+ const visitor = new LabelAndShallowSizeVisitor();
+ CensusUtils.walk(breakdown, givenDescription, visitor);
+
+ dumpn("Expected shallow size: " + expectedShallowSize);
+ dumpn("Actual shallow size: " + visitor.shallowSize());
+ equal(
+ visitor.shallowSize(),
+ expectedShallowSize,
+ "Shallow size should be correct"
+ );
+
+ dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4));
+ dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4));
+ assertStructurallyEquivalent(visitor.label(), expectedLabel);
+}
+
+// Counter for mock DominatorTreeNode ids.
+let TEST_NODE_ID_COUNTER = 0;
+
+/**
+ * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
+ * property by providing it on `opts`. Optionally pass child nodes as well.
+ *
+ * @param {Object} opts
+ * @param {Array<DominatorTreeNode>?} children
+ *
+ * @returns {DominatorTreeNode}
+ */
+function makeTestDominatorTreeNode(opts, children) {
+ const nodeId = TEST_NODE_ID_COUNTER++;
+
+ const node = Object.assign(
+ {
+ nodeId,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: (children || []).reduce(
+ (size, c) => size + c.retainedSize,
+ 1
+ ),
+ parentId: undefined,
+ children,
+ moreChildrenAvailable: true,
+ },
+ opts
+ );
+
+ if (children && children.length) {
+ children.map(c => (c.parentId = node.nodeId));
+ }
+
+ return node;
+}
+
+/**
+ * Insert `newChildren` into the given dominator `tree` as specified by the
+ * `path` from the root to the node the `newChildren` should be inserted
+ * beneath. Assert that the resulting tree matches `expected`.
+ */
+function assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+) {
+ dumpn("Inserting new children into a dominator tree:");
+ dumpn("Dominator tree: " + JSON.stringify(tree, null, 2));
+ dumpn("Path: " + JSON.stringify(path, null, 2));
+ dumpn("New children: " + JSON.stringify(newChildren, null, 2));
+ dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2));
+
+ const actual = DominatorTreeNode.insert(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable
+ );
+ dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+function assertDeduplicatedPaths({
+ target,
+ paths,
+ expectedNodes,
+ expectedEdges,
+}) {
+ dumpn("Deduplicating paths:");
+ dumpn("target = " + target);
+ dumpn("paths = " + JSON.stringify(paths, null, 2));
+ dumpn("expectedNodes = " + expectedNodes);
+ dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2));
+
+ const { nodes, edges } = deduplicatePaths(target, paths);
+
+ dumpn("Actual nodes = " + nodes);
+ dumpn("Actual edges = " + JSON.stringify(edges, null, 2));
+
+ equal(
+ nodes.length,
+ expectedNodes.length,
+ "actual number of nodes is equal to the expected number of nodes"
+ );
+
+ equal(
+ edges.length,
+ expectedEdges.length,
+ "actual number of edges is equal to the expected number of edges"
+ );
+
+ const expectedNodeSet = new Set(expectedNodes);
+ const nodeSet = new Set(nodes);
+ Assert.strictEqual(
+ nodeSet.size,
+ nodes.length,
+ "each returned node should be unique"
+ );
+
+ for (const node of nodes) {
+ ok(expectedNodeSet.has(node), `the ${node} node was expected`);
+ }
+
+ for (const expectedEdge of expectedEdges) {
+ let count = 0;
+ for (const edge of edges) {
+ if (
+ edge.from === expectedEdge.from &&
+ edge.to === expectedEdge.to &&
+ edge.name === expectedEdge.name
+ ) {
+ count++;
+ }
+ }
+ equal(
+ count,
+ 1,
+ "should have exactly one matching edge for the expected edge = " +
+ JSON.stringify(expectedEdge)
+ );
+ }
+}
+
+function assertCountToBucketBreakdown(breakdown, expected) {
+ dumpn("count => bucket breakdown");
+ dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2));
+ dumpn("Expected results = ", JSON.stringify(expected, null, 2));
+
+ const actual = CensusUtils.countToBucketBreakdown(breakdown);
+ dumpn("Actual results = ", JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+/**
+ * Create a mock path entry for the given predecessor and edge.
+ */
+function pathEntry(predecessor, edge) {
+ return { predecessor, edge };
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js
new file mode 100644
index 0000000000..a79f442193
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+console.log("Initializing worker.");
+
+self.onmessage = ex => {
+ console.log("Starting test.");
+ try {
+ ok(ChromeUtils, "Should have access to ChromeUtils in a worker.");
+ ok(HeapSnapshot, "Should have access to HeapSnapshot in a worker.");
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "Should be an instanceof HeapSnapshot"
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack
+ );
+ } finally {
+ done();
+ }
+};
+
+// Proxy assertions to the main thread.
+function ok(val, msg) {
+ console.log("ok(" + !!val + ', "' + msg + '")');
+ self.postMessage({
+ type: "assertion",
+ passed: !!val,
+ msg,
+ stack: Error().stack,
+ });
+}
+
+// Tell the main thread we are done with the tests.
+function done() {
+ console.log("done()");
+ self.postMessage({
+ type: "done",
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js
new file mode 100644
index 0000000000..7d4560e6dc
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ Function: { count: 1, bytes: 32 },
+ other: { count: 0, bytes: 0 },
+ },
+ strings: {},
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["objects", "Function"];
+
+const shallowSize = 32;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js
new file mode 100644
index 0000000000..cd424afdd0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ other: { count: 1, bytes: 10 },
+ },
+ strings: {},
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["objects", "other"];
+
+const shallowSize = 10;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js
new file mode 100644
index 0000000000..098e3efc4f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ other: { count: 0, bytes: 0 },
+ },
+ strings: {
+ JSString: { count: 1, bytes: 42 },
+ },
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["strings", "JSString"];
+
+const shallowSize = 42;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js
new file mode 100644
index 0000000000..a087c39a2a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const stack = saveStack();
+
+const description = {
+ objects: {
+ Array: new Map([[stack, { count: 1, bytes: 512 }]]),
+ other: { count: 0, bytes: 0 },
+ },
+ strings: {},
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["objects", "Array", stack];
+
+const shallowSize = 512;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js
new file mode 100644
index 0000000000..07894c67b1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the DominatorTreeNode.attachShortestPaths function can correctly
+// attach the deduplicated shortest retaining paths for each node it is given.
+
+const startNodeId = 9999;
+const maxNumPaths = 2;
+
+// Mock data mapping node id to shortest paths to that node id.
+const shortestPaths = new Map([
+ [
+ 1000,
+ [
+ [pathEntry(1100, "a"), pathEntry(1200, "b")],
+ [pathEntry(1100, "c"), pathEntry(1300, "d")],
+ ],
+ ],
+ [2000, [[pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")]]],
+ [
+ 3000,
+ [
+ [pathEntry(3100, "h")],
+ [pathEntry(3100, "i")],
+ [pathEntry(3100, "j")],
+ [pathEntry(3200, "k")],
+ [pathEntry(3300, "l")],
+ [pathEntry(3400, "m")],
+ ],
+ ],
+]);
+
+const actual = [
+ makeTestDominatorTreeNode({ nodeId: 1000 }),
+ makeTestDominatorTreeNode({ nodeId: 2000 }),
+ makeTestDominatorTreeNode({ nodeId: 3000 }),
+];
+
+const expected = [
+ makeTestDominatorTreeNode({
+ nodeId: 1000,
+ shortestPaths: {
+ nodes: [
+ { id: 1000, label: ["SomeType-1000"] },
+ { id: 1100, label: ["SomeType-1100"] },
+ { id: 1200, label: ["SomeType-1200"] },
+ { id: 1300, label: ["SomeType-1300"] },
+ ],
+ edges: [
+ { from: 1100, to: 1200, name: "a" },
+ { from: 1100, to: 1300, name: "c" },
+ { from: 1200, to: 1000, name: "b" },
+ { from: 1300, to: 1000, name: "d" },
+ ],
+ },
+ }),
+
+ makeTestDominatorTreeNode({
+ nodeId: 2000,
+ shortestPaths: {
+ nodes: [
+ { id: 2000, label: ["SomeType-2000"] },
+ { id: 2100, label: ["SomeType-2100"] },
+ { id: 2200, label: ["SomeType-2200"] },
+ { id: 2300, label: ["SomeType-2300"] },
+ ],
+ edges: [
+ { from: 2100, to: 2200, name: "e" },
+ { from: 2200, to: 2300, name: "f" },
+ { from: 2300, to: 2000, name: "g" },
+ ],
+ },
+ }),
+
+ makeTestDominatorTreeNode({
+ nodeId: 3000,
+ shortestPaths: {
+ nodes: [
+ { id: 3000, label: ["SomeType-3000"] },
+ { id: 3100, label: ["SomeType-3100"] },
+ { id: 3200, label: ["SomeType-3200"] },
+ { id: 3300, label: ["SomeType-3300"] },
+ { id: 3400, label: ["SomeType-3400"] },
+ ],
+ edges: [
+ { from: 3100, to: 3000, name: "h" },
+ { from: 3100, to: 3000, name: "i" },
+ { from: 3100, to: 3000, name: "j" },
+ { from: 3200, to: 3000, name: "k" },
+ { from: 3300, to: 3000, name: "l" },
+ { from: 3400, to: 3000, name: "m" },
+ ],
+ },
+ }),
+];
+
+const breakdown = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+const mockSnapshot = {
+ computeShortestPaths: (start, nodeIds, max) => {
+ equal(start, startNodeId);
+ equal(max, maxNumPaths);
+
+ return new Map(
+ nodeIds.map(nodeId => {
+ const paths = shortestPaths.get(nodeId);
+ ok(paths, "Expected computeShortestPaths call for node id = " + nodeId);
+ return [nodeId, paths];
+ })
+ );
+ },
+
+ describeNode: (bd, nodeId) => {
+ equal(bd, breakdown);
+ return {
+ ["SomeType-" + nodeId]: {
+ count: 1,
+ bytes: 10,
+ },
+ };
+ },
+};
+
+function run_test() {
+ DominatorTreeNode.attachShortestPaths(
+ mockSnapshot,
+ breakdown,
+ startNodeId,
+ actual,
+ maxNumPaths
+ );
+
+ dumpn("Expected = " + JSON.stringify(expected, null, 2));
+ dumpn("Actual = " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(expected, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js
new file mode 100644
index 0000000000..be1f210c3e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can find the node with the given id along the specified path.
+
+const node3000 = makeTestDominatorTreeNode({ nodeId: 3000 });
+
+const node2000 = makeTestDominatorTreeNode({ nodeId: 2000 }, [
+ makeTestDominatorTreeNode({}),
+ node3000,
+ makeTestDominatorTreeNode({}),
+]);
+
+const node1000 = makeTestDominatorTreeNode({ nodeId: 1000 }, [
+ makeTestDominatorTreeNode({}),
+ node2000,
+ makeTestDominatorTreeNode({}),
+]);
+
+const tree = node1000;
+
+const path = [1000, 2000, 3000];
+
+const tests = [
+ { id: 1000, expected: node1000 },
+ { id: 2000, expected: node2000 },
+ { id: 3000, expected: node3000 },
+];
+
+function run_test() {
+ for (const { id, expected } of tests) {
+ const actual = DominatorTreeNode.getNodeByIdAlongPath(id, tree, path);
+ equal(actual, expected, `We should have got the node with id = ${id}`);
+ }
+
+ equal(
+ null,
+ DominatorTreeNode.getNodeByIdAlongPath(999999999999, tree, path),
+ "null is returned for nodes that are not even in the tree"
+ );
+
+ const lastNodeId = tree.children[tree.children.length - 1].nodeId;
+ equal(
+ null,
+ DominatorTreeNode.getNodeByIdAlongPath(lastNodeId, tree, path),
+ "null is returned for nodes that are not along the path"
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js
new file mode 100644
index 0000000000..7567c473e0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can insert new children into an existing DominatorTreeNode tree.
+
+const tree = makeTestDominatorTreeNode({ nodeId: 1000 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({ nodeId: 2000 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({ nodeId: 3000 }),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [1000, 2000, 3000];
+
+const newChildren = [
+ makeTestDominatorTreeNode({ parentId: 3000 }),
+ makeTestDominatorTreeNode({ parentId: 3000 }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = {
+ nodeId: 1000,
+ parentId: undefined,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 7,
+ children: [
+ {
+ nodeId: 0,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 1000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 2000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 4,
+ parentId: 1000,
+ children: [
+ {
+ nodeId: 1,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ children: [
+ {
+ nodeId: 7,
+ parentId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 8,
+ parentId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false,
+ },
+ {
+ nodeId: 3,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+ },
+ {
+ nodeId: 5,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 1000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+};
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js
new file mode 100644
index 0000000000..b0b80c3c95
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test attempting to insert new children into an existing DominatorTreeNode
+// tree with a bad path.
+
+const tree = makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [111111, 222222, 333333];
+
+const newChildren = [
+ makeTestDominatorTreeNode({ parentId: 333333 }),
+ makeTestDominatorTreeNode({ parentId: 333333 }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = tree;
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js
new file mode 100644
index 0000000000..552ed72735
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test inserting new children into an existing DominatorTreeNode at the root.
+
+const tree = makeTestDominatorTreeNode({ nodeId: 666 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [666];
+
+const newChildren = [
+ makeTestDominatorTreeNode({
+ nodeId: 777,
+ parentId: 666,
+ }),
+ makeTestDominatorTreeNode({
+ nodeId: 888,
+ parentId: 666,
+ }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = {
+ nodeId: 666,
+ label: undefined,
+ parentId: undefined,
+ shallowSize: 1,
+ retainedSize: 7,
+ children: [
+ {
+ nodeId: 0,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 4,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 4,
+ parentId: 666,
+ children: [
+ {
+ nodeId: 1,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 2,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 3,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+ },
+ {
+ nodeId: 5,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 777,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 888,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false,
+};
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js
new file mode 100644
index 0000000000..6da0587f57
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we correctly set `moreChildrenAvailable` when doing a partial
+// traversal of a dominator tree to create the initial incrementally loaded
+// `DominatorTreeNode` tree.
+
+// `tree` maps parent to children:
+//
+// 100
+// |- 200
+// | |- 500
+// | |- 600
+// | `- 700
+// |- 300
+// | |- 800
+// | |- 900
+// `- 400
+// |- 1000
+// |- 1100
+// `- 1200
+const tree = new Map([
+ [100, [200, 300, 400]],
+ [200, [500, 600, 700]],
+ [300, [800, 900]],
+ [400, [1000, 1100, 1200]],
+]);
+
+const mockDominatorTree = {
+ root: 100,
+ getRetainedSize: _ => 10,
+ getImmediatelyDominated: id => (tree.get(id) || []).slice(),
+};
+
+const mockSnapshot = {
+ describeNode: _ => ({
+ objects: { count: 0, bytes: 0 },
+ strings: { count: 0, bytes: 0 },
+ scripts: { count: 0, bytes: 0 },
+ other: { SomeType: { count: 1, bytes: 10 } },
+ domNode: { count: 0, bytes: 0 },
+ }),
+};
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+const expected = {
+ nodeId: 100,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 200,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 100,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 500,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 200,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ {
+ nodeId: 600,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 200,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+ },
+ {
+ nodeId: 300,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 100,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 800,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 300,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ {
+ nodeId: 900,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 300,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false,
+ },
+ ],
+ moreChildrenAvailable: true,
+ parentId: undefined,
+};
+
+function run_test() {
+ // Traverse the whole depth of the test tree, but one short of the number of
+ // siblings. This will exercise the moreChildrenAvailable handling for
+ // siblings.
+ const actual = DominatorTreeNode.partialTraversal(
+ mockDominatorTree,
+ mockSnapshot,
+ breakdown,
+ // maxDepth
+ 4,
+ // siblings
+ 2
+ );
+
+ dumpn("Expected = " + JSON.stringify(expected, null, 2));
+ dumpn("Actual = " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(expected, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js
new file mode 100644
index 0000000000..f98094fd32
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Sanity test that we can compute dominator trees.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(DominatorTree.isInstance(dominatorTree));
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js
new file mode 100644
index 0000000000..525e031ccf
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can compute dominator trees from a snapshot in a worker.
+
+add_task(async function () {
+ const worker = new ChromeWorker("resource://test/dominator-tree-worker.js");
+ worker.postMessage({});
+
+ let assertionCount = 0;
+ worker.onmessage = e => {
+ if (e.data.type !== "assertion") {
+ return;
+ }
+
+ ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
+ assertionCount++;
+ };
+
+ await waitForDone(worker);
+
+ Assert.greater(assertionCount, 0);
+ worker.terminate();
+});
+
+function waitForDone(w) {
+ return new Promise((resolve, reject) => {
+ w.onerror = e => {
+ reject();
+ ok(false, "Error in worker: " + e);
+ };
+
+ w.addEventListener("message", function listener(e) {
+ if (e.data.type === "done") {
+ w.removeEventListener("message", listener);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js
new file mode 100644
index 0000000000..d304845328
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can get the root of dominator trees.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(typeof dominatorTree.root, "number", "root should be a number");
+ equal(
+ Math.floor(dominatorTree.root),
+ dominatorTree.root,
+ "root should be an integer"
+ );
+ Assert.greaterOrEqual(dominatorTree.root, 0, "root should be positive");
+ Assert.lessOrEqual(
+ dominatorTree.root,
+ Math.pow(2, 48),
+ "root should be less than or equal to 2^48"
+ );
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js
new file mode 100644
index 0000000000..2bfe46977f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can get the retained sizes of dominator trees.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(
+ typeof dominatorTree.getRetainedSize,
+ "function",
+ "getRetainedSize should be a function"
+ );
+
+ const size = dominatorTree.getRetainedSize(dominatorTree.root);
+ ok(size, "should get a size for the root");
+ equal(typeof size, "number", "retained sizes should be a number");
+ equal(Math.floor(size), size, "size should be an integer");
+ Assert.greater(size, 0, "size should be positive");
+ Assert.lessOrEqual(
+ size,
+ Math.pow(2, 64),
+ "size should be less than or equal to 2^64"
+ );
+
+ const bad = dominatorTree.getRetainedSize(1);
+ equal(bad, null, "null is returned for unknown node ids");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js
new file mode 100644
index 0000000000..23abf1bd74
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can get the set of immediately dominated nodes for any given
+// node and that this forms a tree.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(
+ typeof dominatorTree.getImmediatelyDominated,
+ "function",
+ "getImmediatelyDominated should be a function"
+ );
+
+ // Do a traversal of the dominator tree.
+ //
+ // Note that we don't assert directly, only if we get an unexpected
+ // value. There are just way too many nodes in the heap graph to assert for
+ // every one. This test would constantly time out and assertion messages would
+ // overflow the log size.
+
+ const root = dominatorTree.root;
+ equal(
+ dominatorTree.getImmediateDominator(root),
+ null,
+ "The root should not have a parent"
+ );
+
+ const seen = new Set();
+ const stack = [root];
+ while (stack.length) {
+ const top = stack.pop();
+
+ if (seen.has(top)) {
+ ok(
+ false,
+ "This is a tree, not a graph: we shouldn't have " +
+ "multiple edges to the same node"
+ );
+ }
+ seen.add(top);
+ if (seen.size % 1000 === 0) {
+ dumpn("Progress update: seen size = " + seen.size);
+ }
+
+ const newNodes = dominatorTree.getImmediatelyDominated(top);
+ if (Object.prototype.toString.call(newNodes) !== "[object Array]") {
+ ok(
+ false,
+ "getImmediatelyDominated should return an array for known node ids"
+ );
+ }
+
+ const topSize = dominatorTree.getRetainedSize(top);
+
+ let lastSize = Infinity;
+ for (let i = 0; i < newNodes.length; i++) {
+ if (typeof newNodes[i] !== "number") {
+ ok(false, "Every dominated id should be a number");
+ }
+
+ if (dominatorTree.getImmediateDominator(newNodes[i]) !== top) {
+ ok(false, "child's parent should be the expected parent");
+ }
+
+ const thisSize = dominatorTree.getRetainedSize(newNodes[i]);
+
+ if (thisSize >= topSize) {
+ ok(
+ false,
+ "the size of children in the dominator tree should" +
+ " always be less than that of their parent"
+ );
+ }
+
+ if (thisSize > lastSize) {
+ ok(
+ false,
+ "children should be sorted by greatest to least retained size, " +
+ "lastSize = " +
+ lastSize +
+ ", thisSize = " +
+ thisSize
+ );
+ }
+
+ lastSize = thisSize;
+ stack.push(newNodes[i]);
+ }
+ }
+
+ ok(true, "Successfully walked the tree");
+ dumpn("Walked " + seen.size + " nodes");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js
new file mode 100644
index 0000000000..fdd4191c5f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the retained size of a node is the sum of its children retained
+// sizes plus its shallow size.
+
+// Note that we don't assert directly, only if we get an unexpected
+// value. There are just way too many nodes in the heap graph to assert for
+// every one. This test would constantly time out and assertion messages would
+// overflow the log size.
+function fastAssert(cond, msg) {
+ if (!cond) {
+ ok(false, msg);
+ }
+}
+
+const COUNT = { by: "count", count: false, bytes: true };
+
+function run_test() {
+ const path = saveNewHeapSnapshot();
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ const dominatorTree = snapshot.computeDominatorTree();
+
+ // Do a traversal of the dominator tree and assert the relationship between
+ // retained size, shallow size, and children's retained sizes.
+
+ const root = dominatorTree.root;
+ const stack = [root];
+ while (stack.length) {
+ const top = stack.pop();
+
+ const children = dominatorTree.getImmediatelyDominated(top);
+
+ const topRetainedSize = dominatorTree.getRetainedSize(top);
+ const topShallowSize = snapshot.describeNode(COUNT, top).bytes;
+ fastAssert(
+ topShallowSize <= topRetainedSize,
+ "The shallow size should be less than or equal to the " + "retained size"
+ );
+
+ let sumOfChildrensRetainedSizes = 0;
+ for (let i = 0; i < children.length; i++) {
+ sumOfChildrensRetainedSizes += dominatorTree.getRetainedSize(children[i]);
+ stack.push(children[i]);
+ }
+
+ fastAssert(
+ sumOfChildrensRetainedSizes <= topRetainedSize,
+ "The sum of the children's retained sizes should be less than " +
+ "or equal to the retained size"
+ );
+ fastAssert(
+ sumOfChildrensRetainedSizes + topShallowSize === topRetainedSize,
+ "The sum of the children's retained sizes plus the shallow " +
+ "size should be equal to the retained size"
+ );
+ }
+
+ ok(true, "Successfully walked the tree");
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js
new file mode 100644
index 0000000000..ba622f55df
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ equal(
+ typeof dominatorTreeId,
+ "number",
+ "should get a dominator tree id, and it should be a number"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js
new file mode 100644
index 0000000000..c3ee76be13
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad
+// file paths.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ await client.computeDominatorTree("/etc/passwd");
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "should throw when given a bad path");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js
new file mode 100644
index 0000000000..b85e4b19fc
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ ok(true, "Should have computed the dominator tree");
+
+ await client.deleteHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have deleted the snapshot");
+
+ let threw = false;
+ try {
+ await client.getDominatorTree({
+ dominatorTreeId,
+ breakdown,
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getDominatorTree on deleted tree should throw an error");
+
+ threw = false;
+ try {
+ await client.computeDominatorTree(snapshotFilePath);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "computeDominatorTree on deleted snapshot should throw an error");
+
+ threw = false;
+ try {
+ await client.takeCensus(snapshotFilePath);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "takeCensus on deleted tree should throw an error");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js
new file mode 100644
index 0000000000..6960081afd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ await client.deleteHeapSnapshot("path-does-not-exist");
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "deleteHeapSnapshot on non-existant path should throw an error");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js
new file mode 100644
index 0000000000..c84622f633
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test other dominatorTrees can still be retrieved after deleting a snapshot
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+async function createSnapshotAndDominatorTree(client) {
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ return { dominatorTreeId, snapshotFilePath };
+}
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const savedSnapshots = [
+ await createSnapshotAndDominatorTree(client),
+ await createSnapshotAndDominatorTree(client),
+ await createSnapshotAndDominatorTree(client),
+ ];
+ ok(true, "Create 3 snapshots and dominator trees");
+
+ await client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath);
+ ok(true, "Snapshot deleted");
+
+ let tree = await client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[0].dominatorTreeId,
+ breakdown,
+ });
+ ok(tree, "Should get a valid tree for first snapshot");
+
+ let threw = false;
+ try {
+ await client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[1].dominatorTreeId,
+ breakdown,
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getDominatorTree on a deleted snapshot should throw an error");
+
+ tree = await client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[2].dominatorTreeId,
+ breakdown,
+ });
+ ok(tree, "Should get a valid tree for third snapshot");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js
new file mode 100644
index 0000000000..942547835b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can get census individuals.
+
+const COUNT = { by: "count", count: true, bytes: true };
+
+const CENSUS_BREAKDOWN = {
+ by: "coarseType",
+ objects: COUNT,
+ strings: COUNT,
+ scripts: COUNT,
+ other: COUNT,
+ domNode: COUNT,
+};
+
+const LABEL_BREAKDOWN = {
+ by: "internalType",
+ then: COUNT,
+};
+
+const MAX_INDIVIDUALS = 10;
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ ok(true, "Should have computed dominator tree");
+
+ const { report } = await client.takeCensus(
+ snapshotFilePath,
+ { breakdown: CENSUS_BREAKDOWN },
+ { asTreeNode: true }
+ );
+ ok(report, "Should get a report");
+
+ let nodesWithLeafIndicesFound = 0;
+
+ await (async function assertCanGetIndividuals(censusNode) {
+ if (censusNode.reportLeafIndex !== undefined) {
+ nodesWithLeafIndicesFound++;
+
+ const response = await client.getCensusIndividuals({
+ dominatorTreeId,
+ indices: DevToolsUtils.isSet(censusNode.reportLeafIndex)
+ ? censusNode.reportLeafIndex
+ : new Set([censusNode.reportLeafIndex]),
+ censusBreakdown: CENSUS_BREAKDOWN,
+ labelBreakdown: LABEL_BREAKDOWN,
+ maxRetainingPaths: 1,
+ maxIndividuals: MAX_INDIVIDUALS,
+ });
+
+ dumpn(`response = ${JSON.stringify(response, null, 4)}`);
+
+ equal(
+ response.nodes.length,
+ Math.min(MAX_INDIVIDUALS, censusNode.count),
+ "response.nodes.length === Math.min(MAX_INDIVIDUALS, censusNode.count)"
+ );
+
+ let lastRetainedSize = Infinity;
+ for (const individual of response.nodes) {
+ equal(
+ typeof individual.nodeId,
+ "number",
+ "individual.nodeId should be a number"
+ );
+ Assert.lessOrEqual(
+ individual.retainedSize,
+ lastRetainedSize,
+ "individual.retainedSize <= lastRetainedSize"
+ );
+ lastRetainedSize = individual.retainedSize;
+ ok(
+ individual.shallowSize,
+ "individual.shallowSize should exist and be non-zero"
+ );
+ ok(individual.shortestPaths, "individual.shortestPaths should exist");
+ ok(
+ individual.shortestPaths.nodes,
+ "individual.shortestPaths.nodes should exist"
+ );
+ ok(
+ individual.shortestPaths.edges,
+ "individual.shortestPaths.edges should exist"
+ );
+ ok(individual.label, "individual.label should exist");
+ }
+ }
+
+ if (censusNode.children) {
+ for (const child of censusNode.children) {
+ await assertCanGetIndividuals(child);
+ }
+ }
+ })(report);
+
+ equal(
+ nodesWithLeafIndicesFound,
+ 4,
+ "Should have found a leaf for each coarse type"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js
new file mode 100644
index 0000000000..b52280d970
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's
+// creation time.
+
+function waitForThirtyMilliseconds() {
+ const start = Date.now();
+ while (Date.now() - start < 30) {
+ // do nothing
+ }
+}
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+ const start = Date.now() * 1000;
+
+ // Because Date.now() is less precise than the snapshot's time stamp, give it
+ // a little bit of head room. Additionally, WinXP's timers have a granularity
+ // of only +/-15 ms.
+ waitForThirtyMilliseconds();
+ const snapshotFilePath = saveNewHeapSnapshot();
+ waitForThirtyMilliseconds();
+ const end = Date.now() * 1000;
+
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ let threw = false;
+ try {
+ await client.getCreationTime("/not/a/real/path", {
+ breakdown: BREAKDOWN,
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getCreationTime should throw when snapshot does not exist");
+
+ const time = await client.getCreationTime(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ dumpn("Start = " + start);
+ dumpn("End = " + end);
+ dumpn("Time = " + time);
+
+ Assert.greaterOrEqual(time, start, "creation time occurred after start");
+ Assert.lessOrEqual(time, end, "creation time occurred before end");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js
new file mode 100644
index 0000000000..9035624ca2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ equal(
+ typeof dominatorTreeId,
+ "number",
+ "should get a dominator tree id, and it should be a number"
+ );
+
+ const partialTree = await client.getDominatorTree({
+ dominatorTreeId,
+ breakdown,
+ });
+ ok(partialTree, "Should get a partial tree");
+ equal(typeof partialTree, "object", "partialTree should be an object");
+
+ function checkTree(node) {
+ equal(typeof node.nodeId, "number", "each node should have an id");
+
+ if (node === partialTree) {
+ equal(node.parentId, undefined, "the root has no parent");
+ } else {
+ equal(
+ typeof node.parentId,
+ "number",
+ "each node should have a parent id"
+ );
+ }
+
+ equal(
+ typeof node.retainedSize,
+ "number",
+ "each node should have a retained size"
+ );
+
+ ok(
+ node.children === undefined || Array.isArray(node.children),
+ "each node either has a list of children, " +
+ "or undefined meaning no children loaded"
+ );
+ equal(
+ typeof node.moreChildrenAvailable,
+ "boolean",
+ "each node should indicate if there are more children available or not"
+ );
+
+ equal(typeof node.shortestPaths, "object", "Should have shortest paths");
+ equal(
+ typeof node.shortestPaths.nodes,
+ "object",
+ "Should have shortest paths' nodes"
+ );
+ equal(
+ typeof node.shortestPaths.edges,
+ "object",
+ "Should have shortest paths' edges"
+ );
+
+ if (node.children) {
+ node.children.forEach(checkTree);
+ }
+ }
+
+ checkTree(partialTree);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js
new file mode 100644
index 0000000000..83a7cbbd3d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad
+// dominator tree ids.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ await client.getDominatorTree({ dominatorTreeId: 42, breakdown });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "should throw when given a bad id");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js
new file mode 100644
index 0000000000..47860870cb
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+
+ const partialTree = await client.getDominatorTree({
+ dominatorTreeId,
+ breakdown,
+ });
+ ok(
+ !!partialTree.children.length,
+ "root should immediately dominate some nodes"
+ );
+
+ // First, test getting a subset of children available.
+ const response = await client.getImmediatelyDominated({
+ dominatorTreeId,
+ breakdown,
+ nodeId: partialTree.nodeId,
+ startIndex: 0,
+ maxCount: partialTree.children.length - 1,
+ });
+
+ ok(Array.isArray(response.nodes));
+ ok(response.nodes.every(node => node.parentId === partialTree.nodeId));
+ ok(response.moreChildrenAvailable);
+ equal(response.path.length, 1);
+ equal(response.path[0], partialTree.nodeId);
+
+ for (const node of response.nodes) {
+ equal(typeof node.shortestPaths, "object", "Should have shortest paths");
+ equal(
+ typeof node.shortestPaths.nodes,
+ "object",
+ "Should have shortest paths' nodes"
+ );
+ equal(
+ typeof node.shortestPaths.edges,
+ "object",
+ "Should have shortest paths' edges"
+ );
+ }
+
+ // Next, test getting a subset of children available.
+ const secondResponse = await client.getImmediatelyDominated({
+ dominatorTreeId,
+ breakdown,
+ nodeId: partialTree.nodeId,
+ startIndex: 0,
+ maxCount: Infinity,
+ });
+
+ ok(Array.isArray(secondResponse.nodes));
+ ok(secondResponse.nodes.every(node => node.parentId === partialTree.nodeId));
+ ok(!secondResponse.moreChildrenAvailable);
+ equal(secondResponse.path.length, 1);
+ equal(secondResponse.path[0], partialTree.nodeId);
+
+ for (const node of secondResponse.nodes) {
+ equal(typeof node.shortestPaths, "object", "Should have shortest paths");
+ equal(
+ typeof node.shortestPaths.nodes,
+ "object",
+ "Should have shortest paths' nodes"
+ );
+ equal(
+ typeof node.shortestPaths.edges,
+ "object",
+ "Should have shortest paths' edges"
+ );
+ }
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js
new file mode 100644
index 0000000000..dea8269a92
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can read heap snapshots.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js
new file mode 100644
index 0000000000..9703229438
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses.
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: false },
+ other: { by: "count", count: true, bytes: false },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const markers = [allocationMarker()];
+
+ const firstSnapshotFilePath = saveNewHeapSnapshot();
+
+ // Allocate and hold an additional AllocationMarker object so we can see it in
+ // the next heap snapshot.
+ markers.push(allocationMarker());
+
+ const secondSnapshotFilePath = saveNewHeapSnapshot();
+
+ await client.readHeapSnapshot(firstSnapshotFilePath);
+ await client.readHeapSnapshot(secondSnapshotFilePath);
+ ok(true, "Should have read both heap snapshot files");
+
+ const { delta } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN }
+ );
+
+ equal(
+ delta.AllocationMarker.count,
+ 1,
+ "There exists one new AllocationMarker in the second heap snapshot"
+ );
+
+ const { delta: deltaTreeNode } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN },
+ { asTreeNode: true }
+ );
+
+ // Have to manually set these because symbol properties aren't structured
+ // cloned.
+ delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
+ delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
+
+ compareCensusViewData(
+ BREAKDOWN,
+ delta,
+ deltaTreeNode,
+ "Returning delta-census as a tree node represents same data as the report"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js
new file mode 100644
index 0000000000..d5d988f78f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as
+// inverted trees.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+add_task(async function () {
+ const firstSnapshotFilePath = saveNewHeapSnapshot();
+ const secondSnapshotFilePath = saveNewHeapSnapshot();
+
+ const client = new HeapAnalysesClient();
+ await client.readHeapSnapshot(firstSnapshotFilePath);
+ await client.readHeapSnapshot(secondSnapshotFilePath);
+
+ ok(true, "Should have read both heap snapshot files");
+
+ const { delta } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN }
+ );
+
+ const { delta: deltaTreeNode } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN },
+ { asInvertedTreeNode: true }
+ );
+
+ // Have to manually set these because symbol properties aren't structured
+ // cloned.
+ delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
+ delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
+
+ compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, { invert: true });
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js
new file mode 100644
index 0000000000..55da2bf4b8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath);
+ ok(report, "Should get a report");
+ equal(typeof report, "object", "report should be an object");
+
+ ok(report.objects);
+ ok(report.scripts);
+ ok(report.strings);
+ ok(report.other);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js
new file mode 100644
index 0000000000..5b1dcefe03
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown
+// options.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "count", count: true, bytes: true },
+ });
+
+ ok(report, "Should get a report");
+ equal(typeof report, "object", "report should be an object");
+
+ ok(report.count);
+ ok(report.bytes);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js
new file mode 100644
index 0000000000..0dfda73f1f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things
+// go wrong.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ // Snapshot file path to a file that doesn't exist.
+ let failed = false;
+ try {
+ await client.readHeapSnapshot(
+ getFilePath("foo-bar-baz" + Math.random(), true)
+ );
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not read heap snapshots that do not exist");
+
+ // Snapshot file path to a file that is not a heap snapshot.
+ failed = false;
+ try {
+ await client.readHeapSnapshot(
+ getFilePath("test_HeapAnalyses_takeCensus_03.js")
+ );
+ } catch (e) {
+ failed = true;
+ }
+ ok(
+ failed,
+ "should not be able to read a file " +
+ "that is not a heap snapshot as a heap snapshot"
+ );
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ // Bad census breakdown options.
+ failed = false;
+ try {
+ await client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "some classification that we do not have" },
+ });
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not be able to breakdown by an unknown classification");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js
new file mode 100644
index 0000000000..26f78ad4f7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from
+// by-allocation-stack reports from the worker.
+
+add_task(async function test() {
+ const client = new HeapAnalysesClient();
+
+ // Track some allocation stacks.
+
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+ g.eval(` // 1
+ this.log = []; // 2
+ function f() { this.log.push(allocationMarker()); } // 3
+ function g() { this.log.push(allocationMarker()); } // 4
+ function h() { this.log.push(allocationMarker()); } // 5
+ `);
+
+ // Create one allocationMarker with tracking turned off,
+ // so it will have no associated stack.
+ g.f();
+
+ dbg.memory.allocationSamplingProbability = 1;
+
+ for (const [func, n] of [
+ [g.f, 20],
+ [g.g, 10],
+ [g.h, 5],
+ ]) {
+ for (let i = 0; i < n; i++) {
+ dbg.memory.trackingAllocationSites = true;
+ // All allocations of allocationMarker occur with this line as the oldest
+ // stack frame.
+ func();
+ dbg.memory.trackingAllocationSites = false;
+ }
+ }
+
+ // Take a heap snapshot.
+
+ const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ // Run a census broken down by class name -> allocation stack so we can grab
+ // only the AllocationMarker objects we have complete control over.
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: {
+ by: "count",
+ bytes: true,
+ count: true,
+ },
+ noStack: {
+ by: "count",
+ bytes: true,
+ count: true,
+ },
+ },
+ },
+ });
+
+ // Test the generated report.
+
+ ok(report, "Should get a report");
+
+ const map = report.AllocationMarker;
+ ok(map, "Should get AllocationMarkers in the report.");
+ // From a module with a different global, and therefore a different Map
+ // constructor, so we can't use instanceof.
+ equal(Object.getPrototypeOf(map).constructor.name, "Map");
+
+ equal(
+ map.size,
+ 4,
+ "Should have 4 allocation stacks (including the lack of a stack)"
+ );
+
+ // Gather the stacks we are expecting to appear as keys, and
+ // check that there are no unexpected keys.
+ const stacks = {};
+
+ map.forEach((v, k) => {
+ if (k === "noStack") {
+ // No need to save this key.
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "test"
+ ) {
+ stacks.f = k;
+ } else if (
+ k.functionDisplayName === "g" &&
+ k.parent.functionDisplayName === "test"
+ ) {
+ stacks.g = k;
+ } else if (
+ k.functionDisplayName === "h" &&
+ k.parent.functionDisplayName === "test"
+ ) {
+ stacks.h = k;
+ } else {
+ dumpn("Unexpected allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+ ok(false);
+ }
+ });
+
+ ok(map.get("noStack"));
+ equal(map.get("noStack").count, 1);
+
+ ok(stacks.f);
+ ok(map.get(stacks.f));
+ equal(map.get(stacks.f).count, 20);
+
+ ok(stacks.g);
+ ok(map.get(stacks.g));
+ equal(map.get(stacks.g).count, 10);
+
+ ok(stacks.h);
+ ok(map.get(stacks.h));
+ equal(map.get(stacks.h).count, 5);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js
new file mode 100644
index 0000000000..951a3b3133
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses and return
+// a CensusTreeNode.
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ const { report: treeNode } = await client.takeCensus(
+ snapshotFilePath,
+ {
+ breakdown: BREAKDOWN,
+ },
+ {
+ asTreeNode: true,
+ }
+ );
+
+ ok(!!treeNode.children.length, "treeNode has children");
+ ok(
+ treeNode.children.every(type => {
+ return "name" in type && "bytes" in type && "count" in type;
+ }),
+ "all of tree node's children have name, bytes, count"
+ );
+
+ compareCensusViewData(
+ BREAKDOWN,
+ report,
+ treeNode,
+ "Returning census as a tree node represents same data as the report"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js
new file mode 100644
index 0000000000..d9fdaa3708
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses by
+// "allocationStack" and return a CensusTreeNode.
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ // 5 allocation markers with no stack.
+ g.eval(`
+ this.markers = [];
+ for (var i = 0; i < 5; i++) {
+ markers.push(allocationMarker());
+ }
+ `);
+
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+
+ // 5 allocation markers at 5 stacks.
+ g.eval(`
+ (function shouldHaveCountOfOne() {
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ }());
+ `);
+
+ // 5 allocation markers at 1 stack.
+ g.eval(`
+ (function shouldHaveCountOfFive() {
+ for (var i = 0; i < 5; i++) {
+ markers.push(allocationMarker());
+ }
+ }());
+ `);
+
+ const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
+
+ const client = new HeapAnalysesClient();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ const { report: treeNode } = await client.takeCensus(
+ snapshotFilePath,
+ {
+ breakdown: BREAKDOWN,
+ },
+ {
+ asTreeNode: true,
+ }
+ );
+
+ const markers = treeNode.children.find(c => c.name === "AllocationMarker");
+ ok(markers);
+
+ const noStack = markers.children.find(c => c.name === "noStack");
+ equal(noStack.count, 5);
+
+ let numShouldHaveFiveFound = 0;
+ let numShouldHaveOneFound = 0;
+
+ function walk(node) {
+ if (node.children) {
+ node.children.forEach(walk);
+ }
+
+ if (!isSavedFrame(node.name)) {
+ return;
+ }
+
+ if (node.name.functionDisplayName === "shouldHaveCountOfFive") {
+ equal(node.count, 5, "shouldHaveCountOfFive should have count of five");
+ numShouldHaveFiveFound++;
+ }
+
+ if (node.name.functionDisplayName === "shouldHaveCountOfOne") {
+ equal(node.count, 1, "shouldHaveCountOfOne should have count of one");
+ numShouldHaveOneFound++;
+ }
+ }
+ markers.children.forEach(walk);
+
+ equal(numShouldHaveFiveFound, 1);
+ equal(numShouldHaveOneFound, 5);
+
+ compareCensusViewData(
+ BREAKDOWN,
+ report,
+ treeNode,
+ "Returning census as a tree node represents same data as the report"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js
new file mode 100644
index 0000000000..3f603122d3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses and return
+// an inverted CensusTreeNode.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ const { report: treeNode } = await client.takeCensus(
+ snapshotFilePath,
+ {
+ breakdown: BREAKDOWN,
+ },
+ {
+ asInvertedTreeNode: true,
+ }
+ );
+
+ compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true });
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js
new file mode 100644
index 0000000000..354aa129a0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Sanity test that we can compute shortest paths.
+//
+// Because the actual heap graph is too unpredictable and likely to drastically
+// change as various implementation bits change, we don't test exact paths
+// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can
+// control the specific graph shape and structure and so testing exact paths is
+// reliable.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const dominatedByRoot = dominatorTree
+ .getImmediatelyDominated(dominatorTree.root)
+ .slice(0, 10);
+ ok(dominatedByRoot);
+ ok(dominatedByRoot.length);
+
+ const targetSet = new Set(dominatedByRoot);
+
+ const shortestPaths = snapshot.computeShortestPaths(
+ dominatorTree.root,
+ dominatedByRoot,
+ 2
+ );
+ ok(shortestPaths);
+ ok(shortestPaths instanceof Map);
+ Assert.strictEqual(shortestPaths.size, targetSet.size);
+
+ for (const [target, paths] of shortestPaths) {
+ ok(targetSet.has(target), "We should only get paths for our targets");
+ targetSet.delete(target);
+
+ ok(
+ !!paths.length,
+ "We must have at least one path, since the target is dominated by the root"
+ );
+ Assert.lessOrEqual(
+ paths.length,
+ 2,
+ "Should not have recorded more paths than the max requested"
+ );
+
+ dumpn("---------------------");
+ dumpn("Shortest paths for 0x" + target.toString(16) + ":");
+ for (const pth of paths) {
+ dumpn(" path =");
+ for (const part of pth) {
+ dumpn(
+ " predecessor: 0x" +
+ part.predecessor.toString(16) +
+ "; edge: " +
+ part.edge
+ );
+ }
+ }
+ dumpn("---------------------");
+
+ for (const path2 of paths) {
+ ok(!!path2.length, "Cannot have zero length paths");
+ Assert.strictEqual(
+ path2[0].predecessor,
+ dominatorTree.root,
+ "The first predecessor is always our start node"
+ );
+
+ for (const part of path2) {
+ ok(part.predecessor, "Each part of a path has a predecessor");
+ ok(
+ !!snapshot.describeNode(
+ { by: "count", count: true, bytes: true },
+ part.predecessor
+ ),
+ "The predecessor is in the heap snapshot"
+ );
+ ok("edge" in part, "Each part has an (potentially null) edge property");
+ }
+ }
+ }
+
+ Assert.strictEqual(
+ targetSet.size,
+ 0,
+ "We found paths for all of our targets"
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js
new file mode 100644
index 0000000000..714986c601
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test computing shortest paths with invalid arguments.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const target = dominatorTree
+ .getImmediatelyDominated(dominatorTree.root)
+ .pop();
+ ok(target);
+
+ let threw = false;
+ try {
+ snapshot.computeShortestPaths(0, [target], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "invalid start node should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [0], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "invalid target nodes should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "empty target nodes should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [target], 0);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "0 max paths should throw");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js
new file mode 100644
index 0000000000..e18c79752f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.creationTime returns the expected time.
+
+function waitForThirtyMilliseconds() {
+ const start = Date.now();
+ while (Date.now() - start < 30) {
+ // do nothing
+ }
+}
+
+function run_test() {
+ const start = Date.now() * 1000;
+ info("start = " + start);
+
+ // Because Date.now() is less precise than the snapshot's time stamp, give it
+ // a little bit of head room. Additionally, WinXP's timer only has granularity
+ // of +/- 15ms.
+ waitForThirtyMilliseconds();
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ waitForThirtyMilliseconds();
+
+ const end = Date.now() * 1000;
+ info("end = " + end);
+
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ info("snapshot.creationTime = " + snapshot.creationTime);
+
+ Assert.greaterOrEqual(snapshot.creationTime, start);
+ Assert.lessOrEqual(snapshot.creationTime, end);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js
new file mode 100644
index 0000000000..7f88d0a142
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can save a core dump with very deep allocation stacks and read
+// it back into a HeapSnapshot.
+
+function stackDepth(stack) {
+ return stack ? 1 + stackDepth(stack.parent) : 0;
+}
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ Services.prefs.clearUserPref("security.allow_eval_in_parent_process");
+ });
+
+ // Create a Debugger observing a debuggee's allocations.
+ const debuggee = new Cu.Sandbox(null);
+ const dbg = new Debugger(debuggee);
+ dbg.memory.trackingAllocationSites = true;
+
+ // Allocate some objects in the debuggee that will have their allocation
+ // stacks recorded by the Debugger.
+
+ debuggee.eval("this.objects = []");
+ debuggee.eval(
+ function recursiveAllocate(n) {
+ if (n <= 0) {
+ return;
+ }
+
+ // Make sure to recurse before pushing the object so that when TCO is
+ // implemented sometime in the future, it doesn't invalidate this test.
+ recursiveAllocate(n - 1);
+ this.objects.push({});
+ }.toString()
+ );
+ debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);");
+ debuggee.eval("recursiveAllocate(200);");
+
+ // Now save a snapshot that will include the allocation stacks and read it
+ // back again.
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+
+ const report = snapshot.takeCensus({
+ breakdown: {
+ by: "allocationStack",
+ then: { by: "count", bytes: true, count: true },
+ noStack: { by: "count", bytes: true, count: true },
+ },
+ });
+
+ // Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`!
+ const MAX_STACK_DEPTH = 60;
+
+ let foundStacks = false;
+ report.forEach((v, k) => {
+ if (k === "noStack") {
+ return;
+ }
+
+ foundStacks = true;
+ const depth = stackDepth(k);
+ dumpn("Stack depth is " + depth);
+ Assert.lessOrEqual(
+ depth,
+ MAX_STACK_DEPTH,
+ "Every stack should have depth less than or equal to the maximum stack depth"
+ );
+ });
+ ok(foundStacks);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js
new file mode 100644
index 0000000000..8597fb4edf
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can describe nodes with a breakdown.
+
+function run_test() {
+ const path = saveNewHeapSnapshot();
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ ok(snapshot.describeNode);
+ equal(typeof snapshot.describeNode, "function");
+
+ const dt = snapshot.computeDominatorTree();
+
+ let threw = false;
+ try {
+ snapshot.describeNode(undefined, dt.root);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "Should require a breakdown");
+
+ const breakdown = {
+ by: "coarseType",
+ objects: { by: "objectClass" },
+ scripts: { by: "internalType" },
+ strings: { by: "internalType" },
+ other: { by: "internalType" },
+ };
+
+ threw = false;
+ try {
+ snapshot.describeNode(breakdown, 0);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "Should throw when given an invalid node id");
+
+ const description = snapshot.describeNode(breakdown, dt.root);
+ ok(description);
+ ok(description.other);
+ ok(description.other["JS::ubi::RootList"]);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js
new file mode 100644
index 0000000000..26997dd904
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test ChromeUtils.getObjectNodeId()
+
+function run_test() {
+ // Create a test object, which we want to analyse
+ const testObject = {
+ foo: {
+ bar: {},
+ },
+ };
+
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ // Get the NodeId for our test object
+ const objectNodeIdRoot = ChromeUtils.getObjectNodeId(testObject);
+ const objectNodeIdFoo = ChromeUtils.getObjectNodeId(testObject.foo);
+ const objectNodeIdBar = ChromeUtils.getObjectNodeId(testObject.foo.bar);
+
+ // Also try to ensure that this is the right object via its retained path
+ const shortestPaths = snapshot.computeShortestPaths(
+ objectNodeIdRoot,
+ [objectNodeIdBar],
+ 50
+ );
+ ok(shortestPaths);
+ ok(shortestPaths instanceof Map);
+ Assert.equal(
+ shortestPaths.size,
+ 1,
+ "We get only one path between the root object and bar object"
+ );
+
+ const paths = shortestPaths.get(objectNodeIdBar);
+ Assert.equal(paths.length, 1, "There is only one path between root and bar");
+ Assert.equal(
+ paths[0].length,
+ 2,
+ "The shortest path is made of two edges: foo and bar"
+ );
+
+ const [path1, path2] = paths[0];
+ Assert.equal(
+ path1.predecessor,
+ objectNodeIdRoot,
+ "The first edge goes from the root object"
+ );
+ Assert.equal(path1.edge, "foo", "The first edge is the foo attribute");
+
+ Assert.equal(
+ path2.predecessor,
+ objectNodeIdFoo,
+ "The second edge goes from the foo object"
+ );
+ Assert.equal(path2.edge, "bar", "The first edge is the bar attribute");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js
new file mode 100644
index 0000000000..f4a7836be3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus returns a value of an appropriate
+// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js
+
+function run_test() {
+ const dbg = new Debugger();
+
+ function checkProperties(census) {
+ equal(typeof census, "object");
+ for (const prop of Object.getOwnPropertyNames(census)) {
+ const desc = Object.getOwnPropertyDescriptor(census, prop);
+ equal(desc.enumerable, true);
+ equal(desc.configurable, true);
+ equal(desc.writable, true);
+ if (typeof desc.value === "object") {
+ checkProperties(desc.value);
+ } else {
+ equal(typeof desc.value, "number");
+ }
+ }
+ }
+
+ checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
+
+ const g = newGlobal();
+ dbg.addDebuggee(g);
+ checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js
new file mode 100644
index 0000000000..a26e9a96d8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus behaves plausibly as we allocate objects.
+//
+// Exact object counts vary in ways we can't predict. For example,
+// BaselineScripts can hold onto "template objects", which exist only to hold
+// the shape and type for newly created objects. When BaselineScripts are
+// discarded, these template objects go with them.
+//
+// So instead of expecting precise counts, we expect counts that are at least as
+// many as we would expect given the object graph we've built.
+//
+// Ported from js/src/jit-tests/debug/Memory-takeCensus-02.js
+
+function run_test() {
+ // A Debugger with no debuggees had better not find anything.
+ const dbg = new Debugger();
+ const census0 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census0, "census0", Census.assertAllZeros);
+
+ function newGlobalWithDefs() {
+ const g = newGlobal();
+ g.eval(`
+ function times(n, fn) {
+ var a=[];
+ for (var i = 0; i<n; i++)
+ a.push(fn());
+ return a;
+ }
+ `);
+ return g;
+ }
+
+ // Allocate a large number of various types of objects, and check that census
+ // finds them.
+ const g = newGlobalWithDefs();
+ dbg.addDebuggee(g);
+
+ g.eval("var objs = times(100, () => ({}));");
+ g.eval("var rxs = times(200, () => /foo/);");
+ g.eval("var ars = times(400, () => []);");
+ g.eval("var fns = times(800, () => () => {});");
+
+ const census1 = dbg.memory.takeCensus(dbg);
+ Census.walkCensus(
+ census1,
+ "census1",
+ Census.assertAllNotLessThan({
+ objects: {
+ Object: { count: 100 },
+ RegExp: { count: 200 },
+ Array: { count: 400 },
+ Function: { count: 800 },
+ },
+ })
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js
new file mode 100644
index 0000000000..06c574d4e0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus behaves plausibly as we add and remove
+// debuggees.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-03.js
+
+function run_test() {
+ const dbg = new Debugger();
+
+ const census0 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census0, "census0", Census.assertAllZeros);
+
+ const g1 = newGlobal();
+ dbg.addDebuggee(g1);
+ const census1 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0));
+
+ const g2 = newGlobal();
+ dbg.addDebuggee(g2);
+ const census2 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1));
+
+ dbg.removeDebuggee(g2);
+ const census3 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census3, "census3", Census.assertAllEqual(census1));
+
+ dbg.removeDebuggee(g1);
+ const census4 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census4, "census4", Census.assertAllEqual(census0));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js
new file mode 100644
index 0000000000..ac484c8815
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that HeapSnapshot.prototype.takeCensus finds GC roots that are on the
+// stack.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-04.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval(`
+function withAllocationMarkerOnStack(f) {
+ (function () {
+ var onStack = allocationMarker();
+ f();
+ }());
+}
+`);
+
+ equal(
+ "AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects,
+ false,
+ "There shouldn't exist any allocation markers in the census."
+ );
+
+ let allocationMarkerCount;
+ g.withAllocationMarkerOnStack(() => {
+ const census = saveHeapSnapshotAndTakeCensus(dbg);
+ allocationMarkerCount = census.objects.AllocationMarker.count;
+ });
+
+ equal(
+ allocationMarkerCount,
+ 1,
+ "Should have one allocation marker in the census, because there " +
+ "was one on the stack."
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js
new file mode 100644
index 0000000000..22c5324c68
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that HeapSnapshot.prototype.takeCensus finds cross compartment
+// wrapper GC roots.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-05.js
+
+/* eslint-disable strict */
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ equal(
+ "AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects,
+ false,
+ "No allocation markers should exist in the census."
+ );
+
+ this.ccw = g.allocationMarker();
+
+ const census = saveHeapSnapshotAndTakeCensus(dbg);
+ equal(
+ census.objects.AllocationMarker.count,
+ 1,
+ "Should have one allocation marker in the census, because there " +
+ "is one cross-compartment wrapper referring to it."
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js
new file mode 100644
index 0000000000..2b639411f9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js
+
+function run_test() {
+ const Pattern = Match.Pattern;
+
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ Pattern({ count: Pattern.NATURAL, bytes: Pattern.NATURAL }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } })
+ );
+
+ let census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: false, bytes: false },
+ });
+ equal("count" in census, false);
+ equal("bytes" in census, false);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: true, bytes: false },
+ });
+ equal("count" in census, true);
+ equal("bytes" in census, false);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: false, bytes: true },
+ });
+ equal("count" in census, false);
+ equal("bytes" in census, true);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: true, bytes: true },
+ });
+ equal("count" in census, true);
+ equal("bytes" in census, true);
+
+ // Pattern doesn't mind objects with extra properties, so we'll restrict this
+ // list to the object classes we're pretty sure are going to stick around for
+ // the forseeable future.
+ Pattern({
+ Function: { count: Pattern.NATURAL },
+ Object: { count: Pattern.NATURAL },
+ DebuggerPrototype: { count: Pattern.NATURAL },
+ Sandbox: { count: Pattern.NATURAL },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } })
+ );
+
+ Pattern({
+ objects: { count: Pattern.NATURAL },
+ scripts: { count: Pattern.NATURAL },
+ strings: { count: Pattern.NATURAL },
+ other: { count: Pattern.NATURAL },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "coarseType" } })
+ );
+
+ // As for { by: 'objectClass' }, restrict our pattern to the types
+ // we predict will stick around for a long time.
+ Pattern({
+ JSString: { count: Pattern.NATURAL },
+ "js::Shape": { count: Pattern.NATURAL },
+ JSObject: { count: Pattern.NATURAL },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } })
+ );
+
+ // Nested breakdowns.
+
+ const coarseTypePattern = {
+ objects: { count: Pattern.NATURAL },
+ scripts: { count: Pattern.NATURAL },
+ strings: { count: Pattern.NATURAL },
+ other: { count: Pattern.NATURAL },
+ };
+
+ Pattern({
+ JSString: coarseTypePattern,
+ "js::Shape": coarseTypePattern,
+ JSObject: coarseTypePattern,
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "internalType", then: { by: "coarseType" } },
+ })
+ );
+
+ Pattern({
+ Function: { count: Pattern.NATURAL },
+ Object: { count: Pattern.NATURAL },
+ DebuggerPrototype: { count: Pattern.NATURAL },
+ Sandbox: { count: Pattern.NATURAL },
+ other: coarseTypePattern,
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ then: { by: "count" },
+ other: { by: "coarseType" },
+ },
+ })
+ );
+
+ Pattern({
+ objects: { count: Pattern.NATURAL, label: "object" },
+ scripts: { count: Pattern.NATURAL, label: "scripts" },
+ strings: { count: Pattern.NATURAL, label: "strings" },
+ other: { count: Pattern.NATURAL, label: "other" },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ objects: { by: "count", label: "object" },
+ scripts: { by: "count", label: "scripts" },
+ strings: { by: "count", label: "strings" },
+ other: { by: "count", label: "other" },
+ },
+ })
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js
new file mode 100644
index 0000000000..56bec51074
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus breakdown: check error handling on property
+// gets.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-07.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ get by() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "count",
+ get count() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "count",
+ get bytes() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ get then() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ get other() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get objects() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get scripts() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get strings() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get other() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "internalType",
+ get then() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js
new file mode 100644
index 0000000000..f40d367cd6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval(`
+ var stuff = [];
+ function add(n, c) {
+ for (let i = 0; i < n; i++)
+ stuff.push(c());
+ }
+
+ let count = 0;
+
+ function obj() { return { count: count++ }; }
+ obj.factor = 1;
+
+ // This creates a closure (a function JSObject) that has captured
+ // a Call object. So each call creates two items.
+ function fun() { let v = count; return () => { return v; } }
+ fun.factor = 2;
+
+ function str() { return 'perambulator' + count++; }
+ str.factor = 1;
+
+ // Eval a fresh text each time, allocating:
+ // - a fresh ScriptSourceObject
+ // - a new JSScripts, not an eval cache hits
+ // - a fresh prototype object
+ // - a fresh Call object, since the eval makes 'ev' heavyweight
+ // - the new function itself
+ function ev() {
+ return eval(\`(function () { return \${ count++ } })\`);
+ }
+ ev.factor = 5;
+
+ // A new object (1) with a new shape (2) with a new atom (3)
+ function shape() { return { [ 'theobroma' + count++ ]: count }; }
+ shape.factor = 3;
+ `);
+
+ let baseline = 0;
+ function countIncreasedByAtLeast(n) {
+ const oldBaseline = baseline;
+
+ // Since a census counts only reachable objects, one might assume that calling
+ // GC here would have no effect on the census results. But GC also throws away
+ // JIT code and any objects it might be holding (template objects, say);
+ // takeCensus reaches those. Shake everything loose that we can, to make the
+ // census approximate reachability a bit more closely, and make our results a
+ // bit more predictable.
+ gc(g, "shrinking");
+
+ baseline = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count" },
+ }).count;
+ return baseline >= oldBaseline + n;
+ }
+
+ countIncreasedByAtLeast(0);
+
+ g.add(100, g.obj);
+ ok(countIncreasedByAtLeast(g.obj.factor * 100));
+
+ g.add(100, g.fun);
+ ok(countIncreasedByAtLeast(g.fun.factor * 100));
+
+ g.add(100, g.str);
+ ok(countIncreasedByAtLeast(g.str.factor * 100));
+
+ g.add(100, g.ev);
+ ok(countIncreasedByAtLeast(g.ev.factor * 100));
+
+ g.add(100, g.shape);
+ ok(countIncreasedByAtLeast(g.shape.factor * 100));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js
new file mode 100644
index 0000000000..99830d75cb
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval(` // 1
+ var log = []; // 2
+ function f() { log.push(allocationMarker()); } // 3
+ function g() { f(); } // 4
+ function h() { f(); } // 5
+ `);
+
+ // Create one allocationMarker with tracking turned off,
+ // so it will have no associated stack.
+ g.f();
+
+ dbg.memory.allocationSamplingProbability = 1;
+
+ for (const [func, n] of [
+ [g.f, 20],
+ [g.g, 10],
+ [g.h, 5],
+ ]) {
+ for (let i = 0; i < n; i++) {
+ dbg.memory.trackingAllocationSites = true;
+ // All allocations of allocationMarker occur with this line as the oldest
+ // stack frame.
+ func();
+ dbg.memory.trackingAllocationSites = false;
+ }
+ }
+
+ const census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", label: "haz stack" },
+ noStack: {
+ by: "count",
+ label: "no haz stack",
+ },
+ },
+ },
+ });
+
+ const map = census.AllocationMarker;
+ ok(map instanceof Map, "Should be a Map instance");
+ equal(
+ map.size,
+ 4,
+ "Should have 4 allocation stacks (including the lack of a stack)"
+ );
+
+ // Gather the stacks we are expecting to appear as keys, and
+ // check that there are no unexpected keys.
+ const stacks = {};
+
+ map.forEach((v, k) => {
+ if (k === "noStack") {
+ // No need to save this key.
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "run_test"
+ ) {
+ stacks.f = k;
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "g" &&
+ k.parent.parent.functionDisplayName === "run_test"
+ ) {
+ stacks.fg = k;
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "h" &&
+ k.parent.parent.functionDisplayName === "run_test"
+ ) {
+ stacks.fh = k;
+ } else {
+ dumpn("Unexpected allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+ ok(false);
+ }
+ });
+
+ equal(map.get("noStack").label, "no haz stack");
+ equal(map.get("noStack").count, 1);
+
+ ok(stacks.f);
+ equal(map.get(stacks.f).label, "haz stack");
+ equal(map.get(stacks.f).count, 20);
+
+ ok(stacks.fg);
+ equal(map.get(stacks.fg).label, "haz stack");
+ equal(map.get(stacks.fg).count, 10);
+
+ ok(stacks.fh);
+ equal(map.get(stacks.fh).label, "haz stack");
+ equal(map.get(stacks.fh).count, 5);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js
new file mode 100644
index 0000000000..9b0e0f8c74
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Check byte counts produced by takeCensus.
+//
+// Note that tracking allocation sites adds unique IDs to objects which
+// increases their size, making it hard to test reported sizes exactly.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ const sizeOfAM = byteSize(allocationMarker());
+
+ // Allocate a single allocation marker, and check that we can find it.
+ g.eval("var hold = allocationMarker();");
+ let census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass" },
+ });
+ equal(census.AllocationMarker.count, 1);
+ equal(census.AllocationMarker.bytes >= sizeOfAM, true);
+ g.hold = null;
+
+ g.eval(` // 1
+ var objs = []; // 2
+ function fnerd() { // 3
+ objs.push(allocationMarker()); // 4
+ for (let i = 0; i < 10; i++) // 5
+ objs.push(allocationMarker()); // 6
+ } // 7
+ `);
+
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.fnerd();
+ dbg.memory.trackingAllocationSites = false;
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass", then: { by: "allocationStack" } },
+ });
+
+ let seen = 0;
+ census.AllocationMarker.forEach((v, k) => {
+ equal(k.functionDisplayName, "fnerd");
+ switch (k.line) {
+ case 4:
+ equal(v.count, 1);
+ equal(v.bytes >= sizeOfAM, true);
+ seen++;
+ break;
+
+ case 6:
+ equal(v.count, 10);
+ equal(v.bytes >= 10 * sizeOfAM, true);
+ seen++;
+ break;
+
+ default:
+ dumpn("Unexpected stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+ ok(false);
+ break;
+ }
+ });
+
+ equal(seen, 2);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js
new file mode 100644
index 0000000000..151437bb41
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that Debugger.Memory.prototype.takeCensus and
+// HeapSnapshot.prototype.takeCensus return the same data for the same heap
+// graph.
+
+function doLiveAndOfflineCensus(g, dbg, opts) {
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.eval(` // 1
+ (function unsafeAtAnySpeed() { // 2
+ for (var i = 0; i < 100; i++) { // 3
+ this.markers.push(allocationMarker()); // 4
+ } // 5
+ }()); // 6
+ `);
+ dbg.memory.trackingAllocationSites = false;
+
+ return {
+ live: dbg.memory.takeCensus(opts),
+ offline: saveHeapSnapshotAndTakeCensus(dbg, opts),
+ };
+}
+
+function getMarkerSize(g, dbg) {
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.eval("var hold = allocationMarker();");
+ dbg.memory.trackingAllocationSites = false;
+ const live = dbg.memory.takeCensus({
+ breakdown: { by: "objectClass", then: { by: "count" } },
+ });
+ g.hold = null;
+ equal(live.AllocationMarker.count, 1);
+ return live.AllocationMarker.bytes;
+}
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval("this.markers = []");
+ const markerSize = getMarkerSize(g, dbg);
+
+ // First, test that we get the same counts and sizes as we allocate and retain
+ // more things.
+
+ let prevCount = 0;
+ let prevBytes = 0;
+
+ for (let i = 0; i < 10; i++) {
+ const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
+ breakdown: { by: "objectClass", then: { by: "count" } },
+ });
+
+ equal(live.AllocationMarker.count, offline.AllocationMarker.count);
+ equal(live.AllocationMarker.bytes, offline.AllocationMarker.bytes);
+ equal(live.AllocationMarker.count, prevCount + 100);
+ equal(live.AllocationMarker.bytes, prevBytes + 100 * markerSize);
+
+ prevCount = live.AllocationMarker.count;
+ prevBytes = live.AllocationMarker.bytes;
+ }
+
+ // Second, test that the reported allocation stacks and counts and sizes at
+ // those allocation stacks match up.
+
+ const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
+ breakdown: { by: "objectClass", then: { by: "allocationStack" } },
+ });
+
+ equal(live.AllocationMarker.size, offline.AllocationMarker.size);
+ // One stack with the loop further above, and another stack featuring the call
+ // right above.
+ equal(live.AllocationMarker.size, 2);
+
+ // Note that because SavedFrame stacks reconstructed from an offline heap
+ // snapshot don't have the same principals as SavedFrame stacks captured from
+ // a live stack, the live and offline allocation stacks won't be identity
+ // equal, but should be structurally the same.
+
+ const liveEntries = [];
+ live.AllocationMarker.forEach((v, k) => {
+ dumpn("Allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+
+ equal(k.functionDisplayName, "unsafeAtAnySpeed");
+ equal(k.line, 4);
+
+ liveEntries.push([k.toString(), v]);
+ });
+
+ const offlineEntries = [];
+ offline.AllocationMarker.forEach((v, k) => {
+ dumpn("Allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+
+ equal(k.functionDisplayName, "unsafeAtAnySpeed");
+ equal(k.line, 4);
+
+ offlineEntries.push([k.toString(), v]);
+ });
+
+ const sortEntries = (a, b) => {
+ if (a[0] < b[0]) {
+ return -1;
+ } else if (a[0] > b[0]) {
+ return 1;
+ }
+ return 0;
+ };
+ liveEntries.sort(sortEntries);
+ offlineEntries.sort(sortEntries);
+
+ equal(liveEntries.length, live.AllocationMarker.size);
+ equal(liveEntries.length, offlineEntries.length);
+
+ for (let i = 0; i < liveEntries.length; i++) {
+ equal(liveEntries[i][0], offlineEntries[i][0]);
+ equal(liveEntries[i][1].count, offlineEntries[i][1].count);
+ equal(liveEntries[i][1].bytes, offlineEntries[i][1].bytes);
+ }
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js
new file mode 100644
index 0000000000..f3a72102b0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that when we take a census and get a bucket list of ids that matched the
+// given category, that the returned ids are all in the snapshot and their
+// reported category.
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ const path = saveNewHeapSnapshot({ debugger: dbg });
+ const snapshot = readHeapSnapshot(path);
+
+ const bucket = { by: "bucket" };
+ const count = { by: "count", count: true, bytes: false };
+ const objectClassCount = { by: "objectClass", then: count, other: count };
+
+ const byClassName = snapshot.takeCensus({
+ breakdown: {
+ by: "objectClass",
+ then: bucket,
+ other: bucket,
+ },
+ });
+
+ const byClassNameCount = snapshot.takeCensus({
+ breakdown: objectClassCount,
+ });
+
+ const keys = new Set(Object.keys(byClassName));
+ equal(
+ keys.size,
+ Object.keys(byClassNameCount).length,
+ "Should have the same number of keys."
+ );
+ for (const k of Object.keys(byClassNameCount)) {
+ ok(keys.has(k), "Should not have any unexpected class names");
+ }
+
+ for (const key of Object.keys(byClassName)) {
+ equal(
+ byClassNameCount[key].count,
+ byClassName[key].length,
+ "Length of the bucket and count should be equal"
+ );
+
+ for (const id of byClassName[key]) {
+ const desc = snapshot.describeNode(objectClassCount, id);
+ equal(
+ desc[key].count,
+ 1,
+ "Describing the bucketed node confirms that it belongs to the category"
+ );
+ }
+ }
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js
new file mode 100644
index 0000000000..b7a3c9c2f8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can read core dumps into HeapSnapshot instances.
+/* eslint-disable strict */
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+}
+
+function run_test() {
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js
new file mode 100644
index 0000000000..273736abcd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can save a core dump with allocation stacks and read it back
+// into a HeapSnapshot.
+
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+}
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ Services.prefs.clearUserPref("security.allow_eval_in_parent_process");
+ });
+
+ // Create a Debugger observing a debuggee's allocations.
+ const debuggee = new Cu.Sandbox(null);
+ const dbg = new Debugger(debuggee);
+ dbg.memory.trackingAllocationSites = true;
+
+ // Allocate some objects in the debuggee that will have their allocation
+ // stacks recorded by the Debugger.
+ debuggee.eval("this.objects = []");
+ for (let i = 0; i < 100; i++) {
+ debuggee.eval("this.objects.push({})");
+ }
+
+ // Now save a snapshot that will include the allocation stacks and read it
+ // back again.
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js
new file mode 100644
index 0000000000..aecbd9cdda
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can read core dumps with a UTF8 path into HeapSnapshot instances.
+/* eslint-disable strict */
+add_task(async function () {
+ const fileNameWithRussianCharacters =
+ "Снимок памяти Click.ru 08.06.2020 (Firefox dump).fxsnapshot";
+ const filePathWithRussianCharacters = PathUtils.join(
+ PathUtils.tempDir,
+ fileNameWithRussianCharacters
+ );
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ await IOUtils.copy(filePath, filePathWithRussianCharacters);
+
+ ok(
+ await IOUtils.exists(filePathWithRussianCharacters),
+ `We could copy the file to the expected path ${filePathWithRussianCharacters}`
+ );
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePathWithRussianCharacters);
+ ok(snapshot, "Should be able to read a heap snapshot from an utf8 path");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js
new file mode 100644
index 0000000000..d7427a3367
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can read core dumps into HeapSnapshot instances in a worker.
+add_task(async function () {
+ const worker = new ChromeWorker("resource://test/heap-snapshot-worker.js");
+ worker.postMessage({});
+
+ let assertionCount = 0;
+ worker.onmessage = e => {
+ if (e.data.type !== "assertion") {
+ return;
+ }
+
+ ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
+ assertionCount++;
+ };
+
+ await waitForDone(worker);
+
+ Assert.greater(assertionCount, 0);
+ worker.terminate();
+});
+
+function waitForDone(w) {
+ return new Promise((resolve, reject) => {
+ w.onerror = e => {
+ reject();
+ ok(false, "Error in worker: " + e);
+ };
+
+ w.addEventListener("message", function listener(e) {
+ if (e.data.type === "done") {
+ w.removeEventListener("message", listener);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js
new file mode 100644
index 0000000000..33367af7f7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the ChromeUtils interface.
+// eslint-disable-next-line
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+}
+
+function run_test() {
+ ok(ChromeUtils, "Should be able to get the ChromeUtils interface");
+
+ testBadParameters();
+ testGoodParameters();
+
+ do_test_finished();
+}
+
+function testBadParameters() {
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot(),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if arguments aren't passed in."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot(null),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if boundaries isn't an object."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({}),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the boundaries object doesn't have any properties."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ runtime: true, globals: [this] }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the boundaries object has more than one property."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ debugger: {} }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the debuggees object is not a Debugger object"
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ globals: [{}] }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the globals array contains non-global objects."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ runtime: false }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if runtime is supplied and is not true."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ globals: null }),
+ /TypeError:.*can't be converted to a sequence/,
+ "Should throw if globals is not an object."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ globals: {} }),
+ /TypeError:.*can't be converted to a sequence/,
+ "Should throw if globals is not an array."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if debugger is the Debugger.prototype object."
+ );
+
+ Assert.throws(
+ () =>
+ ChromeUtils.saveHeapSnapshot({
+ get globals() {
+ return [this];
+ },
+ }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if boundaries property is a getter."
+ );
+}
+
+const makeNewSandbox = () =>
+ Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
+
+function testGoodParameters() {
+ const sandbox = makeNewSandbox();
+ let dbg = new Debugger(sandbox);
+
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot for a debuggee global.");
+
+ dbg = new Debugger();
+ const sandboxes = Array(10).fill(null).map(makeNewSandbox);
+ sandboxes.forEach(sb => dbg.addDebuggee(sb));
+
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot for many debuggee globals.");
+
+ dbg = new Debugger();
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot with no debuggee globals.");
+
+ ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot for a specific global.");
+
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot of the full runtime.");
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js
new file mode 100644
index 0000000000..625d359675
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `internalType` breakdown.
+ */
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+const REPORT = {
+ JSObject: {
+ bytes: 100,
+ count: 10,
+ },
+ "js::Shape": {
+ bytes: 500,
+ count: 50,
+ },
+ JSString: {
+ bytes: 10,
+ count: 1,
+ },
+};
+
+const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 610,
+ count: 0,
+ totalCount: 61,
+ children: [
+ {
+ name: "js::Shape",
+ bytes: 500,
+ totalBytes: 500,
+ count: 50,
+ totalCount: 50,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "JSObject",
+ bytes: 100,
+ totalBytes: 100,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ reportLeafIndex: 1,
+ },
+ {
+ name: "JSString",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 4,
+ parent: 1,
+ reportLeafIndex: 3,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js
new file mode 100644
index 0000000000..75ed87c25b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `coarseType` breakdown.
+ */
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: countBreakdown },
+ strings: countBreakdown,
+ scripts: countBreakdown,
+ other: { by: "internalType", then: countBreakdown },
+ domNode: countBreakdown,
+};
+
+const REPORT = {
+ objects: {
+ Function: { bytes: 10, count: 1 },
+ Array: { bytes: 20, count: 2 },
+ },
+ strings: { bytes: 10, count: 1 },
+ scripts: { bytes: 1, count: 1 },
+ other: {
+ "js::Shape": { bytes: 30, count: 3 },
+ "js::Shape2": { bytes: 40, count: 4 },
+ },
+ domNode: { bytes: 0, count: 0 },
+};
+
+const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 111,
+ count: 0,
+ totalCount: 12,
+ children: [
+ {
+ name: "other",
+ count: 0,
+ totalCount: 7,
+ bytes: 0,
+ totalBytes: 70,
+ children: [
+ {
+ name: "js::Shape2",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 9,
+ parent: 7,
+ reportLeafIndex: 8,
+ },
+ {
+ name: "js::Shape",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 8,
+ parent: 7,
+ reportLeafIndex: 7,
+ },
+ ],
+ id: 7,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "objects",
+ count: 0,
+ totalCount: 3,
+ bytes: 0,
+ totalBytes: 30,
+ children: [
+ {
+ name: "Array",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 4,
+ parent: 2,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Function",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 2,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "strings",
+ count: 1,
+ totalCount: 1,
+ bytes: 10,
+ totalBytes: 10,
+ children: undefined,
+ id: 6,
+ parent: 1,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "scripts",
+ count: 1,
+ totalCount: 1,
+ bytes: 1,
+ totalBytes: 1,
+ children: undefined,
+ id: 5,
+ parent: 1,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js
new file mode 100644
index 0000000000..e0ca7dc15d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `objectClass` breakdown.
+ */
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: countBreakdown,
+ other: { by: "internalType", then: countBreakdown },
+};
+
+const REPORT = {
+ Function: { bytes: 10, count: 10 },
+ Array: { bytes: 100, count: 1 },
+ other: {
+ "JIT::CODE::NOW!!!": { bytes: 20, count: 2 },
+ "JIT::CODE::LATER!!!": { bytes: 40, count: 4 },
+ },
+};
+
+const EXPECTED = {
+ name: null,
+ count: 0,
+ totalCount: 17,
+ bytes: 0,
+ totalBytes: 170,
+ children: [
+ {
+ name: "Array",
+ bytes: 100,
+ totalBytes: 100,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "other",
+ count: 0,
+ totalCount: 6,
+ bytes: 0,
+ totalBytes: 60,
+ children: [
+ {
+ name: "JIT::CODE::LATER!!!",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 6,
+ parent: 4,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "JIT::CODE::NOW!!!",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 4,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "Function",
+ bytes: 10,
+ totalBytes: 10,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js
new file mode 100644
index 0000000000..e1cc32d697
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `allocationStack` breakdown.
+ */
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ let stack1, stack2, stack3, stack4;
+
+ (function a() {
+ (function b() {
+ (function c() {
+ stack1 = saveStack(3);
+ })();
+ (function d() {
+ stack2 = saveStack(3);
+ stack3 = saveStack(3);
+ })();
+ stack4 = saveStack(2);
+ })();
+ })();
+
+ const stack5 = saveStack(1);
+
+ const REPORT = new Map([
+ [stack1, { bytes: 10, count: 1 }],
+ [stack2, { bytes: 20, count: 2 }],
+ [stack3, { bytes: 30, count: 3 }],
+ [stack4, { bytes: 40, count: 4 }],
+ [stack5, { bytes: 50, count: 5 }],
+ ["noStack", { bytes: 60, count: 6 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 210,
+ count: 0,
+ totalCount: 21,
+ children: [
+ {
+ name: stack4.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack3.parent,
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: stack3,
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 7,
+ parent: 5,
+ reportLeafIndex: 3,
+ },
+ {
+ name: stack2,
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 6,
+ parent: 5,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 5,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: stack4,
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 8,
+ parent: 2,
+ reportLeafIndex: 4,
+ },
+ {
+ name: stack1.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: stack1,
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 4,
+ parent: 3,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 3,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "noStack",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ children: undefined,
+ id: 10,
+ parent: 1,
+ reportLeafIndex: 6,
+ },
+ {
+ name: stack5,
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 9,
+ parent: 1,
+ reportLeafIndex: 5,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js
new file mode 100644
index 0000000000..fbd9944507
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `allocationStack` => `objectClass` breakdown.
+ */
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: {
+ by: "objectClass",
+ then: countBreakdown,
+ other: countBreakdown,
+ },
+ noStack: countBreakdown,
+ };
+
+ let stack;
+
+ (function a() {
+ (function b() {
+ (function c() {
+ stack = saveStack(3);
+ })();
+ })();
+ })();
+
+ const REPORT = new Map([
+ [
+ stack,
+ {
+ Foo: { bytes: 10, count: 1 },
+ Bar: { bytes: 20, count: 2 },
+ Baz: { bytes: 30, count: 3 },
+ other: { bytes: 40, count: 4 },
+ },
+ ],
+ ["noStack", { bytes: 50, count: 5 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 150,
+ count: 0,
+ totalCount: 15,
+ children: [
+ {
+ name: stack.parent.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "other",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 8,
+ parent: 4,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "Baz",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 7,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ {
+ name: "Bar",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 6,
+ parent: 4,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Foo",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 4,
+ parent: 3,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 3,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "noStack",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 9,
+ parent: 1,
+ reportLeafIndex: 6,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js
new file mode 100644
index 0000000000..70cc4a2696
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test inverting CensusTreeNode with a by alloaction stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ };
+
+ function a(n) {
+ return b(n);
+ }
+ function b(n) {
+ return c(n);
+ }
+ function c(n) {
+ return saveStack(n);
+ }
+ function d(n) {
+ return b(n);
+ }
+ function e(n) {
+ return c(n);
+ }
+
+ const abc_Stack = a(3);
+ const bc_Stack = b(2);
+ const c_Stack = c(1);
+ const dbc_Stack = d(3);
+ const ec_Stack = e(2);
+
+ const REPORT = new Map([
+ [abc_Stack, { bytes: 10, count: 1 }],
+ [bc_Stack, { bytes: 10, count: 1 }],
+ [c_Stack, { bytes: 10, count: 1 }],
+ [dbc_Stack, { bytes: 10, count: 1 }],
+ [ec_Stack, { bytes: 10, count: 1 }],
+ ["noStack", { bytes: 50, count: 5 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "noStack",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 16,
+ parent: 15,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 6,
+ },
+ {
+ name: abc_Stack,
+ bytes: 50,
+ totalBytes: 10,
+ count: 5,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: abc_Stack.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 22,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: abc_Stack.parent.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 21,
+ parent: 20,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 20,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: dbc_Stack.parent.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 24,
+ parent: 23,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 23,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 19,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: ec_Stack.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 26,
+ parent: 25,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 25,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 17,
+ parent: 14,
+ reportLeafIndex: new Set([1, 2, 3, 4, 5]),
+ },
+ ],
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js
new file mode 100644
index 0000000000..009848b9e6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: [
+ {
+ name: "js::Shape",
+ bytes: 80,
+ totalBytes: 80,
+ count: 8,
+ totalCount: 8,
+ children: [
+ {
+ name: "other",
+ bytes: 0,
+ totalBytes: 80,
+ count: 0,
+ totalCount: 8,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 15,
+ parent: 14,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: 9,
+ },
+ {
+ name: "JSAtom",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ children: [
+ {
+ name: "strings",
+ bytes: 0,
+ totalBytes: 60,
+ count: 0,
+ totalCount: 6,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 17,
+ parent: 16,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 16,
+ parent: 12,
+ reportLeafIndex: 7,
+ },
+ {
+ name: "Array",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 21,
+ parent: 20,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 20,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 19,
+ parent: 12,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "js::jit::JitScript",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: [
+ {
+ name: "scripts",
+ bytes: 0,
+ totalBytes: 30,
+ count: 0,
+ totalCount: 3,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 24,
+ parent: 23,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 23,
+ parent: 22,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 22,
+ parent: 12,
+ reportLeafIndex: 5,
+ },
+ ],
+ id: 12,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js
new file mode 100644
index 0000000000..0e8e2ebcc1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ "http://example.com/app.js": {
+ JSScript: { count: 10, bytes: 100 },
+ },
+ "http://example.com/ads.js": {
+ "js::LazyScript": { count: 20, bytes: 200 },
+ },
+ "http://example.com/trackers.js": {
+ JSScript: { count: 30, bytes: 300 },
+ },
+ noFilename: {
+ "js::jit::JitCode": { count: 40, bytes: 400 },
+ },
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 1000,
+ count: 0,
+ totalCount: 100,
+ children: [
+ {
+ name: "noFilename",
+ bytes: 0,
+ totalBytes: 400,
+ count: 0,
+ totalCount: 40,
+ children: [
+ {
+ name: "js::jit::JitCode",
+ bytes: 400,
+ totalBytes: 400,
+ count: 40,
+ totalCount: 40,
+ children: undefined,
+ id: 9,
+ parent: 8,
+ reportLeafIndex: 8,
+ },
+ ],
+ id: 8,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/trackers.js",
+ bytes: 0,
+ totalBytes: 300,
+ count: 0,
+ totalCount: 30,
+ children: [
+ {
+ name: "JSScript",
+ bytes: 300,
+ totalBytes: 300,
+ count: 30,
+ totalCount: 30,
+ children: undefined,
+ id: 7,
+ parent: 6,
+ reportLeafIndex: 6,
+ },
+ ],
+ id: 6,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/ads.js",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 20,
+ children: [
+ {
+ name: "js::LazyScript",
+ bytes: 200,
+ totalBytes: 200,
+ count: 20,
+ totalCount: 20,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 4,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/app.js",
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "JSScript",
+ bytes: 100,
+ totalBytes: 100,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 3,
+ parent: 2,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js
new file mode 100644
index 0000000000..5f91e5e67b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test that repeatedly converting the same census report to a CensusTreeNode
+ * tree results in the same CensusTreeNode tree.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ "http://example.com/app.js": {
+ JSScript: { count: 10, bytes: 100 },
+ },
+ "http://example.com/ads.js": {
+ "js::LazyScript": { count: 20, bytes: 200 },
+ },
+ "http://example.com/trackers.js": {
+ JSScript: { count: 30, bytes: 300 },
+ },
+ noFilename: {
+ "js::jit::JitCode": { count: 40, bytes: 400 },
+ },
+ };
+
+ const first = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ const second = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ const third = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+
+ assertStructurallyEquivalent(first, second);
+ assertStructurallyEquivalent(second, third);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js
new file mode 100644
index 0000000000..ba783f8e47
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test when multiple leaves in the census report map to the same node in an
+ * inverted CensusReportTree.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { count: 1, bytes: 10 },
+ },
+ other: {
+ Array: { count: 1, bytes: 10 },
+ },
+ strings: { count: 0, bytes: 0 },
+ scripts: { count: 0, bytes: 0 },
+ domNode: { count: 0, bytes: 0 },
+ };
+
+ const node = censusReportToCensusTreeNode(BREAKDOWN, REPORT, {
+ invert: true,
+ });
+
+ equal(node.children[0].name, "Array");
+ equal(node.children[0].reportLeafIndex.size, 2);
+ dumpn(
+ `node.children[0].reportLeafIndex = ${[
+ ...node.children[0].reportLeafIndex,
+ ]}`
+ );
+ ok(node.children[0].reportLeafIndex.has(2));
+ ok(node.children[0].reportLeafIndex.has(6));
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js
new file mode 100644
index 0000000000..c90b34af70
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "internalType".
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ JSObject: {
+ count: 10,
+ bytes: 100,
+ },
+ "js::Shape": {
+ count: 50,
+ bytes: 500,
+ },
+ JSString: {
+ count: 0,
+ bytes: 0,
+ },
+ "js::LazyScript": {
+ count: 1,
+ bytes: 10,
+ },
+};
+
+const REPORT2 = {
+ JSObject: {
+ count: 11,
+ bytes: 110,
+ },
+ "js::Shape": {
+ count: 51,
+ bytes: 510,
+ },
+ JSString: {
+ count: 1,
+ bytes: 1,
+ },
+ "js::BaseShape": {
+ count: 1,
+ bytes: 42,
+ },
+};
+
+const EXPECTED = {
+ JSObject: {
+ count: 1,
+ bytes: 10,
+ },
+ "js::Shape": {
+ count: 1,
+ bytes: 10,
+ },
+ JSString: {
+ count: 1,
+ bytes: 1,
+ },
+ "js::LazyScript": {
+ count: -1,
+ bytes: -10,
+ },
+ "js::BaseShape": {
+ count: 1,
+ bytes: 42,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js
new file mode 100644
index 0000000000..cefe76abde
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "count".
+
+const BREAKDOWN = { by: "count", count: true, bytes: true };
+
+const REPORT1 = {
+ count: 10,
+ bytes: 100,
+};
+
+const REPORT2 = {
+ count: 11,
+ bytes: 110,
+};
+
+const EXPECTED = {
+ count: 1,
+ bytes: 10,
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js
new file mode 100644
index 0000000000..e625f4e0be
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "coarseType".
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ objects: {
+ count: 1,
+ bytes: 10,
+ },
+ scripts: {
+ count: 1,
+ bytes: 10,
+ },
+ strings: {
+ count: 1,
+ bytes: 10,
+ },
+ other: {
+ count: 3,
+ bytes: 30,
+ },
+ domNode: {
+ count: 0,
+ bytes: 0,
+ },
+};
+
+const REPORT2 = {
+ objects: {
+ count: 1,
+ bytes: 10,
+ },
+ scripts: {
+ count: 0,
+ bytes: 0,
+ },
+ strings: {
+ count: 2,
+ bytes: 20,
+ },
+ other: {
+ count: 4,
+ bytes: 40,
+ },
+ domNode: {
+ count: 0,
+ bytes: 0,
+ },
+};
+
+const EXPECTED = {
+ objects: {
+ count: 0,
+ bytes: 0,
+ },
+ scripts: {
+ count: -1,
+ bytes: -10,
+ },
+ strings: {
+ count: 1,
+ bytes: 10,
+ },
+ other: {
+ count: 1,
+ bytes: 10,
+ },
+ domNode: {
+ count: 0,
+ bytes: 0,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js
new file mode 100644
index 0000000000..036a33804f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "objectClass".
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ Array: {
+ count: 1,
+ bytes: 100,
+ },
+ Function: {
+ count: 10,
+ bytes: 10,
+ },
+ other: {
+ count: 10,
+ bytes: 100,
+ },
+};
+
+const REPORT2 = {
+ Object: {
+ count: 1,
+ bytes: 100,
+ },
+ Function: {
+ count: 20,
+ bytes: 20,
+ },
+ other: {
+ count: 10,
+ bytes: 100,
+ },
+};
+
+const EXPECTED = {
+ Array: {
+ count: -1,
+ bytes: -100,
+ },
+ Function: {
+ count: 10,
+ bytes: 10,
+ },
+ other: {
+ count: 0,
+ bytes: 0,
+ },
+ Object: {
+ count: 1,
+ bytes: 100,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js
new file mode 100644
index 0000000000..1ce0cfeb5b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "allocationStack".
+
+const BREAKDOWN = {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+};
+
+const stack1 = saveStack();
+const stack2 = saveStack();
+const stack3 = saveStack();
+
+const REPORT1 = new Map([
+ [stack1, { count: 10, bytes: 100 }],
+ [stack2, { count: 1, bytes: 10 }],
+]);
+
+const REPORT2 = new Map([
+ [stack2, { count: 10, bytes: 100 }],
+ [stack3, { count: 1, bytes: 10 }],
+]);
+
+const EXPECTED = new Map([
+ [stack1, { count: -10, bytes: -100 }],
+ [stack2, { count: 9, bytes: 90 }],
+ [stack3, { count: 1, bytes: 10 }],
+]);
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js
new file mode 100644
index 0000000000..b805de48dd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of a "complex" and "realistic" breakdown.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "allocationStack",
+ then: {
+ by: "objectClass",
+ then: { by: "count", count: false, bytes: true },
+ other: { by: "count", count: false, bytes: true },
+ },
+ noStack: {
+ by: "objectClass",
+ then: { by: "count", count: false, bytes: true },
+ other: { by: "count", count: false, bytes: true },
+ },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+ domNode: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+};
+
+const stack1 = saveStack();
+const stack2 = saveStack();
+const stack3 = saveStack();
+
+const REPORT1 = {
+ objects: new Map([
+ [
+ stack1,
+ { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } },
+ ],
+ [stack2, { Array: { bytes: 3 }, Date: { bytes: 4 }, other: { bytes: 0 } }],
+ ["noStack", { Object: { bytes: 3 } }],
+ ]),
+ strings: {
+ JSAtom: { bytes: 10 },
+ JSLinearString: { bytes: 5 },
+ },
+ scripts: {
+ JSScript: { bytes: 1 },
+ "js::jit::JitCode": { bytes: 2 },
+ },
+ other: {
+ "mozilla::dom::Thing": { bytes: 1 },
+ },
+ domNode: {},
+};
+
+const REPORT2 = {
+ objects: new Map([
+ [stack2, { Array: { bytes: 1 }, Date: { bytes: 2 }, other: { bytes: 3 } }],
+ [
+ stack3,
+ { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } },
+ ],
+ ["noStack", { Object: { bytes: 3 } }],
+ ]),
+ strings: {
+ JSAtom: { bytes: 5 },
+ JSLinearString: { bytes: 10 },
+ },
+ scripts: {
+ JSScript: { bytes: 2 },
+ "js::LazyScript": { bytes: 42 },
+ "js::jit::JitCode": { bytes: 1 },
+ },
+ other: {
+ "mozilla::dom::OtherThing": { bytes: 1 },
+ },
+ domNode: {},
+};
+
+const EXPECTED = {
+ objects: new Map([
+ [
+ stack1,
+ { Function: { bytes: -1 }, Object: { bytes: -2 }, other: { bytes: 0 } },
+ ],
+ [
+ stack2,
+ { Array: { bytes: -2 }, Date: { bytes: -2 }, other: { bytes: 3 } },
+ ],
+ [
+ stack3,
+ { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } },
+ ],
+ ["noStack", { Object: { bytes: 0 } }],
+ ]),
+ scripts: {
+ JSScript: {
+ bytes: 1,
+ },
+ "js::jit::JitCode": {
+ bytes: -1,
+ },
+ "js::LazyScript": {
+ bytes: 42,
+ },
+ },
+ strings: {
+ JSAtom: {
+ bytes: -5,
+ },
+ JSLinearString: {
+ bytes: 5,
+ },
+ },
+ other: {
+ "mozilla::dom::Thing": {
+ bytes: -1,
+ },
+ "mozilla::dom::OtherThing": {
+ bytes: 1,
+ },
+ },
+ domNode: {},
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js
new file mode 100644
index 0000000000..961023b2c2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test filtering basic CensusTreeNode trees.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ UInt8Array: { bytes: 80, count: 8 },
+ Int32Array: { bytes: 320, count: 32 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 620,
+ count: 0,
+ totalCount: 62,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 450,
+ count: 0,
+ totalCount: 45,
+ children: [
+ {
+ name: "Int32Array",
+ bytes: 320,
+ totalBytes: 320,
+ count: 32,
+ totalCount: 32,
+ children: undefined,
+ id: 16,
+ parent: 15,
+ reportLeafIndex: 4,
+ },
+ {
+ name: "UInt8Array",
+ bytes: 80,
+ totalBytes: 80,
+ count: 8,
+ totalCount: 8,
+ children: undefined,
+ id: 17,
+ parent: 15,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Array",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 18,
+ parent: 15,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 15,
+ parent: 14,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "Array" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js
new file mode 100644
index 0000000000..9915acb678
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test filtering CensusTreeNode trees with an `allocationStack` breakdown.
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ let stack1, stack2, stack3, stack4;
+
+ (function foo() {
+ (function bar() {
+ (function baz() {
+ stack1 = saveStack(3);
+ })();
+ (function quux() {
+ stack2 = saveStack(3);
+ stack3 = saveStack(3);
+ })();
+ })();
+ stack4 = saveStack(2);
+ })();
+
+ const stack5 = saveStack(1);
+
+ const REPORT = new Map([
+ [stack1, { bytes: 10, count: 1 }],
+ [stack2, { bytes: 20, count: 2 }],
+ [stack3, { bytes: 30, count: 3 }],
+ [stack4, { bytes: 40, count: 4 }],
+ [stack5, { bytes: 50, count: 5 }],
+ ["noStack", { bytes: 60, count: 6 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 210,
+ count: 0,
+ totalCount: 21,
+ children: [
+ {
+ name: stack1.parent.parent,
+ bytes: 0,
+ totalBytes: 60,
+ count: 0,
+ totalCount: 6,
+ children: [
+ {
+ name: stack2.parent,
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: stack3,
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 3,
+ },
+ {
+ name: stack2,
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 16,
+ parent: 14,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: stack1.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: stack1,
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 17,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 12,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "bar" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js
new file mode 100644
index 0000000000..dc144bac2e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test filtering with no matches.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ UInt8Array: { bytes: 80, count: 8 },
+ Int32Array: { bytes: 320, count: 32 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 620,
+ count: 0,
+ totalCount: 62,
+ children: undefined,
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, {
+ filter: "zzzzzzzzzzzzzzzzzzzz",
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js
new file mode 100644
index 0000000000..7e37b2c5da
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the filtered nodes' counts and bytes are the same as they were when
+// unfiltered.
+
+function run_test() {
+ const COUNT = { by: "count", count: true, bytes: true };
+ const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: COUNT, other: COUNT },
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: INTERNAL_TYPE,
+ noFilename: INTERNAL_TYPE,
+ },
+ other: INTERNAL_TYPE,
+ domNode: { by: "descriptiveType", then: COUNT, other: COUNT },
+ };
+
+ const REPORT = {
+ objects: {
+ Function: {
+ count: 7,
+ bytes: 70,
+ },
+ Array: {
+ count: 6,
+ bytes: 60,
+ },
+ },
+ scripts: {
+ "http://mozilla.github.io/pdf.js/build/pdf.js": {
+ "js::LazyScript": {
+ count: 4,
+ bytes: 40,
+ },
+ },
+ },
+ strings: {
+ count: 2,
+ bytes: 20,
+ },
+ other: {
+ "js::Shape": {
+ count: 1,
+ bytes: 10,
+ },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 20,
+ parent: undefined,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 130,
+ count: 0,
+ totalCount: 13,
+ children: [
+ {
+ name: "Function",
+ bytes: 70,
+ totalBytes: 70,
+ count: 7,
+ totalCount: 7,
+ id: 14,
+ parent: 13,
+ children: undefined,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "Array",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ id: 15,
+ parent: 13,
+ children: undefined,
+ reportLeafIndex: 3,
+ },
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 12,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "objects" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js
new file mode 100644
index 0000000000..667717688c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that filtered and inverted allocation stack census trees are sorted
+// properly.
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ const stacks = [];
+
+ function foo(depth = 1) {
+ stacks.push(saveStack(depth));
+ bar(depth + 1);
+ baz(depth + 1);
+ stacks.push(saveStack(depth));
+ }
+
+ function bar(depth = 1) {
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ }
+
+ function baz(depth = 1) {
+ stacks.push(saveStack(depth));
+ bang(depth + 1);
+ stacks.push(saveStack(depth));
+ }
+
+ function bang(depth = 1) {
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ }
+
+ foo();
+ bar();
+ baz();
+ bang();
+
+ const REPORT = new Map(
+ stacks.map((s, i) => {
+ return [
+ s,
+ {
+ count: i + 1,
+ bytes: (i + 1) * 10,
+ },
+ ];
+ })
+ );
+
+ const tree = censusReportToCensusTreeNode(BREAKDOWN, REPORT, {
+ filter: "baz",
+ invert: true,
+ });
+
+ dumpn("tree = " + JSON.stringify(tree, savedFrameReplacer, 4));
+
+ (function assertSortedBySelf(node) {
+ if (node.children) {
+ let lastSelfBytes = Infinity;
+ for (const child of node.children) {
+ Assert.lessOrEqual(
+ child.bytes,
+ lastSelfBytes,
+ `${child.bytes} <= ${lastSelfBytes}`
+ );
+ lastSelfBytes = child.bytes;
+ assertSortedBySelf(child);
+ }
+ }
+ })(tree);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js
new file mode 100644
index 0000000000..e89048c333
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can turn a breakdown with { by: "count" } leaves into a
+// breakdown with { by: "bucket" } leaves.
+
+const COUNT = { by: "count", count: true, bytes: true };
+const BUCKET = { by: "bucket" };
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: COUNT, other: COUNT },
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: { by: "internalType", then: COUNT },
+ noFilename: { by: "internalType", then: COUNT },
+ },
+ other: { by: "internalType", then: COUNT },
+};
+
+const EXPECTED = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: BUCKET, other: BUCKET },
+ strings: BUCKET,
+ scripts: {
+ by: "filename",
+ then: { by: "internalType", then: BUCKET },
+ noFilename: { by: "internalType", then: BUCKET },
+ },
+ other: { by: "internalType", then: BUCKET },
+};
+
+function run_test() {
+ assertCountToBucketBreakdown(BREAKDOWN, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js
new file mode 100644
index 0000000000..fc2864f5f4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the behavior of the deduplicatePaths utility function.
+
+function edge(from, to, name) {
+ return { from, to, name };
+}
+
+function run_test() {
+ const a = 1;
+ const b = 2;
+ const c = 3;
+ const d = 4;
+ const e = 5;
+ const f = 6;
+ const g = 7;
+
+ dumpn("Single long path");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [
+ pathEntry(a, "e1"),
+ pathEntry(b, "e2"),
+ pathEntry(c, "e3"),
+ pathEntry(d, "e4"),
+ pathEntry(e, "e5"),
+ pathEntry(f, "e6"),
+ ],
+ ],
+ expectedNodes: [a, b, c, d, e, f, g],
+ expectedEdges: [
+ edge(a, b, "e1"),
+ edge(b, c, "e2"),
+ edge(c, d, "e3"),
+ edge(d, e, "e4"),
+ edge(e, f, "e5"),
+ edge(f, g, "e6"),
+ ],
+ });
+
+ dumpn("Multiple edges from and to the same nodes");
+ assertDeduplicatedPaths({
+ target: a,
+ paths: [[pathEntry(b, "x")], [pathEntry(b, "y")], [pathEntry(b, "z")]],
+ expectedNodes: [a, b],
+ expectedEdges: [edge(b, a, "x"), edge(b, a, "y"), edge(b, a, "z")],
+ });
+
+ dumpn("Multiple paths sharing some nodes and edges");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [pathEntry(a, "a->b"), pathEntry(b, "b->c"), pathEntry(c, "foo")],
+ [pathEntry(a, "a->b"), pathEntry(b, "b->d"), pathEntry(d, "bar")],
+ [pathEntry(a, "a->b"), pathEntry(b, "b->e"), pathEntry(e, "baz")],
+ ],
+ expectedNodes: [a, b, c, d, e, g],
+ expectedEdges: [
+ edge(a, b, "a->b"),
+ edge(b, c, "b->c"),
+ edge(b, d, "b->d"),
+ edge(b, e, "b->e"),
+ edge(c, g, "foo"),
+ edge(d, g, "bar"),
+ edge(e, g, "baz"),
+ ],
+ });
+
+ dumpn("Second shortest path contains target itself");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [pathEntry(a, "a->b"), pathEntry(b, "b->g")],
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->g"),
+ pathEntry(g, "g->f"),
+ pathEntry(f, "f->g"),
+ ],
+ ],
+ expectedNodes: [a, b, g],
+ expectedEdges: [edge(a, b, "a->b"), edge(b, g, "b->g")],
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js
new file mode 100644
index 0000000000..6963f3f33a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test basic functionality of `CensusUtils.getCensusIndividuals`.
+
+function run_test() {
+ const stack1 = saveStack(1);
+ const stack2 = saveStack(1);
+ const stack3 = saveStack(1);
+
+ const COUNT = { by: "count", count: true, bytes: true };
+ const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: INTERNAL_TYPE,
+ noStack: INTERNAL_TYPE,
+ };
+
+ const MOCK_SNAPSHOT = {
+ takeCensus: ({ breakdown }) => {
+ assertStructurallyEquivalent(
+ breakdown,
+ CensusUtils.countToBucketBreakdown(BREAKDOWN)
+ );
+
+ // DFS Index
+ // prettier-ignore
+ return new Map([ // 0
+ [stack1, { // 1
+ JSObject: [101, 102, 103], // 2
+ JSString: [111, 112, 113], // 3
+ }],
+ [stack2, { // 4
+ JSObject: [201, 202, 203], // 5
+ JSString: [211, 212, 213], // 6
+ }],
+ [stack3, { // 7
+ JSObject: [301, 302, 303], // 8
+ JSString: [311, 312, 313], // 9
+ }],
+ ["noStack", { // 10
+ JSObject: [401, 402, 403], // 11
+ JSString: [411, 412, 413], // 12
+ }],
+ ]);
+ },
+ };
+
+ const INDICES = new Set([3, 5, 9]);
+
+ const EXPECTED = new Set([111, 112, 113, 201, 202, 203, 311, 312, 313]);
+
+ const actual = new Set(
+ CensusUtils.getCensusIndividuals(INDICES, BREAKDOWN, MOCK_SNAPSHOT)
+ );
+
+ assertStructurallyEquivalent(EXPECTED, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js
new file mode 100644
index 0000000000..04aae90e99
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test basic functionality of `CensusUtils.getReportLeaves`.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { count: 6, bytes: 60 },
+ Function: { count: 1, bytes: 10 },
+ Object: { count: 1, bytes: 10 },
+ RegExp: { count: 1, bytes: 10 },
+ other: { count: 0, bytes: 0 },
+ },
+ strings: { count: 1, bytes: 10 },
+ scripts: {
+ "foo.js": {
+ JSScript: { count: 1, bytes: 10 },
+ "js::jit::IonScript": { count: 1, bytes: 10 },
+ },
+ noFilename: {
+ JSScript: { count: 1, bytes: 10 },
+ "js::jit::IonScript": { count: 1, bytes: 10 },
+ },
+ },
+ other: {
+ "js::Shape": { count: 7, bytes: 70 },
+ "js::BaseShape": { count: 1, bytes: 10 },
+ },
+ domNode: {},
+ };
+
+ const root = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ dumpn("CensusTreeNode tree = " + JSON.stringify(root, null, 4));
+
+ (function assertEveryNodeCanFindItsLeaf(node) {
+ if (node.reportLeafIndex) {
+ const [leaf] = CensusUtils.getReportLeaves(
+ new Set([node.reportLeafIndex]),
+ BREAKDOWN,
+ REPORT
+ );
+ ok(
+ leaf,
+ "Should be able to find leaf " +
+ "for a node with a reportLeafIndex = " +
+ node.reportLeafIndex
+ );
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ assertEveryNodeCanFindItsLeaf(child);
+ }
+ }
+ })(root);
+
+ // Test finding multiple leaves at a time.
+
+ function find(name, node) {
+ if (node.name === name) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = find(name, child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return undefined;
+ }
+
+ const arrayNode = find("Array", root);
+ ok(arrayNode);
+ equal(typeof arrayNode.reportLeafIndex, "number");
+
+ const shapeNode = find("js::Shape", root);
+ ok(shapeNode);
+ equal(typeof shapeNode.reportLeafIndex, "number");
+
+ const indices = new Set([
+ arrayNode.reportLeafIndex,
+ shapeNode.reportLeafIndex,
+ ]);
+ const leaves = CensusUtils.getReportLeaves(indices, BREAKDOWN, REPORT);
+ equal(leaves.length, 2);
+
+ // `getReportLeaves` does not guarantee order of the results, so handle both
+ // cases.
+ ok(leaves.some(l => l === REPORT.objects.Array));
+ ok(leaves.some(l => l === REPORT.other["js::Shape"]));
+
+ // Test that bad indices do not yield results.
+
+ const none = CensusUtils.getReportLeaves(
+ new Set([999999999999]),
+ BREAKDOWN,
+ REPORT
+ );
+ equal(none.length, 0);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js
new file mode 100644
index 0000000000..0a46618003
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test saving a heap snapshot in the sandboxed e10s child process.
+
+function run_test() {
+ run_test_in_child("test_SaveHeapSnapshot.js");
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..155aeef009
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,184 @@
+[DEFAULT]
+tags = "devtools heapsnapshot devtools-memory"
+head = "head_heapsnapshot.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = [
+ "Census.sys.mjs",
+ "dominator-tree-worker.js",
+ "heap-snapshot-worker.js",
+ "Match.sys.mjs",
+]
+
+["test_DominatorTreeNode_LabelAndShallowSize_01.js"]
+
+["test_DominatorTreeNode_LabelAndShallowSize_02.js"]
+
+["test_DominatorTreeNode_LabelAndShallowSize_03.js"]
+
+["test_DominatorTreeNode_LabelAndShallowSize_04.js"]
+
+["test_DominatorTreeNode_attachShortestPaths_01.js"]
+
+["test_DominatorTreeNode_getNodeByIdAlongPath_01.js"]
+
+["test_DominatorTreeNode_insert_01.js"]
+
+["test_DominatorTreeNode_insert_02.js"]
+
+["test_DominatorTreeNode_insert_03.js"]
+
+["test_DominatorTreeNode_partialTraversal_01.js"]
+
+["test_DominatorTree_01.js"]
+
+["test_DominatorTree_02.js"]
+
+["test_DominatorTree_03.js"]
+
+["test_DominatorTree_04.js"]
+
+["test_DominatorTree_05.js"]
+
+["test_DominatorTree_06.js"]
+
+["test_HeapAnalyses_computeDominatorTree_01.js"]
+
+["test_HeapAnalyses_computeDominatorTree_02.js"]
+
+["test_HeapAnalyses_deleteHeapSnapshot_01.js"]
+
+["test_HeapAnalyses_deleteHeapSnapshot_02.js"]
+
+["test_HeapAnalyses_deleteHeapSnapshot_03.js"]
+
+["test_HeapAnalyses_getCensusIndividuals_01.js"]
+
+["test_HeapAnalyses_getCreationTime_01.js"]
+
+["test_HeapAnalyses_getDominatorTree_01.js"]
+
+["test_HeapAnalyses_getDominatorTree_02.js"]
+
+["test_HeapAnalyses_getImmediatelyDominated_01.js"]
+skip-if = ["tsan"] # Unreasonably slow, bug 1612707
+
+["test_HeapAnalyses_readHeapSnapshot_01.js"]
+
+["test_HeapAnalyses_takeCensusDiff_01.js"]
+
+["test_HeapAnalyses_takeCensusDiff_02.js"]
+
+["test_HeapAnalyses_takeCensus_01.js"]
+
+["test_HeapAnalyses_takeCensus_02.js"]
+
+["test_HeapAnalyses_takeCensus_03.js"]
+
+["test_HeapAnalyses_takeCensus_04.js"]
+
+["test_HeapAnalyses_takeCensus_05.js"]
+
+["test_HeapAnalyses_takeCensus_06.js"]
+
+["test_HeapAnalyses_takeCensus_07.js"]
+
+["test_HeapSnapshot_computeShortestPaths_01.js"]
+
+["test_HeapSnapshot_computeShortestPaths_02.js"]
+
+["test_HeapSnapshot_creationTime_01.js"]
+
+["test_HeapSnapshot_deepStack_01.js"]
+
+["test_HeapSnapshot_describeNode_01.js"]
+
+["test_HeapSnapshot_getObjectNodeId_01.js"]
+
+["test_HeapSnapshot_takeCensus_01.js"]
+
+["test_HeapSnapshot_takeCensus_02.js"]
+
+["test_HeapSnapshot_takeCensus_03.js"]
+
+["test_HeapSnapshot_takeCensus_04.js"]
+
+["test_HeapSnapshot_takeCensus_05.js"]
+
+["test_HeapSnapshot_takeCensus_06.js"]
+
+["test_HeapSnapshot_takeCensus_07.js"]
+
+["test_HeapSnapshot_takeCensus_08.js"]
+
+["test_HeapSnapshot_takeCensus_09.js"]
+
+["test_HeapSnapshot_takeCensus_10.js"]
+
+["test_HeapSnapshot_takeCensus_11.js"]
+
+["test_HeapSnapshot_takeCensus_12.js"]
+
+["test_ReadHeapSnapshot.js"]
+
+["test_ReadHeapSnapshot_with_allocations.js"]
+skip-if = ["os == 'linux'"] # Bug 1176173
+
+["test_ReadHeapSnapshot_with_utf8_paths.js"]
+
+["test_ReadHeapSnapshot_worker.js"]
+skip-if = ["os == 'linux'"] # Bug 1176173
+
+["test_SaveHeapSnapshot.js"]
+
+["test_census-tree-node-01.js"]
+
+["test_census-tree-node-02.js"]
+
+["test_census-tree-node-03.js"]
+
+["test_census-tree-node-04.js"]
+
+["test_census-tree-node-05.js"]
+
+["test_census-tree-node-06.js"]
+
+["test_census-tree-node-07.js"]
+
+["test_census-tree-node-08.js"]
+
+["test_census-tree-node-09.js"]
+
+["test_census-tree-node-10.js"]
+
+["test_census_diff_01.js"]
+
+["test_census_diff_02.js"]
+
+["test_census_diff_03.js"]
+
+["test_census_diff_04.js"]
+
+["test_census_diff_05.js"]
+
+["test_census_diff_06.js"]
+
+["test_census_filtering_01.js"]
+
+["test_census_filtering_02.js"]
+
+["test_census_filtering_03.js"]
+
+["test_census_filtering_04.js"]
+
+["test_census_filtering_05.js"]
+
+["test_countToBucketBreakdown_01.js"]
+
+["test_deduplicatePaths_01.js"]
+
+["test_getCensusIndividuals_01.js"]
+
+["test_getReportLeaves_01.js"]
+
+["test_saveHeapSnapshot_e10s_01.js"]
diff --git a/devtools/shared/images/command-pick-remote-touch.svg b/devtools/shared/images/command-pick-remote-touch.svg
new file mode 100644
index 0000000000..3911e3e280
--- /dev/null
+++ b/devtools/shared/images/command-pick-remote-touch.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" width="16" height="16" viewBox="0 0 16 16" fill="context-fill #0c0c0d">
+ <path d="M3 3C2.73478 3 2.48043 3.10536 2.29289 3.29289C2.10536 3.48043 2 3.73478 2 4V12C2 12.2652 2.10536 12.5196 2.29289 12.7071C2.48043 12.8946 2.73478 13 3 13H5.6C5.86522 13 6.11957 13.1054 6.30711 13.2929C6.49464 13.4804 6.6 13.7348 6.6 14C6.6 14.2652 6.49464 14.5196 6.30711 14.7071C6.11957 14.8946 5.86522 15 5.6 15H3C2.20435 15 1.44129 14.6839 0.87868 14.1213C0.31607 13.5587 0 12.7956 0 12L0 4C0 3.20435 0.31607 2.44129 0.87868 1.87868C1.44129 1.31607 2.20435 1 3 1H13C13.7956 1 14.5587 1.31607 15.1213 1.87868C15.6839 2.44129 16 3.20435 16 4V6.6C16 6.86522 15.8946 7.11957 15.7071 7.30711C15.5196 7.49464 15.2652 7.6 15 7.6C14.7348 7.6 14.4804 7.49464 14.2929 7.30711C14.1054 7.11957 14 6.86522 14 6.6V4C14 3.73478 13.8946 3.48043 13.7071 3.29289C13.5196 3.10536 13.2652 3 13 3H3Z"/>
+ <path d="M6.21903 7.75153L8.79269 11.3027L8.06998 10.9489C7.83801 10.8352 7.58498 10.8171 7.36652 10.8986C7.14806 10.9802 6.98202 11.1547 6.90491 11.3838C6.89437 11.4162 6.89437 11.4162 6.88626 11.4487L6.87815 11.4812C6.81795 11.7203 6.84669 11.9866 6.95903 12.2306C7.07137 12.4746 7.25964 12.6797 7.48882 12.8077L10.835 14.6745C11.1195 14.8333 11.4293 14.9137 11.7316 14.9073C12.0338 14.9008 12.3171 14.8077 12.5514 14.6377L14.6502 13.1166C14.8085 12.9953 14.9194 12.8195 14.9677 12.6138C15.016 12.4081 14.9993 12.1825 14.9199 11.9683L13.4528 7.98781C13.4117 7.87533 13.3547 7.77158 13.2871 7.67829L13.2892 7.67678C13.175 7.51901 13.0271 7.38977 12.8603 7.30195C12.6935 7.21412 12.5136 7.17077 12.3386 7.17621C12.1635 7.18164 11.9993 7.23568 11.8624 7.33293C11.7254 7.43019 11.6205 7.56727 11.5581 7.73053C11.3101 7.61345 11.0416 7.59498 10.808 7.67893C10.5743 7.76288 10.3933 7.94286 10.302 8.18185C10.0829 8.07845 9.84692 8.05164 9.63258 8.10578C9.41824 8.15993 9.23818 8.29183 9.12174 8.47999L7.77602 6.62313C7.60248 6.38368 7.35403 6.21348 7.08533 6.14997C6.81662 6.08646 6.54967 6.13485 6.3432 6.28448C6.13673 6.43412 6.00765 6.67274 5.98437 6.94787C5.96108 7.22299 6.04549 7.51208 6.21903 7.75153Z"/>
+</svg>
diff --git a/devtools/shared/images/command-pick.svg b/devtools/shared/images/command-pick.svg
new file mode 100644
index 0000000000..bdaf1b0f53
--- /dev/null
+++ b/devtools/shared/images/command-pick.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" width="16" height="16" viewBox="0 0 16 16" fill="context-fill #0c0c0d">
+ <path d="M3 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.6a1 1 0 1 1 0 2H3a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v2.6a1 1 0 1 1-2 0V4a1 1 0 0 0-1-1H3z"/>
+ <path d="M12.87 14.6c.3.36.85.4 1.2.1.36-.31.4-.86.1-1.22l-1.82-2.13 2.42-1a.3.3 0 0 0 .01-.56L7.43 6.43a.3.3 0 0 0-.42.35l2.13 7.89a.3.3 0 0 0 .55.07l1.35-2.28 1.83 2.14z"/>
+</svg>
diff --git a/devtools/shared/images/error-small.svg b/devtools/shared/images/error-small.svg
new file mode 100644
index 0000000000..a5ca167097
--- /dev/null
+++ b/devtools/shared/images/error-small.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 12 12" width="12" height="12">
+ <path fill="context-fill" d="M12 6A6 6 0 1 1 0 6a6 6 0 0 1 12 0zM5.75 8a.75.75 0 0 0-.75.75v.5c0 .41.34.75.75.75h.5c.41 0 .75-.34.75-.75v-.5A.75.75 0 0 0 6.25 8h-.5zM5 6c0 .54.46 1 1 1s1-.46 1-1V3.15c0-.54-.46-1-1-1s-1 .46-1 1V6z"/>
+</svg>
diff --git a/devtools/shared/images/moz.build b/devtools/shared/images/moz.build
new file mode 100644
index 0000000000..f5fe22d6e5
--- /dev/null
+++ b/devtools/shared/images/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(
+ "command-pick-remote-touch.svg",
+ "command-pick.svg",
+ "error-small.svg",
+ "resume.svg",
+ "stepOver.svg",
+)
diff --git a/devtools/shared/images/resume.svg b/devtools/shared/images/resume.svg
new file mode 100644
index 0000000000..cd92028ccf
--- /dev/null
+++ b/devtools/shared/images/resume.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 16 16" width="16" height="16">
+<path d="M5 3v10l7-5-7-5zM4 3c0-.81.92-1.31 1.58-.84l7 5.03a1 1 0 0 1 0 1.62l-7 5.03C4.92 14.31 4 13.81 4 13V3z"/>
+</svg>
diff --git a/devtools/shared/images/stepOver.svg b/devtools/shared/images/stepOver.svg
new file mode 100644
index 0000000000..c1d30c051f
--- /dev/null
+++ b/devtools/shared/images/stepOver.svg
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd">
+ <path d="M13.297 6.912C12.595 4.39 10.167 2.5 7.398 2.5A5.898 5.898 0 0 0 1.5 8.398a.5.5 0 0 0 1 0A4.898 4.898 0 0 1 7.398 3.5c2.75 0 5.102 2.236 5.102 4.898v.004L8.669 7.029a.5.5 0 0 0-.338.942l4.462 1.598a.5.5 0 0 0 .651-.34.506.506 0 0 0 .02-.043l2-5a.5.5 0 1 0-.928-.372l-1.24 3.098z"/>
+ <circle cx="7" cy="12" r="1"/>
+ </g>
+</svg>
diff --git a/devtools/shared/indentation.js b/devtools/shared/indentation.js
new file mode 100644
index 0000000000..13c80f3225
--- /dev/null
+++ b/devtools/shared/indentation.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";
+
+const EXPAND_TAB = "devtools.editor.expandtab";
+const TAB_SIZE = "devtools.editor.tabsize";
+const DETECT_INDENT = "devtools.editor.detectindentation";
+const DETECT_INDENT_MAX_LINES = 500;
+
+/**
+ * Get the number of indentation units to use to indent a "block"
+ * and a boolean indicating whether indentation must be done using tabs.
+ *
+ * @return {Object} an object of the form {indentUnit, indentWithTabs}.
+ * |indentUnit| is the number of indentation units to use
+ * to indent a "block".
+ * |indentWithTabs| is a boolean which is true if indentation
+ * should be done using tabs.
+ */
+function getTabPrefs() {
+ const indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
+ const indentUnit = Services.prefs.getIntPref(TAB_SIZE, 2);
+ return { indentUnit, indentWithTabs };
+}
+
+/**
+ * Get the indentation to use in an editor, or return false if the user has
+ * asked for the indentation to be guessed from some text.
+ *
+ * @return {false | Object}
+ * Returns false if the "detect indentation" pref is set.
+ * If the pref is not set, returns an object of the same
+ * form as returned by getTabPrefs.
+ */
+function getIndentationFromPrefs() {
+ const shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT);
+ if (shouldDetect) {
+ return false;
+ }
+
+ return getTabPrefs();
+}
+
+/**
+ * Given a function that can iterate over some text, compute the indentation to
+ * use. This consults various prefs to arrive at a decision.
+ *
+ * @param {Function} iterFunc A function of three arguments:
+ * (start, end, callback); where |start| and |end| describe
+ * the range of text lines to examine, and |callback| is a function
+ * to be called with the text of each line.
+ *
+ * @return {Object} an object of the form {indentUnit, indentWithTabs}.
+ * |indentUnit| is the number of indentation units to use
+ * to indent a "block".
+ * |indentWithTabs| is a boolean which is true if indentation
+ * should be done using tabs.
+ */
+function getIndentationFromIteration(iterFunc) {
+ let indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
+ let indentUnit = Services.prefs.getIntPref(TAB_SIZE);
+ const shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT);
+
+ if (shouldDetect) {
+ const indent = detectIndentation(iterFunc);
+ if (indent != null) {
+ indentWithTabs = indent.tabs;
+ indentUnit = indent.spaces ? indent.spaces : indentUnit;
+ }
+ }
+
+ return { indentUnit, indentWithTabs };
+}
+
+/**
+ * A wrapper for @see getIndentationFromIteration which computes the
+ * indentation of a given string.
+ *
+ * @param {String} string the input text
+ * @return {Object} an object of the same form as returned by
+ * getIndentationFromIteration
+ */
+function getIndentationFromString(string) {
+ const iteratorFn = function (start, end, callback) {
+ const split = string.split(/\r\n|\r|\n|\f/);
+ split.slice(start, end).forEach(callback);
+ };
+ return getIndentationFromIteration(iteratorFn);
+}
+
+/**
+ * Detect the indentation used in an editor. Returns an object
+ * with 'tabs' - whether this is tab-indented and 'spaces' - the
+ * width of one indent in spaces. Or `null` if it's inconclusive.
+ */
+function detectIndentation(textIteratorFn) {
+ // # spaces indent -> # lines with that indent
+ const spaces = {};
+ // indentation width of the last line we saw
+ let last = 0;
+ // # of lines that start with a tab
+ let tabs = 0;
+ // # of indented lines (non-zero indent)
+ let total = 0;
+
+ textIteratorFn(0, DETECT_INDENT_MAX_LINES, text => {
+ if (text.startsWith("\t")) {
+ tabs++;
+ total++;
+ return;
+ }
+ let width = 0;
+ while (text[width] === " ") {
+ width++;
+ }
+ // don't count lines that are all spaces
+ if (width == text.length) {
+ last = 0;
+ return;
+ }
+ if (width > 1) {
+ total++;
+ }
+
+ // see how much this line is offset from the line above it
+ const indent = Math.abs(width - last);
+ if (indent > 1 && indent <= 8) {
+ spaces[indent] = (spaces[indent] || 0) + 1;
+ }
+ last = width;
+ });
+
+ // this file is not indented at all
+ if (total == 0) {
+ return null;
+ }
+
+ // mark as tabs if they start more than half the lines
+ if (tabs >= total / 2) {
+ return { tabs: true };
+ }
+
+ // find most frequent non-zero width difference between adjacent lines
+ let freqIndent = null,
+ max = 1;
+ for (let width in spaces) {
+ width = parseInt(width, 10);
+ const tally = spaces[width];
+ if (tally > max) {
+ max = tally;
+ freqIndent = width;
+ }
+ }
+ if (!freqIndent) {
+ return null;
+ }
+
+ return { tabs: false, spaces: freqIndent };
+}
+
+exports.EXPAND_TAB = EXPAND_TAB;
+exports.TAB_SIZE = TAB_SIZE;
+exports.DETECT_INDENT = DETECT_INDENT;
+exports.getTabPrefs = getTabPrefs;
+exports.getIndentationFromPrefs = getIndentationFromPrefs;
+exports.getIndentationFromIteration = getIndentationFromIteration;
+exports.getIndentationFromString = getIndentationFromString;
diff --git a/devtools/shared/indexed-db.js b/devtools/shared/indexed-db.js
new file mode 100644
index 0000000000..25cb858142
--- /dev/null
+++ b/devtools/shared/indexed-db.js
@@ -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/. */
+
+"use strict";
+
+/**
+ * This indexedDB helper is a simplified version of sdk/indexed-db. It creates a DB with
+ * a principal dedicated to DevTools.
+ */
+
+const PSEUDOURI = "indexeddb://fx-devtools";
+const principaluri = Services.io.newURI(PSEUDOURI);
+const principal = Services.scriptSecurityManager.createContentPrincipal(
+ principaluri,
+ {}
+);
+
+// indexedDB is only exposed to document globals.
+// We are retrieving an instance from a Sandbox, which has to be loaded
+// from the system principal in order to avoid having wrappers around
+// all indexed DB objects.
+const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+const sandbox = Cu.Sandbox(systemPrincipal, {
+ wantGlobalProperties: ["indexedDB"],
+});
+const { indexedDB } = sandbox;
+
+module.exports = Object.freeze({
+ /**
+ * Only the standard version of indexedDB.open is supported.
+ */
+ open(name, version) {
+ const options = {};
+ if (typeof version === "number") {
+ options.version = version;
+ }
+ return indexedDB.openForPrincipal(principal, name, options);
+ },
+
+ /**
+ * Only the standard version of indexedDB.deleteDatabase is supported.
+ */
+ deleteDatabase(name) {
+ return indexedDB.deleteForPrincipal(principal, name);
+ },
+
+ cmp: indexedDB.cmp.bind(indexedDB),
+});
diff --git a/devtools/shared/inspector/css-logic.js b/devtools/shared/inspector/css-logic.js
new file mode 100644
index 0000000000..a716827f17
--- /dev/null
+++ b/devtools/shared/inspector/css-logic.js
@@ -0,0 +1,844 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 MAX_DATA_URL_LENGTH = 40;
+/**
+ * Provide access to the style information in a page.
+ * CssLogic uses the standard DOM API, and the Gecko InspectorUtils API to
+ * access styling information in the page, and present this to the user in a way
+ * that helps them understand:
+ * - why their expectations may not have been fulfilled
+ * - how browsers process CSS
+ * @constructor
+ */
+
+loader.lazyRequireGetter(
+ this,
+ "getCSSLexer",
+ "resource://devtools/shared/css/lexer.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getTabPrefs",
+ "resource://devtools/shared/indentation.js",
+ true
+);
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const styleInspectorL10N = new LocalizationHelper(
+ "devtools/shared/locales/styleinspector.properties"
+);
+
+/**
+ * Special values for filter, in addition to an href these values can be used
+ */
+exports.FILTER = {
+ // show properties for all user style sheets.
+ USER: "user",
+ // USER, plus user-agent (i.e. browser) style sheets
+ UA: "ua",
+};
+
+/**
+ * Each rule has a status, the bigger the number, the better placed it is to
+ * provide styling information.
+ *
+ * These statuses are localized inside the styleinspector.properties
+ * string bundle.
+ * @see csshtmltree.js RuleView._cacheStatusNames()
+ */
+exports.STATUS = {
+ BEST: 3,
+ MATCHED: 2,
+ PARENT_MATCH: 1,
+ UNMATCHED: 0,
+ UNKNOWN: -1,
+};
+
+/**
+ * Mapping of CSS at-Rule className to CSSRule type name.
+ */
+exports.CSSAtRuleClassNameType = {
+ CSSContainerRule: "container",
+ CSSCounterStyleRule: "counter-style",
+ CSSDocumentRule: "document",
+ CSSFontFaceRule: "font-face",
+ CSSFontFeatureValuesRule: "font-feature-values",
+ CSSImportRule: "import",
+ CSSKeyframeRule: "keyframe",
+ CSSKeyframesRule: "keyframes",
+ CSSLayerBlockRule: "layer",
+ CSSMediaRule: "media",
+ CSSNamespaceRule: "namespace",
+ CSSPageRule: "page",
+ CSSSupportsRule: "supports",
+};
+
+/**
+ * Get Rule type as human-readable string (ex: "@media", "@container", …)
+ *
+ * @param {CSSRule} cssRule
+ * @returns {String}
+ */
+exports.getCSSAtRuleTypeName = function (cssRule) {
+ const ruleClassName = ChromeUtils.getClassName(cssRule);
+ const atRuleTypeName = exports.CSSAtRuleClassNameType[ruleClassName];
+ if (atRuleTypeName) {
+ return "@" + atRuleTypeName;
+ }
+
+ return "";
+};
+
+/**
+ * Lookup a l10n string in the shared styleinspector string bundle.
+ *
+ * @param {String} name
+ * The key to lookup.
+ * @returns {String} A localized version of the given key.
+ */
+exports.l10n = name => styleInspectorL10N.getStr(name);
+exports.l10nFormatStr = (name, ...args) =>
+ styleInspectorL10N.getFormatStr(name, ...args);
+
+/**
+ * Is the given property sheet an author stylesheet?
+ *
+ * @param {CSSStyleSheet} sheet a stylesheet
+ * @return {boolean} true if the given stylesheet is an author stylesheet,
+ * false otherwise.
+ */
+exports.isAuthorStylesheet = function (sheet) {
+ return sheet.parsingMode === "author";
+};
+
+/**
+ * Is the given property sheet a user stylesheet?
+ *
+ * @param {CSSStyleSheet} sheet a stylesheet
+ * @return {boolean} true if the given stylesheet is a user stylesheet,
+ * false otherwise.
+ */
+exports.isUserStylesheet = function (sheet) {
+ return sheet.parsingMode === "user";
+};
+
+/**
+ * Is the given property sheet a agent stylesheet?
+ *
+ * @param {CSSStyleSheet} sheet a stylesheet
+ * @return {boolean} true if the given stylesheet is a agent stylesheet,
+ * false otherwise.
+ */
+exports.isAgentStylesheet = function (sheet) {
+ return sheet.parsingMode === "agent";
+};
+
+/**
+ * Return a shortened version of a style sheet's source.
+ *
+ * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
+ */
+exports.shortSource = function (sheet) {
+ if (!sheet) {
+ return exports.l10n("rule.sourceInline");
+ }
+
+ if (!sheet.href) {
+ return exports.l10n(
+ sheet.constructed ? "rule.sourceConstructed" : "rule.sourceInline"
+ );
+ }
+
+ let name = sheet.href;
+
+ // If the sheet is a data URL, return a trimmed version of it.
+ const dataUrl = sheet.href.trim().match(/^data:.*?,((?:.|\r|\n)*)$/);
+ if (dataUrl) {
+ name =
+ dataUrl[1].length > MAX_DATA_URL_LENGTH
+ ? `${dataUrl[1].substr(0, MAX_DATA_URL_LENGTH - 1)}…`
+ : dataUrl[1];
+ } else {
+ // We try, in turn, the filename, filePath, query string, whole thing
+ let url = {};
+ try {
+ url = new URL(sheet.href);
+ } catch (ex) {
+ // Some UA-provided stylesheets are not valid URLs.
+ }
+
+ if (url.pathname) {
+ const index = url.pathname.lastIndexOf("/");
+ if (index !== -1 && index < url.pathname.length) {
+ name = url.pathname.slice(index + 1);
+ } else {
+ name = url.pathname;
+ }
+ } else if (url.query) {
+ name = url.query;
+ }
+ }
+
+ try {
+ name = decodeURIComponent(name);
+ } catch (e) {
+ // This may still fail if the URL contains invalid % numbers (for ex)
+ }
+
+ return name;
+};
+
+/**
+ * Return the style sheet's source, handling element, inline and constructed stylesheets.
+ *
+ * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
+ */
+exports.longSource = function (sheet) {
+ if (!sheet) {
+ return exports.l10n("rule.sourceInline");
+ }
+
+ if (!sheet.href) {
+ return exports.l10n(
+ sheet.constructed ? "rule.sourceConstructed" : "rule.sourceInline"
+ );
+ }
+
+ return sheet.href;
+};
+
+const TAB_CHARS = "\t";
+const SPACE_CHARS = " ";
+
+function getLineCountInComments(text) {
+ let count = 0;
+
+ for (const comment of text.match(/\/\*(?:.|\n)*?\*\//gm) || []) {
+ count += comment.split("\n").length + 1;
+ }
+
+ return count;
+}
+
+/**
+ * Prettify minified CSS text.
+ * This prettifies CSS code where there is no indentation in usual places while
+ * keeping original indentation as-is elsewhere.
+ *
+ * Returns an object with the resulting prettified source and a list of mappings of
+ * token positions between the original and the prettified source. Each single mapping
+ * is an object that looks like this:
+ *
+ * {
+ * original: {line: {Number}, column: {Number}},
+ * generated: {line: {Number}, column: {Number}},
+ * }
+ *
+ * @param {String} text
+ * The CSS source to prettify.
+ * @param {Number} ruleCount
+ * The number of CSS rules expected in the CSS source.
+ * Set to null to force the text to be pretty-printed.
+ *
+ * @return {Object}
+ * Object with the prettified source and source mappings.
+ * {
+ * result: {String} // Prettified source
+ * mappings: {Array} // List of objects with mappings for lines and columns
+ * // between the original source and prettified source
+ * }
+ */
+// eslint-disable-next-line complexity
+function prettifyCSS(text, ruleCount) {
+ if (prettifyCSS.LINE_SEPARATOR == null) {
+ const os = Services.appinfo.OS;
+ prettifyCSS.LINE_SEPARATOR = os === "WINNT" ? "\r\n" : "\n";
+ }
+
+ // Stylesheets may start and end with HTML comment tags (possibly with whitespaces
+ // before and after). Remove those first. Don't do anything there aren't any.
+ const trimmed = text.trim();
+ if (trimmed.startsWith("<!--")) {
+ text = trimmed.replace(/^<!--/, "").replace(/-->$/, "").trim();
+ }
+
+ const originalText = text;
+ text = text.trim();
+
+ // don't attempt to prettify if there's more than one line per rule, excluding comments.
+ const lineCount = text.split("\n").length - 1 - getLineCountInComments(text);
+ if (ruleCount !== null && lineCount >= ruleCount) {
+ return { result: originalText, mappings: [] };
+ }
+
+ // We reformat the text using a simple state machine. The
+ // reformatting preserves most of the input text, changing only
+ // whitespace. The rules are:
+ //
+ // * After a "{" or ";" symbol, ensure there is a newline and
+ // indentation before the next non-comment, non-whitespace token.
+ // * Additionally after a "{" symbol, increase the indentation.
+ // * A "}" symbol ensures there is a preceding newline, and
+ // decreases the indentation level.
+ // * Ensure there is whitespace before a "{".
+ //
+ // This approach can be confused sometimes, but should do ok on a
+ // minified file.
+ let indent = "";
+ let indentLevel = 0;
+ const tokens = getCSSLexer(text);
+ // List of mappings of token positions from original source to prettified source.
+ const mappings = [];
+ // Line and column offsets used to shift the token positions after prettyfication.
+ let lineOffset = 0;
+ let columnOffset = 0;
+ let indentOffset = 0;
+ let result = "";
+ let pushbackToken = undefined;
+
+ // A helper function that reads tokens, looking for the next
+ // non-comment, non-whitespace token. Comment and whitespace tokens
+ // are appended to |result|. If this encounters EOF, it returns
+ // null. Otherwise it returns the last whitespace token that was
+ // seen. This function also updates |pushbackToken|.
+ const readUntilSignificantToken = () => {
+ while (true) {
+ const token = tokens.nextToken();
+ if (!token || token.tokenType !== "whitespace") {
+ pushbackToken = token;
+ return token;
+ }
+ // Saw whitespace. Before committing to it, check the next
+ // token.
+ const nextToken = tokens.nextToken();
+ if (!nextToken || nextToken.tokenType !== "comment") {
+ pushbackToken = nextToken;
+ return token;
+ }
+ // Saw whitespace + comment. Update the result and continue.
+ result = result + text.substring(token.startOffset, nextToken.endOffset);
+ }
+ };
+
+ // State variables for readUntilNewlineNeeded.
+ //
+ // Starting index of the accumulated tokens.
+ let startIndex;
+ // Ending index of the accumulated tokens.
+ let endIndex;
+ // True if any non-whitespace token was seen.
+ let anyNonWS;
+ // True if the terminating token is "}".
+ let isCloseBrace;
+ // True if the token just before the terminating token was
+ // whitespace.
+ let lastWasWS;
+ // True if the current token is inside a CSS selector.
+ let isInSelector = true;
+ // True if the current token is inside an at-rule definition.
+ let isInAtRuleDefinition = false;
+
+ // A helper function that reads tokens until there is a reason to
+ // insert a newline. This updates the state variables as needed.
+ // If this encounters EOF, it returns null. Otherwise it returns
+ // the final token read. Note that if the returned token is "{",
+ // then it will not be included in the computed start/end token
+ // range. This is used to handle whitespace insertion before a "{".
+ const readUntilNewlineNeeded = () => {
+ let token;
+ while (true) {
+ if (pushbackToken) {
+ token = pushbackToken;
+ pushbackToken = undefined;
+ } else {
+ token = tokens.nextToken();
+ }
+ if (!token) {
+ endIndex = text.length;
+ break;
+ }
+
+ const line = tokens.lineNumber;
+ const column = tokens.columnNumber;
+ mappings.push({
+ original: {
+ line,
+ column,
+ },
+ generated: {
+ line: lineOffset + line,
+ column: columnOffset,
+ },
+ });
+ // Shift the column offset for the next token by the current token's length.
+ columnOffset += token.endOffset - token.startOffset;
+
+ if (token.tokenType === "at") {
+ isInAtRuleDefinition = true;
+ }
+
+ // A "}" symbol must be inserted later, to deal with indentation
+ // and newline.
+ if (token.tokenType === "symbol" && token.text === "}") {
+ isInSelector = true;
+ isCloseBrace = true;
+ break;
+ } else if (token.tokenType === "symbol" && token.text === "{") {
+ if (isInAtRuleDefinition) {
+ isInAtRuleDefinition = false;
+ } else {
+ isInSelector = false;
+ }
+ break;
+ }
+
+ if (token.tokenType !== "whitespace") {
+ anyNonWS = true;
+ }
+
+ if (startIndex === undefined) {
+ startIndex = token.startOffset;
+ }
+ endIndex = token.endOffset;
+
+ if (token.tokenType === "symbol" && token.text === ";") {
+ break;
+ }
+
+ if (
+ token.tokenType === "symbol" &&
+ token.text === "," &&
+ isInSelector &&
+ !isInAtRuleDefinition
+ ) {
+ break;
+ }
+
+ lastWasWS = token.tokenType === "whitespace";
+ }
+ return token;
+ };
+
+ // Get preference of the user regarding what to use for indentation,
+ // spaces or tabs.
+ const tabPrefs = getTabPrefs();
+ const baseIndentString = tabPrefs.indentWithTabs
+ ? TAB_CHARS
+ : SPACE_CHARS.repeat(tabPrefs.indentUnit);
+
+ while (true) {
+ // Set the initial state.
+ startIndex = undefined;
+ endIndex = undefined;
+ anyNonWS = false;
+ isCloseBrace = false;
+ lastWasWS = false;
+
+ // Read tokens until we see a reason to insert a newline.
+ let token = readUntilNewlineNeeded();
+
+ // Append any saved up text to the result, applying indentation.
+ if (startIndex !== undefined) {
+ if (isCloseBrace && !anyNonWS) {
+ // If we saw only whitespace followed by a "}", then we don't
+ // need anything here.
+ } else {
+ result = result + indent + text.substring(startIndex, endIndex);
+ if (isCloseBrace) {
+ result += prettifyCSS.LINE_SEPARATOR;
+ lineOffset = lineOffset + 1;
+ }
+ }
+ }
+
+ if (isCloseBrace) {
+ // Even if the stylesheet contains extra closing braces, the indent level should
+ // remain > 0.
+ indentLevel = Math.max(0, indentLevel - 1);
+ indent = baseIndentString.repeat(indentLevel);
+
+ // FIXME: This is incorrect and should be fixed in Bug 1839297
+ if (tabPrefs.indentWithTabs) {
+ indentOffset = 4 * indentLevel;
+ } else {
+ indentOffset = 1 * indentLevel;
+ }
+ result = result + indent + "}";
+ }
+
+ if (!token) {
+ break;
+ }
+
+ if (token.tokenType === "symbol" && token.text === "{") {
+ if (!lastWasWS) {
+ result += " ";
+ columnOffset++;
+ }
+ result += "{";
+ indentLevel++;
+ indent = baseIndentString.repeat(indentLevel);
+ indentOffset = indent.length;
+
+ // FIXME: This is incorrect and should be fixed in Bug 1839297
+ if (tabPrefs.indentWithTabs) {
+ indentOffset = 4 * indentLevel;
+ } else {
+ indentOffset = 1 * indentLevel;
+ }
+ }
+
+ // Now it is time to insert a newline. However first we want to
+ // deal with any trailing comments.
+ token = readUntilSignificantToken();
+
+ // "Early" bail-out if the text does not appear to be minified.
+ // Here we ignore the case where whitespace appears at the end of
+ // the text.
+ if (
+ ruleCount !== null &&
+ pushbackToken &&
+ token &&
+ token.tokenType === "whitespace" &&
+ /\n/g.test(text.substring(token.startOffset, token.endOffset))
+ ) {
+ return { result: originalText, mappings: [] };
+ }
+
+ // Finally time for that newline.
+ result = result + prettifyCSS.LINE_SEPARATOR;
+
+ // Update line and column offsets for the new line.
+ lineOffset = lineOffset + 1;
+ columnOffset = 0 + indentOffset;
+
+ // Maybe we hit EOF.
+ if (!pushbackToken) {
+ break;
+ }
+ }
+
+ return { result, mappings };
+}
+
+exports.prettifyCSS = prettifyCSS;
+
+/**
+ * Given a node, check to see if it is a ::marker, ::before, or ::after element.
+ * If so, return the node that is accessible from within the document
+ * (the parent of the anonymous node), along with which pseudo element
+ * it was. Otherwise, return the node itself.
+ *
+ * @returns {Object}
+ * - {DOMNode} node The non-anonymous node
+ * - {string} pseudo One of '::marker', '::before', '::after', or null.
+ */
+function getBindingElementAndPseudo(node) {
+ let bindingElement = node;
+ let pseudo = null;
+ if (node.nodeName == "_moz_generated_content_marker") {
+ bindingElement = node.parentNode;
+ pseudo = "::marker";
+ } else if (node.nodeName == "_moz_generated_content_before") {
+ bindingElement = node.parentNode;
+ pseudo = "::before";
+ } else if (node.nodeName == "_moz_generated_content_after") {
+ bindingElement = node.parentNode;
+ pseudo = "::after";
+ }
+ return {
+ bindingElement,
+ pseudo,
+ };
+}
+exports.getBindingElementAndPseudo = getBindingElementAndPseudo;
+
+/**
+ * Returns css style rules for a given a node.
+ * This function can handle ::before or ::after pseudo element as well as
+ * normal element.
+ */
+function getCSSStyleRules(node) {
+ const { bindingElement, pseudo } = getBindingElementAndPseudo(node);
+ const rules = InspectorUtils.getCSSStyleRules(bindingElement, pseudo);
+ return rules;
+}
+exports.getCSSStyleRules = getCSSStyleRules;
+
+/**
+ * Returns true if the given node has visited state.
+ */
+function hasVisitedState(node) {
+ if (!node) {
+ return false;
+ }
+
+ // ElementState::VISITED
+ const ELEMENT_STATE_VISITED = 1 << 18;
+
+ return (
+ !!(InspectorUtils.getContentState(node) & ELEMENT_STATE_VISITED) ||
+ InspectorUtils.hasPseudoClassLock(node, ":visited")
+ );
+}
+exports.hasVisitedState = hasVisitedState;
+
+/**
+ * Find the position of [element] in [nodeList].
+ * @returns an index of the match, or -1 if there is no match
+ */
+function positionInNodeList(element, nodeList) {
+ for (let i = 0; i < nodeList.length; i++) {
+ if (element === nodeList[i]) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * For a provided node, find the appropriate container/node couple so that
+ * container.contains(node) and a CSS selector can be created from the
+ * container to the node.
+ */
+function findNodeAndContainer(node) {
+ const shadowRoot = node.containingShadowRoot;
+ while (node?.isNativeAnonymous) {
+ node = node.parentNode;
+ }
+
+ if (shadowRoot) {
+ // If the node is under a shadow root, the shadowRoot contains the node and
+ // we can find the node via shadowRoot.querySelector(path).
+ return {
+ containingDocOrShadow: shadowRoot,
+ node,
+ };
+ }
+
+ // Otherwise, get the root binding parent to get a non anonymous element that
+ // will be accessible from the ownerDocument.
+ return {
+ containingDocOrShadow: node.ownerDocument,
+ node,
+ };
+}
+
+/**
+ * Find a unique CSS selector for a given element
+ * @returns a string such that:
+ * - ele.containingDocOrShadow.querySelector(reply) === ele
+ * - ele.containingDocOrShadow.querySelectorAll(reply).length === 1
+ */
+const findCssSelector = function (ele) {
+ const { node, containingDocOrShadow } = findNodeAndContainer(ele);
+ ele = node;
+
+ if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
+ // findCssSelector received element not inside container.
+ return "";
+ }
+
+ const cssEscape = ele.ownerGlobal.CSS.escape;
+
+ // document.querySelectorAll("#id") returns multiple if elements share an ID
+ if (
+ ele.id &&
+ containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1
+ ) {
+ return "#" + cssEscape(ele.id);
+ }
+
+ // Inherently unique by tag name
+ const tagName = ele.localName;
+ if (tagName === "html") {
+ return "html";
+ }
+ if (tagName === "head") {
+ return "head";
+ }
+ if (tagName === "body") {
+ return "body";
+ }
+
+ // We might be able to find a unique class name
+ let selector, index, matches;
+ for (let i = 0; i < ele.classList.length; i++) {
+ // Is this className unique by itself?
+ selector = "." + cssEscape(ele.classList.item(i));
+ matches = containingDocOrShadow.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ // Maybe it's unique with a tag name?
+ selector = cssEscape(tagName) + selector;
+ matches = containingDocOrShadow.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ // Maybe it's unique using a tag name and nth-child
+ index = positionInNodeList(ele, ele.parentNode.children) + 1;
+ selector = selector + ":nth-child(" + index + ")";
+ matches = containingDocOrShadow.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ }
+
+ // Not unique enough yet.
+ index = positionInNodeList(ele, ele.parentNode.children) + 1;
+ selector = cssEscape(tagName) + ":nth-child(" + index + ")";
+ if (ele.parentNode !== containingDocOrShadow) {
+ selector = findCssSelector(ele.parentNode) + " > " + selector;
+ }
+ return selector;
+};
+exports.findCssSelector = findCssSelector;
+
+/**
+ * Get the full CSS path for a given element.
+ *
+ * @returns a string that can be used as a CSS selector for the element. It might not
+ * match the element uniquely. It does however, represent the full path from the root
+ * node to the element.
+ */
+function getCssPath(ele) {
+ const { node, containingDocOrShadow } = findNodeAndContainer(ele);
+ ele = node;
+ if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
+ // getCssPath received element not inside container.
+ return "";
+ }
+
+ const nodeGlobal = ele.ownerGlobal.Node;
+
+ const getElementSelector = element => {
+ if (!element.localName) {
+ return "";
+ }
+
+ let label =
+ element.nodeName == element.nodeName.toUpperCase()
+ ? element.localName.toLowerCase()
+ : element.localName;
+
+ if (element.id) {
+ label += "#" + element.id;
+ }
+
+ if (element.classList) {
+ for (const cl of element.classList) {
+ label += "." + cl;
+ }
+ }
+
+ return label;
+ };
+
+ const paths = [];
+
+ while (ele) {
+ if (!ele || ele.nodeType !== nodeGlobal.ELEMENT_NODE) {
+ break;
+ }
+
+ paths.splice(0, 0, getElementSelector(ele));
+ ele = ele.parentNode;
+ }
+
+ return paths.length ? paths.join(" ") : "";
+}
+exports.getCssPath = getCssPath;
+
+/**
+ * Get the xpath for a given element.
+ *
+ * @param {DomNode} ele
+ * @returns a string that can be used as an XPath to find the element uniquely.
+ */
+function getXPath(ele) {
+ const { node, containingDocOrShadow } = findNodeAndContainer(ele);
+ ele = node;
+ if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
+ // getXPath received element not inside container.
+ return "";
+ }
+
+ // Create a short XPath for elements with IDs.
+ if (ele.id) {
+ return `//*[@id="${ele.id}"]`;
+ }
+
+ // Otherwise walk the DOM up and create a part for each ancestor.
+ const parts = [];
+
+ const nodeGlobal = ele.ownerGlobal.Node;
+ // Use nodeName (instead of localName) so namespace prefix is included (if any).
+ while (ele && ele.nodeType === nodeGlobal.ELEMENT_NODE) {
+ let nbOfPreviousSiblings = 0;
+ let hasNextSiblings = false;
+
+ // Count how many previous same-name siblings the element has.
+ let sibling = ele.previousSibling;
+ while (sibling) {
+ // Ignore document type declaration.
+ if (
+ sibling.nodeType !== nodeGlobal.DOCUMENT_TYPE_NODE &&
+ sibling.nodeName == ele.nodeName
+ ) {
+ nbOfPreviousSiblings++;
+ }
+
+ sibling = sibling.previousSibling;
+ }
+
+ // Check if the element has at least 1 next same-name sibling.
+ sibling = ele.nextSibling;
+ while (sibling) {
+ if (sibling.nodeName == ele.nodeName) {
+ hasNextSiblings = true;
+ break;
+ }
+ sibling = sibling.nextSibling;
+ }
+
+ const prefix = ele.prefix ? ele.prefix + ":" : "";
+ const nth =
+ nbOfPreviousSiblings || hasNextSiblings
+ ? `[${nbOfPreviousSiblings + 1}]`
+ : "";
+
+ parts.push(prefix + ele.localName + nth);
+
+ ele = ele.parentNode;
+ }
+
+ return parts.length ? "/" + parts.reverse().join("/") : "";
+}
+exports.getXPath = getXPath;
+
+/**
+ * Build up a regular expression that matches a CSS variable token. This is an
+ * ident token that starts with two dashes "--".
+ *
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ */
+var NON_ASCII = "[^\\x00-\\x7F]";
+var ESCAPE = "\\\\[^\n\r]";
+var VALID_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
+var IS_VARIABLE_TOKEN = new RegExp(`^--(${VALID_CHAR})*$`, "i");
+
+/**
+ * Check that this is a CSS variable.
+ *
+ * @param {String} input
+ * @return {Boolean}
+ */
+function isCssVariable(input) {
+ return !!input.match(IS_VARIABLE_TOKEN);
+}
+exports.isCssVariable = isCssVariable;
diff --git a/devtools/shared/inspector/moz.build b/devtools/shared/inspector/moz.build
new file mode 100644
index 0000000000..21a01fae9b
--- /dev/null
+++ b/devtools/shared/inspector/moz.build
@@ -0,0 +1,7 @@
+# -*- 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("css-logic.js", "utils.js")
diff --git a/devtools/shared/inspector/utils.js b/devtools/shared/inspector/utils.js
new file mode 100644
index 0000000000..3bd6b8edff
--- /dev/null
+++ b/devtools/shared/inspector/utils.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";
+
+/**
+ * Truncate the string and add ellipsis to the middle of the string.
+ */
+function truncateString(str, maxLength) {
+ if (!str || str.length <= maxLength) {
+ return str;
+ }
+
+ return (
+ str.substring(0, Math.ceil(maxLength / 2)) +
+ "…" +
+ str.substring(str.length - Math.floor(maxLength / 2))
+ );
+}
+
+exports.truncateString = truncateString;
diff --git a/devtools/shared/jar.mn b/devtools/shared/jar.mn
new file mode 100644
index 0000000000..10357349df
--- /dev/null
+++ b/devtools/shared/jar.mn
@@ -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/.
+
+devtools.jar:
+% content devtools %content/
+ content/shared/webextension-fallback.html (webextension-fallback.html)
+% resource devtools %modules/devtools/
+% resource devtools-highlighter-styles resource://devtools/server/actors/highlighters/css/ contentaccessible=yes
+% resource devtools-shared-images resource://devtools/shared/images/ contentaccessible=true
+% content devtools-jsonview-styles %modules/devtools/client/jsonview/css/ contentaccessible=yes
+# The typical approach would be to list all the resource files in this manifest
+# for installation. Instead of doing this, use the DevToolsModules syntax via
+# moz.build files to do the installation so that we can enforce correct paths
+# based on source tree location.
diff --git a/devtools/shared/jsbeautify/UPGRADING.md b/devtools/shared/jsbeautify/UPGRADING.md
new file mode 100644
index 0000000000..8d0ed4a93b
--- /dev/null
+++ b/devtools/shared/jsbeautify/UPGRADING.md
@@ -0,0 +1,36 @@
+# UPGRADING
+
+1. `git clone https://github.com/beautify-web/js-beautify.git`
+
+2. `cd js-beautify`
+
+3. Retrieve the latest tag with
+
+```
+git describe --tags `git rev-list --tags --max-count=1`
+```
+
+4. Move to the latest tag `git checkout ${latestTag}` (`${latestTag}` should be replaced with
+ what was printed at step 3).
+
+5. `npm install`
+
+6. `npx webpack`
+
+7. Copy `js/lib/beautify.js` to `devtools/shared/jsbeautify/src/beautify-js.js`
+
+8. Copy `js/lib/beautify-html.js` to `devtools/shared/jsbeautify/src/beautify-html.js`
+
+9. Replace the following line at the bottom of the file:
+
+```
+var js_beautify = require('./beautify.js');
+```
+
+with (changing `beautify.js` into `beautify-js.js`):
+
+```
+var js_beautify = require('./beautify-js.js');
+```
+
+10. Copy `js/lib/beautify-css.js` to `devtools/shared/jsbeautify/src/beautify-css.js`
diff --git a/devtools/shared/jsbeautify/beautify.js b/devtools/shared/jsbeautify/beautify.js
new file mode 100644
index 0000000000..5ad96f512c
--- /dev/null
+++ b/devtools/shared/jsbeautify/beautify.js
@@ -0,0 +1,9 @@
+const { css_beautify } = require("resource://devtools/shared/jsbeautify/src/beautify-css.js");
+const {
+ html_beautify,
+} = require("resource://devtools/shared/jsbeautify/src/beautify-html.js");
+const { js_beautify } = require("resource://devtools/shared/jsbeautify/src/beautify-js.js");
+
+exports.css = css_beautify;
+exports.html = html_beautify;
+exports.js = js_beautify;
diff --git a/devtools/shared/jsbeautify/moz.build b/devtools/shared/jsbeautify/moz.build
new file mode 100644
index 0000000000..aa759be322
--- /dev/null
+++ b/devtools/shared/jsbeautify/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/.
+
+DIRS += [
+ 'src',
+]
+
+DevToolsModules(
+ 'beautify.js',
+)
diff --git a/devtools/shared/jsbeautify/src/beautify-css.js b/devtools/shared/jsbeautify/src/beautify-css.js
new file mode 100644
index 0000000000..42cf64a856
--- /dev/null
+++ b/devtools/shared/jsbeautify/src/beautify-css.js
@@ -0,0 +1,1683 @@
+/* AUTO-GENERATED. DO NOT MODIFY. */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+ CSS Beautifier
+---------------
+
+ Written by Harutyun Amirjanyan, (amirjanyan@gmail.com)
+
+ Based on code initially developed by: Einar Lielmanis, <einar@beautifier.io>
+ https://beautifier.io/
+
+ Usage:
+ css_beautify(source_text);
+ css_beautify(source_text, options);
+
+ The options are (default in brackets):
+ indent_size (4) — indentation size,
+ indent_char (space) — character to indent with,
+ selector_separator_newline (true) - separate selectors with newline or
+ not (e.g. "a,\nbr" or "a, br")
+ end_with_newline (false) - end with a newline
+ newline_between_rules (true) - add a new line after every css rule
+ space_around_selector_separator (false) - ensure space around selector separators:
+ '>', '+', '~' (e.g. "a>b" -> "a > b")
+ e.g
+
+ css_beautify(css_source_text, {
+ 'indent_size': 1,
+ 'indent_char': '\t',
+ 'selector_separator': ' ',
+ 'end_with_newline': false,
+ 'newline_between_rules': true,
+ 'space_around_selector_separator': true
+ });
+*/
+
+// http://www.w3.org/TR/CSS21/syndata.html#tokenization
+// http://www.w3.org/TR/css3-syntax/
+
+(function() {
+
+/* GENERATED_BUILD_OUTPUT */
+var legacy_beautify_css =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 15);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */,
+/* 1 */,
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function OutputLine(parent) {
+ this.__parent = parent;
+ this.__character_count = 0;
+ // use indent_count as a marker for this.__lines that have preserved indentation
+ this.__indent_count = -1;
+ this.__alignment_count = 0;
+ this.__wrap_point_index = 0;
+ this.__wrap_point_character_count = 0;
+ this.__wrap_point_indent_count = -1;
+ this.__wrap_point_alignment_count = 0;
+
+ this.__items = [];
+}
+
+OutputLine.prototype.clone_empty = function() {
+ var line = new OutputLine(this.__parent);
+ line.set_indent(this.__indent_count, this.__alignment_count);
+ return line;
+};
+
+OutputLine.prototype.item = function(index) {
+ if (index < 0) {
+ return this.__items[this.__items.length + index];
+ } else {
+ return this.__items[index];
+ }
+};
+
+OutputLine.prototype.has_match = function(pattern) {
+ for (var lastCheckedOutput = this.__items.length - 1; lastCheckedOutput >= 0; lastCheckedOutput--) {
+ if (this.__items[lastCheckedOutput].match(pattern)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+OutputLine.prototype.set_indent = function(indent, alignment) {
+ if (this.is_empty()) {
+ this.__indent_count = indent || 0;
+ this.__alignment_count = alignment || 0;
+ this.__character_count = this.__parent.get_indent_size(this.__indent_count, this.__alignment_count);
+ }
+};
+
+OutputLine.prototype._set_wrap_point = function() {
+ if (this.__parent.wrap_line_length) {
+ this.__wrap_point_index = this.__items.length;
+ this.__wrap_point_character_count = this.__character_count;
+ this.__wrap_point_indent_count = this.__parent.next_line.__indent_count;
+ this.__wrap_point_alignment_count = this.__parent.next_line.__alignment_count;
+ }
+};
+
+OutputLine.prototype._should_wrap = function() {
+ return this.__wrap_point_index &&
+ this.__character_count > this.__parent.wrap_line_length &&
+ this.__wrap_point_character_count > this.__parent.next_line.__character_count;
+};
+
+OutputLine.prototype._allow_wrap = function() {
+ if (this._should_wrap()) {
+ this.__parent.add_new_line();
+ var next = this.__parent.current_line;
+ next.set_indent(this.__wrap_point_indent_count, this.__wrap_point_alignment_count);
+ next.__items = this.__items.slice(this.__wrap_point_index);
+ this.__items = this.__items.slice(0, this.__wrap_point_index);
+
+ next.__character_count += this.__character_count - this.__wrap_point_character_count;
+ this.__character_count = this.__wrap_point_character_count;
+
+ if (next.__items[0] === " ") {
+ next.__items.splice(0, 1);
+ next.__character_count -= 1;
+ }
+ return true;
+ }
+ return false;
+};
+
+OutputLine.prototype.is_empty = function() {
+ return this.__items.length === 0;
+};
+
+OutputLine.prototype.last = function() {
+ if (!this.is_empty()) {
+ return this.__items[this.__items.length - 1];
+ } else {
+ return null;
+ }
+};
+
+OutputLine.prototype.push = function(item) {
+ this.__items.push(item);
+ var last_newline_index = item.lastIndexOf('\n');
+ if (last_newline_index !== -1) {
+ this.__character_count = item.length - last_newline_index;
+ } else {
+ this.__character_count += item.length;
+ }
+};
+
+OutputLine.prototype.pop = function() {
+ var item = null;
+ if (!this.is_empty()) {
+ item = this.__items.pop();
+ this.__character_count -= item.length;
+ }
+ return item;
+};
+
+
+OutputLine.prototype._remove_indent = function() {
+ if (this.__indent_count > 0) {
+ this.__indent_count -= 1;
+ this.__character_count -= this.__parent.indent_size;
+ }
+};
+
+OutputLine.prototype._remove_wrap_indent = function() {
+ if (this.__wrap_point_indent_count > 0) {
+ this.__wrap_point_indent_count -= 1;
+ }
+};
+OutputLine.prototype.trim = function() {
+ while (this.last() === ' ') {
+ this.__items.pop();
+ this.__character_count -= 1;
+ }
+};
+
+OutputLine.prototype.toString = function() {
+ var result = '';
+ if (this.is_empty()) {
+ if (this.__parent.indent_empty_lines) {
+ result = this.__parent.get_indent_string(this.__indent_count);
+ }
+ } else {
+ result = this.__parent.get_indent_string(this.__indent_count, this.__alignment_count);
+ result += this.__items.join('');
+ }
+ return result;
+};
+
+function IndentStringCache(options, baseIndentString) {
+ this.__cache = [''];
+ this.__indent_size = options.indent_size;
+ this.__indent_string = options.indent_char;
+ if (!options.indent_with_tabs) {
+ this.__indent_string = new Array(options.indent_size + 1).join(options.indent_char);
+ }
+
+ // Set to null to continue support for auto detection of base indent
+ baseIndentString = baseIndentString || '';
+ if (options.indent_level > 0) {
+ baseIndentString = new Array(options.indent_level + 1).join(this.__indent_string);
+ }
+
+ this.__base_string = baseIndentString;
+ this.__base_string_length = baseIndentString.length;
+}
+
+IndentStringCache.prototype.get_indent_size = function(indent, column) {
+ var result = this.__base_string_length;
+ column = column || 0;
+ if (indent < 0) {
+ result = 0;
+ }
+ result += indent * this.__indent_size;
+ result += column;
+ return result;
+};
+
+IndentStringCache.prototype.get_indent_string = function(indent_level, column) {
+ var result = this.__base_string;
+ column = column || 0;
+ if (indent_level < 0) {
+ indent_level = 0;
+ result = '';
+ }
+ column += indent_level * this.__indent_size;
+ this.__ensure_cache(column);
+ result += this.__cache[column];
+ return result;
+};
+
+IndentStringCache.prototype.__ensure_cache = function(column) {
+ while (column >= this.__cache.length) {
+ this.__add_column();
+ }
+};
+
+IndentStringCache.prototype.__add_column = function() {
+ var column = this.__cache.length;
+ var indent = 0;
+ var result = '';
+ if (this.__indent_size && column >= this.__indent_size) {
+ indent = Math.floor(column / this.__indent_size);
+ column -= indent * this.__indent_size;
+ result = new Array(indent + 1).join(this.__indent_string);
+ }
+ if (column) {
+ result += new Array(column + 1).join(' ');
+ }
+
+ this.__cache.push(result);
+};
+
+function Output(options, baseIndentString) {
+ this.__indent_cache = new IndentStringCache(options, baseIndentString);
+ this.raw = false;
+ this._end_with_newline = options.end_with_newline;
+ this.indent_size = options.indent_size;
+ this.wrap_line_length = options.wrap_line_length;
+ this.indent_empty_lines = options.indent_empty_lines;
+ this.__lines = [];
+ this.previous_line = null;
+ this.current_line = null;
+ this.next_line = new OutputLine(this);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = false;
+ // initialize
+ this.__add_outputline();
+}
+
+Output.prototype.__add_outputline = function() {
+ this.previous_line = this.current_line;
+ this.current_line = this.next_line.clone_empty();
+ this.__lines.push(this.current_line);
+};
+
+Output.prototype.get_line_number = function() {
+ return this.__lines.length;
+};
+
+Output.prototype.get_indent_string = function(indent, column) {
+ return this.__indent_cache.get_indent_string(indent, column);
+};
+
+Output.prototype.get_indent_size = function(indent, column) {
+ return this.__indent_cache.get_indent_size(indent, column);
+};
+
+Output.prototype.is_empty = function() {
+ return !this.previous_line && this.current_line.is_empty();
+};
+
+Output.prototype.add_new_line = function(force_newline) {
+ // never newline at the start of file
+ // otherwise, newline only if we didn't just add one or we're forced
+ if (this.is_empty() ||
+ (!force_newline && this.just_added_newline())) {
+ return false;
+ }
+
+ // if raw output is enabled, don't print additional newlines,
+ // but still return True as though you had
+ if (!this.raw) {
+ this.__add_outputline();
+ }
+ return true;
+};
+
+Output.prototype.get_code = function(eol) {
+ this.trim(true);
+
+ // handle some edge cases where the last tokens
+ // has text that ends with newline(s)
+ var last_item = this.current_line.pop();
+ if (last_item) {
+ if (last_item[last_item.length - 1] === '\n') {
+ last_item = last_item.replace(/\n+$/g, '');
+ }
+ this.current_line.push(last_item);
+ }
+
+ if (this._end_with_newline) {
+ this.__add_outputline();
+ }
+
+ var sweet_code = this.__lines.join('\n');
+
+ if (eol !== '\n') {
+ sweet_code = sweet_code.replace(/[\n]/g, eol);
+ }
+ return sweet_code;
+};
+
+Output.prototype.set_wrap_point = function() {
+ this.current_line._set_wrap_point();
+};
+
+Output.prototype.set_indent = function(indent, alignment) {
+ indent = indent || 0;
+ alignment = alignment || 0;
+
+ // Next line stores alignment values
+ this.next_line.set_indent(indent, alignment);
+
+ // Never indent your first output indent at the start of the file
+ if (this.__lines.length > 1) {
+ this.current_line.set_indent(indent, alignment);
+ return true;
+ }
+
+ this.current_line.set_indent();
+ return false;
+};
+
+Output.prototype.add_raw_token = function(token) {
+ for (var x = 0; x < token.newlines; x++) {
+ this.__add_outputline();
+ }
+ this.current_line.set_indent(-1);
+ this.current_line.push(token.whitespace_before);
+ this.current_line.push(token.text);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = false;
+};
+
+Output.prototype.add_token = function(printable_token) {
+ this.__add_space_before_token();
+ this.current_line.push(printable_token);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = this.current_line._allow_wrap();
+};
+
+Output.prototype.__add_space_before_token = function() {
+ if (this.space_before_token && !this.just_added_newline()) {
+ if (!this.non_breaking_space) {
+ this.set_wrap_point();
+ }
+ this.current_line.push(' ');
+ }
+};
+
+Output.prototype.remove_indent = function(index) {
+ var output_length = this.__lines.length;
+ while (index < output_length) {
+ this.__lines[index]._remove_indent();
+ index++;
+ }
+ this.current_line._remove_wrap_indent();
+};
+
+Output.prototype.trim = function(eat_newlines) {
+ eat_newlines = (eat_newlines === undefined) ? false : eat_newlines;
+
+ this.current_line.trim();
+
+ while (eat_newlines && this.__lines.length > 1 &&
+ this.current_line.is_empty()) {
+ this.__lines.pop();
+ this.current_line = this.__lines[this.__lines.length - 1];
+ this.current_line.trim();
+ }
+
+ this.previous_line = this.__lines.length > 1 ?
+ this.__lines[this.__lines.length - 2] : null;
+};
+
+Output.prototype.just_added_newline = function() {
+ return this.current_line.is_empty();
+};
+
+Output.prototype.just_added_blankline = function() {
+ return this.is_empty() ||
+ (this.current_line.is_empty() && this.previous_line.is_empty());
+};
+
+Output.prototype.ensure_empty_line_above = function(starts_with, ends_with) {
+ var index = this.__lines.length - 2;
+ while (index >= 0) {
+ var potentialEmptyLine = this.__lines[index];
+ if (potentialEmptyLine.is_empty()) {
+ break;
+ } else if (potentialEmptyLine.item(0).indexOf(starts_with) !== 0 &&
+ potentialEmptyLine.item(-1) !== ends_with) {
+ this.__lines.splice(index + 1, 0, new OutputLine(this));
+ this.previous_line = this.__lines[this.__lines.length - 2];
+ break;
+ }
+ index--;
+ }
+};
+
+module.exports.Output = Output;
+
+
+/***/ }),
+/* 3 */,
+/* 4 */,
+/* 5 */,
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Options(options, merge_child_field) {
+ this.raw_options = _mergeOpts(options, merge_child_field);
+
+ // Support passing the source text back with no change
+ this.disabled = this._get_boolean('disabled');
+
+ this.eol = this._get_characters('eol', 'auto');
+ this.end_with_newline = this._get_boolean('end_with_newline');
+ this.indent_size = this._get_number('indent_size', 4);
+ this.indent_char = this._get_characters('indent_char', ' ');
+ this.indent_level = this._get_number('indent_level');
+
+ this.preserve_newlines = this._get_boolean('preserve_newlines', true);
+ this.max_preserve_newlines = this._get_number('max_preserve_newlines', 32786);
+ if (!this.preserve_newlines) {
+ this.max_preserve_newlines = 0;
+ }
+
+ this.indent_with_tabs = this._get_boolean('indent_with_tabs', this.indent_char === '\t');
+ if (this.indent_with_tabs) {
+ this.indent_char = '\t';
+
+ // indent_size behavior changed after 1.8.6
+ // It used to be that indent_size would be
+ // set to 1 for indent_with_tabs. That is no longer needed and
+ // actually doesn't make sense - why not use spaces? Further,
+ // that might produce unexpected behavior - tabs being used
+ // for single-column alignment. So, when indent_with_tabs is true
+ // and indent_size is 1, reset indent_size to 4.
+ if (this.indent_size === 1) {
+ this.indent_size = 4;
+ }
+ }
+
+ // Backwards compat with 1.3.x
+ this.wrap_line_length = this._get_number('wrap_line_length', this._get_number('max_char'));
+
+ this.indent_empty_lines = this._get_boolean('indent_empty_lines');
+
+ // valid templating languages ['django', 'erb', 'handlebars', 'php']
+ // For now, 'auto' = all off for javascript, all on for html (and inline javascript).
+ // other values ignored
+ this.templating = this._get_selection_list('templating', ['auto', 'none', 'django', 'erb', 'handlebars', 'php'], ['auto']);
+}
+
+Options.prototype._get_array = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = default_value || [];
+ if (typeof option_value === 'object') {
+ if (option_value !== null && typeof option_value.concat === 'function') {
+ result = option_value.concat();
+ }
+ } else if (typeof option_value === 'string') {
+ result = option_value.split(/[^a-zA-Z0-9_\/\-]+/);
+ }
+ return result;
+};
+
+Options.prototype._get_boolean = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = option_value === undefined ? !!default_value : !!option_value;
+ return result;
+};
+
+Options.prototype._get_characters = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = default_value || '';
+ if (typeof option_value === 'string') {
+ result = option_value.replace(/\\r/, '\r').replace(/\\n/, '\n').replace(/\\t/, '\t');
+ }
+ return result;
+};
+
+Options.prototype._get_number = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ default_value = parseInt(default_value, 10);
+ if (isNaN(default_value)) {
+ default_value = 0;
+ }
+ var result = parseInt(option_value, 10);
+ if (isNaN(result)) {
+ result = default_value;
+ }
+ return result;
+};
+
+Options.prototype._get_selection = function(name, selection_list, default_value) {
+ var result = this._get_selection_list(name, selection_list, default_value);
+ if (result.length !== 1) {
+ throw new Error(
+ "Invalid Option Value: The option '" + name + "' can only be one of the following values:\n" +
+ selection_list + "\nYou passed in: '" + this.raw_options[name] + "'");
+ }
+
+ return result[0];
+};
+
+
+Options.prototype._get_selection_list = function(name, selection_list, default_value) {
+ if (!selection_list || selection_list.length === 0) {
+ throw new Error("Selection list cannot be empty.");
+ }
+
+ default_value = default_value || [selection_list[0]];
+ if (!this._is_valid_selection(default_value, selection_list)) {
+ throw new Error("Invalid Default Value!");
+ }
+
+ var result = this._get_array(name, default_value);
+ if (!this._is_valid_selection(result, selection_list)) {
+ throw new Error(
+ "Invalid Option Value: The option '" + name + "' can contain only the following values:\n" +
+ selection_list + "\nYou passed in: '" + this.raw_options[name] + "'");
+ }
+
+ return result;
+};
+
+Options.prototype._is_valid_selection = function(result, selection_list) {
+ return result.length && selection_list.length &&
+ !result.some(function(item) { return selection_list.indexOf(item) === -1; });
+};
+
+
+// merges child options up with the parent options object
+// Example: obj = {a: 1, b: {a: 2}}
+// mergeOpts(obj, 'b')
+//
+// Returns: {a: 2}
+function _mergeOpts(allOptions, childFieldName) {
+ var finalOpts = {};
+ allOptions = _normalizeOpts(allOptions);
+ var name;
+
+ for (name in allOptions) {
+ if (name !== childFieldName) {
+ finalOpts[name] = allOptions[name];
+ }
+ }
+
+ //merge in the per type settings for the childFieldName
+ if (childFieldName && allOptions[childFieldName]) {
+ for (name in allOptions[childFieldName]) {
+ finalOpts[name] = allOptions[childFieldName][name];
+ }
+ }
+ return finalOpts;
+}
+
+function _normalizeOpts(options) {
+ var convertedOpts = {};
+ var key;
+
+ for (key in options) {
+ var newKey = key.replace(/-/g, "_");
+ convertedOpts[newKey] = options[key];
+ }
+ return convertedOpts;
+}
+
+module.exports.Options = Options;
+module.exports.normalizeOpts = _normalizeOpts;
+module.exports.mergeOpts = _mergeOpts;
+
+
+/***/ }),
+/* 7 */,
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var regexp_has_sticky = RegExp.prototype.hasOwnProperty('sticky');
+
+function InputScanner(input_string) {
+ this.__input = input_string || '';
+ this.__input_length = this.__input.length;
+ this.__position = 0;
+}
+
+InputScanner.prototype.restart = function() {
+ this.__position = 0;
+};
+
+InputScanner.prototype.back = function() {
+ if (this.__position > 0) {
+ this.__position -= 1;
+ }
+};
+
+InputScanner.prototype.hasNext = function() {
+ return this.__position < this.__input_length;
+};
+
+InputScanner.prototype.next = function() {
+ var val = null;
+ if (this.hasNext()) {
+ val = this.__input.charAt(this.__position);
+ this.__position += 1;
+ }
+ return val;
+};
+
+InputScanner.prototype.peek = function(index) {
+ var val = null;
+ index = index || 0;
+ index += this.__position;
+ if (index >= 0 && index < this.__input_length) {
+ val = this.__input.charAt(index);
+ }
+ return val;
+};
+
+// This is a JavaScript only helper function (not in python)
+// Javascript doesn't have a match method
+// and not all implementation support "sticky" flag.
+// If they do not support sticky then both this.match() and this.test() method
+// must get the match and check the index of the match.
+// If sticky is supported and set, this method will use it.
+// Otherwise it will check that global is set, and fall back to the slower method.
+InputScanner.prototype.__match = function(pattern, index) {
+ pattern.lastIndex = index;
+ var pattern_match = pattern.exec(this.__input);
+
+ if (pattern_match && !(regexp_has_sticky && pattern.sticky)) {
+ if (pattern_match.index !== index) {
+ pattern_match = null;
+ }
+ }
+
+ return pattern_match;
+};
+
+InputScanner.prototype.test = function(pattern, index) {
+ index = index || 0;
+ index += this.__position;
+
+ if (index >= 0 && index < this.__input_length) {
+ return !!this.__match(pattern, index);
+ } else {
+ return false;
+ }
+};
+
+InputScanner.prototype.testChar = function(pattern, index) {
+ // test one character regex match
+ var val = this.peek(index);
+ pattern.lastIndex = 0;
+ return val !== null && pattern.test(val);
+};
+
+InputScanner.prototype.match = function(pattern) {
+ var pattern_match = this.__match(pattern, this.__position);
+ if (pattern_match) {
+ this.__position += pattern_match[0].length;
+ } else {
+ pattern_match = null;
+ }
+ return pattern_match;
+};
+
+InputScanner.prototype.read = function(starting_pattern, until_pattern, until_after) {
+ var val = '';
+ var match;
+ if (starting_pattern) {
+ match = this.match(starting_pattern);
+ if (match) {
+ val += match[0];
+ }
+ }
+ if (until_pattern && (match || !starting_pattern)) {
+ val += this.readUntil(until_pattern, until_after);
+ }
+ return val;
+};
+
+InputScanner.prototype.readUntil = function(pattern, until_after) {
+ var val = '';
+ var match_index = this.__position;
+ pattern.lastIndex = this.__position;
+ var pattern_match = pattern.exec(this.__input);
+ if (pattern_match) {
+ match_index = pattern_match.index;
+ if (until_after) {
+ match_index += pattern_match[0].length;
+ }
+ } else {
+ match_index = this.__input_length;
+ }
+
+ val = this.__input.substring(this.__position, match_index);
+ this.__position = match_index;
+ return val;
+};
+
+InputScanner.prototype.readUntilAfter = function(pattern) {
+ return this.readUntil(pattern, true);
+};
+
+InputScanner.prototype.get_regexp = function(pattern, match_from) {
+ var result = null;
+ var flags = 'g';
+ if (match_from && regexp_has_sticky) {
+ flags = 'y';
+ }
+ // strings are converted to regexp
+ if (typeof pattern === "string" && pattern !== '') {
+ // result = new RegExp(pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), flags);
+ result = new RegExp(pattern, flags);
+ } else if (pattern) {
+ result = new RegExp(pattern.source, flags);
+ }
+ return result;
+};
+
+InputScanner.prototype.get_literal_regexp = function(literal_string) {
+ return RegExp(literal_string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
+};
+
+/* css beautifier legacy helpers */
+InputScanner.prototype.peekUntilAfter = function(pattern) {
+ var start = this.__position;
+ var val = this.readUntilAfter(pattern);
+ this.__position = start;
+ return val;
+};
+
+InputScanner.prototype.lookBack = function(testVal) {
+ var start = this.__position - 1;
+ return start >= testVal.length && this.__input.substring(start - testVal.length, start)
+ .toLowerCase() === testVal;
+};
+
+module.exports.InputScanner = InputScanner;
+
+
+/***/ }),
+/* 9 */,
+/* 10 */,
+/* 11 */,
+/* 12 */,
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Directives(start_block_pattern, end_block_pattern) {
+ start_block_pattern = typeof start_block_pattern === 'string' ? start_block_pattern : start_block_pattern.source;
+ end_block_pattern = typeof end_block_pattern === 'string' ? end_block_pattern : end_block_pattern.source;
+ this.__directives_block_pattern = new RegExp(start_block_pattern + / beautify( \w+[:]\w+)+ /.source + end_block_pattern, 'g');
+ this.__directive_pattern = / (\w+)[:](\w+)/g;
+
+ this.__directives_end_ignore_pattern = new RegExp(start_block_pattern + /\sbeautify\signore:end\s/.source + end_block_pattern, 'g');
+}
+
+Directives.prototype.get_directives = function(text) {
+ if (!text.match(this.__directives_block_pattern)) {
+ return null;
+ }
+
+ var directives = {};
+ this.__directive_pattern.lastIndex = 0;
+ var directive_match = this.__directive_pattern.exec(text);
+
+ while (directive_match) {
+ directives[directive_match[1]] = directive_match[2];
+ directive_match = this.__directive_pattern.exec(text);
+ }
+
+ return directives;
+};
+
+Directives.prototype.readIgnored = function(input) {
+ return input.readUntilAfter(this.__directives_end_ignore_pattern);
+};
+
+
+module.exports.Directives = Directives;
+
+
+/***/ }),
+/* 14 */,
+/* 15 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Beautifier = __webpack_require__(16).Beautifier,
+ Options = __webpack_require__(17).Options;
+
+function css_beautify(source_text, options) {
+ var beautifier = new Beautifier(source_text, options);
+ return beautifier.beautify();
+}
+
+module.exports = css_beautify;
+module.exports.defaultOptions = function() {
+ return new Options();
+};
+
+
+/***/ }),
+/* 16 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Options = __webpack_require__(17).Options;
+var Output = __webpack_require__(2).Output;
+var InputScanner = __webpack_require__(8).InputScanner;
+var Directives = __webpack_require__(13).Directives;
+
+var directives_core = new Directives(/\/\*/, /\*\//);
+
+var lineBreak = /\r\n|[\r\n]/;
+var allLineBreaks = /\r\n|[\r\n]/g;
+
+// tokenizer
+var whitespaceChar = /\s/;
+var whitespacePattern = /(?:\s|\n)+/g;
+var block_comment_pattern = /\/\*(?:[\s\S]*?)((?:\*\/)|$)/g;
+var comment_pattern = /\/\/(?:[^\n\r\u2028\u2029]*)/g;
+
+function Beautifier(source_text, options) {
+ this._source_text = source_text || '';
+ // Allow the setting of language/file-type specific options
+ // with inheritance of overall settings
+ this._options = new Options(options);
+ this._ch = null;
+ this._input = null;
+
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
+ this.NESTED_AT_RULE = {
+ "@page": true,
+ "@font-face": true,
+ "@keyframes": true,
+ // also in CONDITIONAL_GROUP_RULE below
+ "@media": true,
+ "@supports": true,
+ "@document": true
+ };
+ this.CONDITIONAL_GROUP_RULE = {
+ "@media": true,
+ "@supports": true,
+ "@document": true
+ };
+
+}
+
+Beautifier.prototype.eatString = function(endChars) {
+ var result = '';
+ this._ch = this._input.next();
+ while (this._ch) {
+ result += this._ch;
+ if (this._ch === "\\") {
+ result += this._input.next();
+ } else if (endChars.indexOf(this._ch) !== -1 || this._ch === "\n") {
+ break;
+ }
+ this._ch = this._input.next();
+ }
+ return result;
+};
+
+// Skips any white space in the source text from the current position.
+// When allowAtLeastOneNewLine is true, will output new lines for each
+// newline character found; if the user has preserve_newlines off, only
+// the first newline will be output
+Beautifier.prototype.eatWhitespace = function(allowAtLeastOneNewLine) {
+ var result = whitespaceChar.test(this._input.peek());
+ var isFirstNewLine = true;
+
+ while (whitespaceChar.test(this._input.peek())) {
+ this._ch = this._input.next();
+ if (allowAtLeastOneNewLine && this._ch === '\n') {
+ if (this._options.preserve_newlines || isFirstNewLine) {
+ isFirstNewLine = false;
+ this._output.add_new_line(true);
+ }
+ }
+ }
+ return result;
+};
+
+// Nested pseudo-class if we are insideRule
+// and the next special character found opens
+// a new block
+Beautifier.prototype.foundNestedPseudoClass = function() {
+ var openParen = 0;
+ var i = 1;
+ var ch = this._input.peek(i);
+ while (ch) {
+ if (ch === "{") {
+ return true;
+ } else if (ch === '(') {
+ // pseudoclasses can contain ()
+ openParen += 1;
+ } else if (ch === ')') {
+ if (openParen === 0) {
+ return false;
+ }
+ openParen -= 1;
+ } else if (ch === ";" || ch === "}") {
+ return false;
+ }
+ i++;
+ ch = this._input.peek(i);
+ }
+ return false;
+};
+
+Beautifier.prototype.print_string = function(output_string) {
+ this._output.set_indent(this._indentLevel);
+ this._output.non_breaking_space = true;
+ this._output.add_token(output_string);
+};
+
+Beautifier.prototype.preserveSingleSpace = function(isAfterSpace) {
+ if (isAfterSpace) {
+ this._output.space_before_token = true;
+ }
+};
+
+Beautifier.prototype.indent = function() {
+ this._indentLevel++;
+};
+
+Beautifier.prototype.outdent = function() {
+ if (this._indentLevel > 0) {
+ this._indentLevel--;
+ }
+};
+
+/*_____________________--------------------_____________________*/
+
+Beautifier.prototype.beautify = function() {
+ if (this._options.disabled) {
+ return this._source_text;
+ }
+
+ var source_text = this._source_text;
+ var eol = this._options.eol;
+ if (eol === 'auto') {
+ eol = '\n';
+ if (source_text && lineBreak.test(source_text || '')) {
+ eol = source_text.match(lineBreak)[0];
+ }
+ }
+
+
+ // HACK: newline parsing inconsistent. This brute force normalizes the this._input.
+ source_text = source_text.replace(allLineBreaks, '\n');
+
+ // reset
+ var baseIndentString = source_text.match(/^[\t ]*/)[0];
+
+ this._output = new Output(this._options, baseIndentString);
+ this._input = new InputScanner(source_text);
+ this._indentLevel = 0;
+ this._nestedLevel = 0;
+
+ this._ch = null;
+ var parenLevel = 0;
+
+ var insideRule = false;
+ // This is the value side of a property value pair (blue in the following ex)
+ // label { content: blue }
+ var insidePropertyValue = false;
+ var enteringConditionalGroup = false;
+ var insideAtExtend = false;
+ var insideAtImport = false;
+ var topCharacter = this._ch;
+ var whitespace;
+ var isAfterSpace;
+ var previous_ch;
+
+ while (true) {
+ whitespace = this._input.read(whitespacePattern);
+ isAfterSpace = whitespace !== '';
+ previous_ch = topCharacter;
+ this._ch = this._input.next();
+ if (this._ch === '\\' && this._input.hasNext()) {
+ this._ch += this._input.next();
+ }
+ topCharacter = this._ch;
+
+ if (!this._ch) {
+ break;
+ } else if (this._ch === '/' && this._input.peek() === '*') {
+ // /* css comment */
+ // Always start block comments on a new line.
+ // This handles scenarios where a block comment immediately
+ // follows a property definition on the same line or where
+ // minified code is being beautified.
+ this._output.add_new_line();
+ this._input.back();
+
+ var comment = this._input.read(block_comment_pattern);
+
+ // Handle ignore directive
+ var directives = directives_core.get_directives(comment);
+ if (directives && directives.ignore === 'start') {
+ comment += directives_core.readIgnored(this._input);
+ }
+
+ this.print_string(comment);
+
+ // Ensures any new lines following the comment are preserved
+ this.eatWhitespace(true);
+
+ // Block comments are followed by a new line so they don't
+ // share a line with other properties
+ this._output.add_new_line();
+ } else if (this._ch === '/' && this._input.peek() === '/') {
+ // // single line comment
+ // Preserves the space before a comment
+ // on the same line as a rule
+ this._output.space_before_token = true;
+ this._input.back();
+ this.print_string(this._input.read(comment_pattern));
+
+ // Ensures any new lines following the comment are preserved
+ this.eatWhitespace(true);
+ } else if (this._ch === '@') {
+ this.preserveSingleSpace(isAfterSpace);
+
+ // deal with less propery mixins @{...}
+ if (this._input.peek() === '{') {
+ this.print_string(this._ch + this.eatString('}'));
+ } else {
+ this.print_string(this._ch);
+
+ // strip trailing space, if present, for hash property checks
+ var variableOrRule = this._input.peekUntilAfter(/[: ,;{}()[\]\/='"]/g);
+
+ if (variableOrRule.match(/[ :]$/)) {
+ // we have a variable or pseudo-class, add it and insert one space before continuing
+ variableOrRule = this.eatString(": ").replace(/\s$/, '');
+ this.print_string(variableOrRule);
+ this._output.space_before_token = true;
+ }
+
+ variableOrRule = variableOrRule.replace(/\s$/, '');
+
+ if (variableOrRule === 'extend') {
+ insideAtExtend = true;
+ } else if (variableOrRule === 'import') {
+ insideAtImport = true;
+ }
+
+ // might be a nesting at-rule
+ if (variableOrRule in this.NESTED_AT_RULE) {
+ this._nestedLevel += 1;
+ if (variableOrRule in this.CONDITIONAL_GROUP_RULE) {
+ enteringConditionalGroup = true;
+ }
+ // might be less variable
+ } else if (!insideRule && parenLevel === 0 && variableOrRule.indexOf(':') !== -1) {
+ insidePropertyValue = true;
+ this.indent();
+ }
+ }
+ } else if (this._ch === '#' && this._input.peek() === '{') {
+ this.preserveSingleSpace(isAfterSpace);
+ this.print_string(this._ch + this.eatString('}'));
+ } else if (this._ch === '{') {
+ if (insidePropertyValue) {
+ insidePropertyValue = false;
+ this.outdent();
+ }
+
+ // when entering conditional groups, only rulesets are allowed
+ if (enteringConditionalGroup) {
+ enteringConditionalGroup = false;
+ insideRule = (this._indentLevel >= this._nestedLevel);
+ } else {
+ // otherwise, declarations are also allowed
+ insideRule = (this._indentLevel >= this._nestedLevel - 1);
+ }
+ if (this._options.newline_between_rules && insideRule) {
+ if (this._output.previous_line && this._output.previous_line.item(-1) !== '{') {
+ this._output.ensure_empty_line_above('/', ',');
+ }
+ }
+
+ this._output.space_before_token = true;
+
+ // The difference in print_string and indent order is necessary to indent the '{' correctly
+ if (this._options.brace_style === 'expand') {
+ this._output.add_new_line();
+ this.print_string(this._ch);
+ this.indent();
+ this._output.set_indent(this._indentLevel);
+ } else {
+ this.indent();
+ this.print_string(this._ch);
+ }
+
+ this.eatWhitespace(true);
+ this._output.add_new_line();
+ } else if (this._ch === '}') {
+ this.outdent();
+ this._output.add_new_line();
+ if (previous_ch === '{') {
+ this._output.trim(true);
+ }
+ insideAtImport = false;
+ insideAtExtend = false;
+ if (insidePropertyValue) {
+ this.outdent();
+ insidePropertyValue = false;
+ }
+ this.print_string(this._ch);
+ insideRule = false;
+ if (this._nestedLevel) {
+ this._nestedLevel--;
+ }
+
+ this.eatWhitespace(true);
+ this._output.add_new_line();
+
+ if (this._options.newline_between_rules && !this._output.just_added_blankline()) {
+ if (this._input.peek() !== '}') {
+ this._output.add_new_line(true);
+ }
+ }
+ } else if (this._ch === ":") {
+ if ((insideRule || enteringConditionalGroup) && !(this._input.lookBack("&") || this.foundNestedPseudoClass()) && !this._input.lookBack("(") && !insideAtExtend && parenLevel === 0) {
+ // 'property: value' delimiter
+ // which could be in a conditional group query
+ this.print_string(':');
+ if (!insidePropertyValue) {
+ insidePropertyValue = true;
+ this._output.space_before_token = true;
+ this.eatWhitespace(true);
+ this.indent();
+ }
+ } else {
+ // sass/less parent reference don't use a space
+ // sass nested pseudo-class don't use a space
+
+ // preserve space before pseudoclasses/pseudoelements, as it means "in any child"
+ if (this._input.lookBack(" ")) {
+ this._output.space_before_token = true;
+ }
+ if (this._input.peek() === ":") {
+ // pseudo-element
+ this._ch = this._input.next();
+ this.print_string("::");
+ } else {
+ // pseudo-class
+ this.print_string(':');
+ }
+ }
+ } else if (this._ch === '"' || this._ch === '\'') {
+ this.preserveSingleSpace(isAfterSpace);
+ this.print_string(this._ch + this.eatString(this._ch));
+ this.eatWhitespace(true);
+ } else if (this._ch === ';') {
+ if (parenLevel === 0) {
+ if (insidePropertyValue) {
+ this.outdent();
+ insidePropertyValue = false;
+ }
+ insideAtExtend = false;
+ insideAtImport = false;
+ this.print_string(this._ch);
+ this.eatWhitespace(true);
+
+ // This maintains single line comments on the same
+ // line. Block comments are also affected, but
+ // a new line is always output before one inside
+ // that section
+ if (this._input.peek() !== '/') {
+ this._output.add_new_line();
+ }
+ } else {
+ this.print_string(this._ch);
+ this.eatWhitespace(true);
+ this._output.space_before_token = true;
+ }
+ } else if (this._ch === '(') { // may be a url
+ if (this._input.lookBack("url")) {
+ this.print_string(this._ch);
+ this.eatWhitespace();
+ parenLevel++;
+ this.indent();
+ this._ch = this._input.next();
+ if (this._ch === ')' || this._ch === '"' || this._ch === '\'') {
+ this._input.back();
+ } else if (this._ch) {
+ this.print_string(this._ch + this.eatString(')'));
+ if (parenLevel) {
+ parenLevel--;
+ this.outdent();
+ }
+ }
+ } else {
+ this.preserveSingleSpace(isAfterSpace);
+ this.print_string(this._ch);
+ this.eatWhitespace();
+ parenLevel++;
+ this.indent();
+ }
+ } else if (this._ch === ')') {
+ if (parenLevel) {
+ parenLevel--;
+ this.outdent();
+ }
+ this.print_string(this._ch);
+ } else if (this._ch === ',') {
+ this.print_string(this._ch);
+ this.eatWhitespace(true);
+ if (this._options.selector_separator_newline && !insidePropertyValue && parenLevel === 0 && !insideAtImport) {
+ this._output.add_new_line();
+ } else {
+ this._output.space_before_token = true;
+ }
+ } else if ((this._ch === '>' || this._ch === '+' || this._ch === '~') && !insidePropertyValue && parenLevel === 0) {
+ //handle combinator spacing
+ if (this._options.space_around_combinator) {
+ this._output.space_before_token = true;
+ this.print_string(this._ch);
+ this._output.space_before_token = true;
+ } else {
+ this.print_string(this._ch);
+ this.eatWhitespace();
+ // squash extra whitespace
+ if (this._ch && whitespaceChar.test(this._ch)) {
+ this._ch = '';
+ }
+ }
+ } else if (this._ch === ']') {
+ this.print_string(this._ch);
+ } else if (this._ch === '[') {
+ this.preserveSingleSpace(isAfterSpace);
+ this.print_string(this._ch);
+ } else if (this._ch === '=') { // no whitespace before or after
+ this.eatWhitespace();
+ this.print_string('=');
+ if (whitespaceChar.test(this._ch)) {
+ this._ch = '';
+ }
+ } else if (this._ch === '!' && !this._input.lookBack("\\")) { // !important
+ this.print_string(' ');
+ this.print_string(this._ch);
+ } else {
+ this.preserveSingleSpace(isAfterSpace);
+ this.print_string(this._ch);
+ }
+ }
+
+ var sweetCode = this._output.get_code(eol);
+
+ return sweetCode;
+};
+
+module.exports.Beautifier = Beautifier;
+
+
+/***/ }),
+/* 17 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var BaseOptions = __webpack_require__(6).Options;
+
+function Options(options) {
+ BaseOptions.call(this, options, 'css');
+
+ this.selector_separator_newline = this._get_boolean('selector_separator_newline', true);
+ this.newline_between_rules = this._get_boolean('newline_between_rules', true);
+ var space_around_selector_separator = this._get_boolean('space_around_selector_separator');
+ this.space_around_combinator = this._get_boolean('space_around_combinator') || space_around_selector_separator;
+
+ var brace_style_split = this._get_selection_list('brace_style', ['collapse', 'expand', 'end-expand', 'none', 'preserve-inline']);
+ this.brace_style = 'collapse';
+ for (var bs = 0; bs < brace_style_split.length; bs++) {
+ if (brace_style_split[bs] !== 'expand') {
+ // default to collapse, as only collapse|expand is implemented for now
+ this.brace_style = 'collapse';
+ } else {
+ this.brace_style = brace_style_split[bs];
+ }
+ }
+}
+Options.prototype = new BaseOptions();
+
+
+
+module.exports.Options = Options;
+
+
+/***/ })
+/******/ ]);
+var css_beautify = legacy_beautify_css;
+/* Footer */
+if (typeof define === "function" && define.amd) {
+ // Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
+ define([], function() {
+ return {
+ css_beautify: css_beautify
+ };
+ });
+} else if (typeof exports !== "undefined") {
+ // Add support for CommonJS. Just put this file somewhere on your require.paths
+ // and you will be able to `var html_beautify = require("beautify").html_beautify`.
+ exports.css_beautify = css_beautify;
+} else if (typeof window !== "undefined") {
+ // If we're running a web page and don't have either of the above, add our one global
+ window.css_beautify = css_beautify;
+} else if (typeof global !== "undefined") {
+ // If we don't even have window, try global.
+ global.css_beautify = css_beautify;
+}
+
+}());
diff --git a/devtools/shared/jsbeautify/src/beautify-html.js b/devtools/shared/jsbeautify/src/beautify-html.js
new file mode 100644
index 0000000000..8ba15ee59f
--- /dev/null
+++ b/devtools/shared/jsbeautify/src/beautify-html.js
@@ -0,0 +1,3166 @@
+/* AUTO-GENERATED. DO NOT MODIFY. */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+ Style HTML
+---------------
+
+ Written by Nochum Sossonko, (nsossonko@hotmail.com)
+
+ Based on code initially developed by: Einar Lielmanis, <einar@beautifier.io>
+ https://beautifier.io/
+
+ Usage:
+ style_html(html_source);
+
+ style_html(html_source, options);
+
+ The options are:
+ indent_inner_html (default false) — indent <head> and <body> sections,
+ indent_size (default 4) — indentation size,
+ indent_char (default space) — character to indent with,
+ wrap_line_length (default 250) - maximum amount of characters per line (0 = disable)
+ brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | "none"
+ put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line, or attempt to keep them where they are.
+ inline (defaults to inline tags) - list of tags to be considered inline tags
+ unformatted (defaults to inline tags) - list of tags, that shouldn't be reformatted
+ content_unformatted (defaults to ["pre", "textarea"] tags) - list of tags, whose content shouldn't be reformatted
+ indent_scripts (default normal) - "keep"|"separate"|"normal"
+ preserve_newlines (default true) - whether existing line breaks before elements should be preserved
+ Only works before elements, not inside tags or for text.
+ max_preserve_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk
+ indent_handlebars (default false) - format and indent {{#foo}} and {{/foo}}
+ end_with_newline (false) - end with a newline
+ extra_liners (default [head,body,/html]) -List of tags that should have an extra newline before them.
+
+ e.g.
+
+ style_html(html_source, {
+ 'indent_inner_html': false,
+ 'indent_size': 2,
+ 'indent_char': ' ',
+ 'wrap_line_length': 78,
+ 'brace_style': 'expand',
+ 'preserve_newlines': true,
+ 'max_preserve_newlines': 5,
+ 'indent_handlebars': false,
+ 'extra_liners': ['/html']
+ });
+*/
+
+(function() {
+
+/* GENERATED_BUILD_OUTPUT */
+var legacy_beautify_html =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 18);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */,
+/* 1 */,
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function OutputLine(parent) {
+ this.__parent = parent;
+ this.__character_count = 0;
+ // use indent_count as a marker for this.__lines that have preserved indentation
+ this.__indent_count = -1;
+ this.__alignment_count = 0;
+ this.__wrap_point_index = 0;
+ this.__wrap_point_character_count = 0;
+ this.__wrap_point_indent_count = -1;
+ this.__wrap_point_alignment_count = 0;
+
+ this.__items = [];
+}
+
+OutputLine.prototype.clone_empty = function() {
+ var line = new OutputLine(this.__parent);
+ line.set_indent(this.__indent_count, this.__alignment_count);
+ return line;
+};
+
+OutputLine.prototype.item = function(index) {
+ if (index < 0) {
+ return this.__items[this.__items.length + index];
+ } else {
+ return this.__items[index];
+ }
+};
+
+OutputLine.prototype.has_match = function(pattern) {
+ for (var lastCheckedOutput = this.__items.length - 1; lastCheckedOutput >= 0; lastCheckedOutput--) {
+ if (this.__items[lastCheckedOutput].match(pattern)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+OutputLine.prototype.set_indent = function(indent, alignment) {
+ if (this.is_empty()) {
+ this.__indent_count = indent || 0;
+ this.__alignment_count = alignment || 0;
+ this.__character_count = this.__parent.get_indent_size(this.__indent_count, this.__alignment_count);
+ }
+};
+
+OutputLine.prototype._set_wrap_point = function() {
+ if (this.__parent.wrap_line_length) {
+ this.__wrap_point_index = this.__items.length;
+ this.__wrap_point_character_count = this.__character_count;
+ this.__wrap_point_indent_count = this.__parent.next_line.__indent_count;
+ this.__wrap_point_alignment_count = this.__parent.next_line.__alignment_count;
+ }
+};
+
+OutputLine.prototype._should_wrap = function() {
+ return this.__wrap_point_index &&
+ this.__character_count > this.__parent.wrap_line_length &&
+ this.__wrap_point_character_count > this.__parent.next_line.__character_count;
+};
+
+OutputLine.prototype._allow_wrap = function() {
+ if (this._should_wrap()) {
+ this.__parent.add_new_line();
+ var next = this.__parent.current_line;
+ next.set_indent(this.__wrap_point_indent_count, this.__wrap_point_alignment_count);
+ next.__items = this.__items.slice(this.__wrap_point_index);
+ this.__items = this.__items.slice(0, this.__wrap_point_index);
+
+ next.__character_count += this.__character_count - this.__wrap_point_character_count;
+ this.__character_count = this.__wrap_point_character_count;
+
+ if (next.__items[0] === " ") {
+ next.__items.splice(0, 1);
+ next.__character_count -= 1;
+ }
+ return true;
+ }
+ return false;
+};
+
+OutputLine.prototype.is_empty = function() {
+ return this.__items.length === 0;
+};
+
+OutputLine.prototype.last = function() {
+ if (!this.is_empty()) {
+ return this.__items[this.__items.length - 1];
+ } else {
+ return null;
+ }
+};
+
+OutputLine.prototype.push = function(item) {
+ this.__items.push(item);
+ var last_newline_index = item.lastIndexOf('\n');
+ if (last_newline_index !== -1) {
+ this.__character_count = item.length - last_newline_index;
+ } else {
+ this.__character_count += item.length;
+ }
+};
+
+OutputLine.prototype.pop = function() {
+ var item = null;
+ if (!this.is_empty()) {
+ item = this.__items.pop();
+ this.__character_count -= item.length;
+ }
+ return item;
+};
+
+
+OutputLine.prototype._remove_indent = function() {
+ if (this.__indent_count > 0) {
+ this.__indent_count -= 1;
+ this.__character_count -= this.__parent.indent_size;
+ }
+};
+
+OutputLine.prototype._remove_wrap_indent = function() {
+ if (this.__wrap_point_indent_count > 0) {
+ this.__wrap_point_indent_count -= 1;
+ }
+};
+OutputLine.prototype.trim = function() {
+ while (this.last() === ' ') {
+ this.__items.pop();
+ this.__character_count -= 1;
+ }
+};
+
+OutputLine.prototype.toString = function() {
+ var result = '';
+ if (this.is_empty()) {
+ if (this.__parent.indent_empty_lines) {
+ result = this.__parent.get_indent_string(this.__indent_count);
+ }
+ } else {
+ result = this.__parent.get_indent_string(this.__indent_count, this.__alignment_count);
+ result += this.__items.join('');
+ }
+ return result;
+};
+
+function IndentStringCache(options, baseIndentString) {
+ this.__cache = [''];
+ this.__indent_size = options.indent_size;
+ this.__indent_string = options.indent_char;
+ if (!options.indent_with_tabs) {
+ this.__indent_string = new Array(options.indent_size + 1).join(options.indent_char);
+ }
+
+ // Set to null to continue support for auto detection of base indent
+ baseIndentString = baseIndentString || '';
+ if (options.indent_level > 0) {
+ baseIndentString = new Array(options.indent_level + 1).join(this.__indent_string);
+ }
+
+ this.__base_string = baseIndentString;
+ this.__base_string_length = baseIndentString.length;
+}
+
+IndentStringCache.prototype.get_indent_size = function(indent, column) {
+ var result = this.__base_string_length;
+ column = column || 0;
+ if (indent < 0) {
+ result = 0;
+ }
+ result += indent * this.__indent_size;
+ result += column;
+ return result;
+};
+
+IndentStringCache.prototype.get_indent_string = function(indent_level, column) {
+ var result = this.__base_string;
+ column = column || 0;
+ if (indent_level < 0) {
+ indent_level = 0;
+ result = '';
+ }
+ column += indent_level * this.__indent_size;
+ this.__ensure_cache(column);
+ result += this.__cache[column];
+ return result;
+};
+
+IndentStringCache.prototype.__ensure_cache = function(column) {
+ while (column >= this.__cache.length) {
+ this.__add_column();
+ }
+};
+
+IndentStringCache.prototype.__add_column = function() {
+ var column = this.__cache.length;
+ var indent = 0;
+ var result = '';
+ if (this.__indent_size && column >= this.__indent_size) {
+ indent = Math.floor(column / this.__indent_size);
+ column -= indent * this.__indent_size;
+ result = new Array(indent + 1).join(this.__indent_string);
+ }
+ if (column) {
+ result += new Array(column + 1).join(' ');
+ }
+
+ this.__cache.push(result);
+};
+
+function Output(options, baseIndentString) {
+ this.__indent_cache = new IndentStringCache(options, baseIndentString);
+ this.raw = false;
+ this._end_with_newline = options.end_with_newline;
+ this.indent_size = options.indent_size;
+ this.wrap_line_length = options.wrap_line_length;
+ this.indent_empty_lines = options.indent_empty_lines;
+ this.__lines = [];
+ this.previous_line = null;
+ this.current_line = null;
+ this.next_line = new OutputLine(this);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = false;
+ // initialize
+ this.__add_outputline();
+}
+
+Output.prototype.__add_outputline = function() {
+ this.previous_line = this.current_line;
+ this.current_line = this.next_line.clone_empty();
+ this.__lines.push(this.current_line);
+};
+
+Output.prototype.get_line_number = function() {
+ return this.__lines.length;
+};
+
+Output.prototype.get_indent_string = function(indent, column) {
+ return this.__indent_cache.get_indent_string(indent, column);
+};
+
+Output.prototype.get_indent_size = function(indent, column) {
+ return this.__indent_cache.get_indent_size(indent, column);
+};
+
+Output.prototype.is_empty = function() {
+ return !this.previous_line && this.current_line.is_empty();
+};
+
+Output.prototype.add_new_line = function(force_newline) {
+ // never newline at the start of file
+ // otherwise, newline only if we didn't just add one or we're forced
+ if (this.is_empty() ||
+ (!force_newline && this.just_added_newline())) {
+ return false;
+ }
+
+ // if raw output is enabled, don't print additional newlines,
+ // but still return True as though you had
+ if (!this.raw) {
+ this.__add_outputline();
+ }
+ return true;
+};
+
+Output.prototype.get_code = function(eol) {
+ this.trim(true);
+
+ // handle some edge cases where the last tokens
+ // has text that ends with newline(s)
+ var last_item = this.current_line.pop();
+ if (last_item) {
+ if (last_item[last_item.length - 1] === '\n') {
+ last_item = last_item.replace(/\n+$/g, '');
+ }
+ this.current_line.push(last_item);
+ }
+
+ if (this._end_with_newline) {
+ this.__add_outputline();
+ }
+
+ var sweet_code = this.__lines.join('\n');
+
+ if (eol !== '\n') {
+ sweet_code = sweet_code.replace(/[\n]/g, eol);
+ }
+ return sweet_code;
+};
+
+Output.prototype.set_wrap_point = function() {
+ this.current_line._set_wrap_point();
+};
+
+Output.prototype.set_indent = function(indent, alignment) {
+ indent = indent || 0;
+ alignment = alignment || 0;
+
+ // Next line stores alignment values
+ this.next_line.set_indent(indent, alignment);
+
+ // Never indent your first output indent at the start of the file
+ if (this.__lines.length > 1) {
+ this.current_line.set_indent(indent, alignment);
+ return true;
+ }
+
+ this.current_line.set_indent();
+ return false;
+};
+
+Output.prototype.add_raw_token = function(token) {
+ for (var x = 0; x < token.newlines; x++) {
+ this.__add_outputline();
+ }
+ this.current_line.set_indent(-1);
+ this.current_line.push(token.whitespace_before);
+ this.current_line.push(token.text);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = false;
+};
+
+Output.prototype.add_token = function(printable_token) {
+ this.__add_space_before_token();
+ this.current_line.push(printable_token);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = this.current_line._allow_wrap();
+};
+
+Output.prototype.__add_space_before_token = function() {
+ if (this.space_before_token && !this.just_added_newline()) {
+ if (!this.non_breaking_space) {
+ this.set_wrap_point();
+ }
+ this.current_line.push(' ');
+ }
+};
+
+Output.prototype.remove_indent = function(index) {
+ var output_length = this.__lines.length;
+ while (index < output_length) {
+ this.__lines[index]._remove_indent();
+ index++;
+ }
+ this.current_line._remove_wrap_indent();
+};
+
+Output.prototype.trim = function(eat_newlines) {
+ eat_newlines = (eat_newlines === undefined) ? false : eat_newlines;
+
+ this.current_line.trim();
+
+ while (eat_newlines && this.__lines.length > 1 &&
+ this.current_line.is_empty()) {
+ this.__lines.pop();
+ this.current_line = this.__lines[this.__lines.length - 1];
+ this.current_line.trim();
+ }
+
+ this.previous_line = this.__lines.length > 1 ?
+ this.__lines[this.__lines.length - 2] : null;
+};
+
+Output.prototype.just_added_newline = function() {
+ return this.current_line.is_empty();
+};
+
+Output.prototype.just_added_blankline = function() {
+ return this.is_empty() ||
+ (this.current_line.is_empty() && this.previous_line.is_empty());
+};
+
+Output.prototype.ensure_empty_line_above = function(starts_with, ends_with) {
+ var index = this.__lines.length - 2;
+ while (index >= 0) {
+ var potentialEmptyLine = this.__lines[index];
+ if (potentialEmptyLine.is_empty()) {
+ break;
+ } else if (potentialEmptyLine.item(0).indexOf(starts_with) !== 0 &&
+ potentialEmptyLine.item(-1) !== ends_with) {
+ this.__lines.splice(index + 1, 0, new OutputLine(this));
+ this.previous_line = this.__lines[this.__lines.length - 2];
+ break;
+ }
+ index--;
+ }
+};
+
+module.exports.Output = Output;
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Token(type, text, newlines, whitespace_before) {
+ this.type = type;
+ this.text = text;
+
+ // comments_before are
+ // comments that have a new line before them
+ // and may or may not have a newline after
+ // this is a set of comments before
+ this.comments_before = null; /* inline comment*/
+
+
+ // this.comments_after = new TokenStream(); // no new line before and newline after
+ this.newlines = newlines || 0;
+ this.whitespace_before = whitespace_before || '';
+ this.parent = null;
+ this.next = null;
+ this.previous = null;
+ this.opened = null;
+ this.closed = null;
+ this.directives = null;
+}
+
+
+module.exports.Token = Token;
+
+
+/***/ }),
+/* 4 */,
+/* 5 */,
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Options(options, merge_child_field) {
+ this.raw_options = _mergeOpts(options, merge_child_field);
+
+ // Support passing the source text back with no change
+ this.disabled = this._get_boolean('disabled');
+
+ this.eol = this._get_characters('eol', 'auto');
+ this.end_with_newline = this._get_boolean('end_with_newline');
+ this.indent_size = this._get_number('indent_size', 4);
+ this.indent_char = this._get_characters('indent_char', ' ');
+ this.indent_level = this._get_number('indent_level');
+
+ this.preserve_newlines = this._get_boolean('preserve_newlines', true);
+ this.max_preserve_newlines = this._get_number('max_preserve_newlines', 32786);
+ if (!this.preserve_newlines) {
+ this.max_preserve_newlines = 0;
+ }
+
+ this.indent_with_tabs = this._get_boolean('indent_with_tabs', this.indent_char === '\t');
+ if (this.indent_with_tabs) {
+ this.indent_char = '\t';
+
+ // indent_size behavior changed after 1.8.6
+ // It used to be that indent_size would be
+ // set to 1 for indent_with_tabs. That is no longer needed and
+ // actually doesn't make sense - why not use spaces? Further,
+ // that might produce unexpected behavior - tabs being used
+ // for single-column alignment. So, when indent_with_tabs is true
+ // and indent_size is 1, reset indent_size to 4.
+ if (this.indent_size === 1) {
+ this.indent_size = 4;
+ }
+ }
+
+ // Backwards compat with 1.3.x
+ this.wrap_line_length = this._get_number('wrap_line_length', this._get_number('max_char'));
+
+ this.indent_empty_lines = this._get_boolean('indent_empty_lines');
+
+ // valid templating languages ['django', 'erb', 'handlebars', 'php']
+ // For now, 'auto' = all off for javascript, all on for html (and inline javascript).
+ // other values ignored
+ this.templating = this._get_selection_list('templating', ['auto', 'none', 'django', 'erb', 'handlebars', 'php'], ['auto']);
+}
+
+Options.prototype._get_array = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = default_value || [];
+ if (typeof option_value === 'object') {
+ if (option_value !== null && typeof option_value.concat === 'function') {
+ result = option_value.concat();
+ }
+ } else if (typeof option_value === 'string') {
+ result = option_value.split(/[^a-zA-Z0-9_\/\-]+/);
+ }
+ return result;
+};
+
+Options.prototype._get_boolean = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = option_value === undefined ? !!default_value : !!option_value;
+ return result;
+};
+
+Options.prototype._get_characters = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = default_value || '';
+ if (typeof option_value === 'string') {
+ result = option_value.replace(/\\r/, '\r').replace(/\\n/, '\n').replace(/\\t/, '\t');
+ }
+ return result;
+};
+
+Options.prototype._get_number = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ default_value = parseInt(default_value, 10);
+ if (isNaN(default_value)) {
+ default_value = 0;
+ }
+ var result = parseInt(option_value, 10);
+ if (isNaN(result)) {
+ result = default_value;
+ }
+ return result;
+};
+
+Options.prototype._get_selection = function(name, selection_list, default_value) {
+ var result = this._get_selection_list(name, selection_list, default_value);
+ if (result.length !== 1) {
+ throw new Error(
+ "Invalid Option Value: The option '" + name + "' can only be one of the following values:\n" +
+ selection_list + "\nYou passed in: '" + this.raw_options[name] + "'");
+ }
+
+ return result[0];
+};
+
+
+Options.prototype._get_selection_list = function(name, selection_list, default_value) {
+ if (!selection_list || selection_list.length === 0) {
+ throw new Error("Selection list cannot be empty.");
+ }
+
+ default_value = default_value || [selection_list[0]];
+ if (!this._is_valid_selection(default_value, selection_list)) {
+ throw new Error("Invalid Default Value!");
+ }
+
+ var result = this._get_array(name, default_value);
+ if (!this._is_valid_selection(result, selection_list)) {
+ throw new Error(
+ "Invalid Option Value: The option '" + name + "' can contain only the following values:\n" +
+ selection_list + "\nYou passed in: '" + this.raw_options[name] + "'");
+ }
+
+ return result;
+};
+
+Options.prototype._is_valid_selection = function(result, selection_list) {
+ return result.length && selection_list.length &&
+ !result.some(function(item) { return selection_list.indexOf(item) === -1; });
+};
+
+
+// merges child options up with the parent options object
+// Example: obj = {a: 1, b: {a: 2}}
+// mergeOpts(obj, 'b')
+//
+// Returns: {a: 2}
+function _mergeOpts(allOptions, childFieldName) {
+ var finalOpts = {};
+ allOptions = _normalizeOpts(allOptions);
+ var name;
+
+ for (name in allOptions) {
+ if (name !== childFieldName) {
+ finalOpts[name] = allOptions[name];
+ }
+ }
+
+ //merge in the per type settings for the childFieldName
+ if (childFieldName && allOptions[childFieldName]) {
+ for (name in allOptions[childFieldName]) {
+ finalOpts[name] = allOptions[childFieldName][name];
+ }
+ }
+ return finalOpts;
+}
+
+function _normalizeOpts(options) {
+ var convertedOpts = {};
+ var key;
+
+ for (key in options) {
+ var newKey = key.replace(/-/g, "_");
+ convertedOpts[newKey] = options[key];
+ }
+ return convertedOpts;
+}
+
+module.exports.Options = Options;
+module.exports.normalizeOpts = _normalizeOpts;
+module.exports.mergeOpts = _mergeOpts;
+
+
+/***/ }),
+/* 7 */,
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var regexp_has_sticky = RegExp.prototype.hasOwnProperty('sticky');
+
+function InputScanner(input_string) {
+ this.__input = input_string || '';
+ this.__input_length = this.__input.length;
+ this.__position = 0;
+}
+
+InputScanner.prototype.restart = function() {
+ this.__position = 0;
+};
+
+InputScanner.prototype.back = function() {
+ if (this.__position > 0) {
+ this.__position -= 1;
+ }
+};
+
+InputScanner.prototype.hasNext = function() {
+ return this.__position < this.__input_length;
+};
+
+InputScanner.prototype.next = function() {
+ var val = null;
+ if (this.hasNext()) {
+ val = this.__input.charAt(this.__position);
+ this.__position += 1;
+ }
+ return val;
+};
+
+InputScanner.prototype.peek = function(index) {
+ var val = null;
+ index = index || 0;
+ index += this.__position;
+ if (index >= 0 && index < this.__input_length) {
+ val = this.__input.charAt(index);
+ }
+ return val;
+};
+
+// This is a JavaScript only helper function (not in python)
+// Javascript doesn't have a match method
+// and not all implementation support "sticky" flag.
+// If they do not support sticky then both this.match() and this.test() method
+// must get the match and check the index of the match.
+// If sticky is supported and set, this method will use it.
+// Otherwise it will check that global is set, and fall back to the slower method.
+InputScanner.prototype.__match = function(pattern, index) {
+ pattern.lastIndex = index;
+ var pattern_match = pattern.exec(this.__input);
+
+ if (pattern_match && !(regexp_has_sticky && pattern.sticky)) {
+ if (pattern_match.index !== index) {
+ pattern_match = null;
+ }
+ }
+
+ return pattern_match;
+};
+
+InputScanner.prototype.test = function(pattern, index) {
+ index = index || 0;
+ index += this.__position;
+
+ if (index >= 0 && index < this.__input_length) {
+ return !!this.__match(pattern, index);
+ } else {
+ return false;
+ }
+};
+
+InputScanner.prototype.testChar = function(pattern, index) {
+ // test one character regex match
+ var val = this.peek(index);
+ pattern.lastIndex = 0;
+ return val !== null && pattern.test(val);
+};
+
+InputScanner.prototype.match = function(pattern) {
+ var pattern_match = this.__match(pattern, this.__position);
+ if (pattern_match) {
+ this.__position += pattern_match[0].length;
+ } else {
+ pattern_match = null;
+ }
+ return pattern_match;
+};
+
+InputScanner.prototype.read = function(starting_pattern, until_pattern, until_after) {
+ var val = '';
+ var match;
+ if (starting_pattern) {
+ match = this.match(starting_pattern);
+ if (match) {
+ val += match[0];
+ }
+ }
+ if (until_pattern && (match || !starting_pattern)) {
+ val += this.readUntil(until_pattern, until_after);
+ }
+ return val;
+};
+
+InputScanner.prototype.readUntil = function(pattern, until_after) {
+ var val = '';
+ var match_index = this.__position;
+ pattern.lastIndex = this.__position;
+ var pattern_match = pattern.exec(this.__input);
+ if (pattern_match) {
+ match_index = pattern_match.index;
+ if (until_after) {
+ match_index += pattern_match[0].length;
+ }
+ } else {
+ match_index = this.__input_length;
+ }
+
+ val = this.__input.substring(this.__position, match_index);
+ this.__position = match_index;
+ return val;
+};
+
+InputScanner.prototype.readUntilAfter = function(pattern) {
+ return this.readUntil(pattern, true);
+};
+
+InputScanner.prototype.get_regexp = function(pattern, match_from) {
+ var result = null;
+ var flags = 'g';
+ if (match_from && regexp_has_sticky) {
+ flags = 'y';
+ }
+ // strings are converted to regexp
+ if (typeof pattern === "string" && pattern !== '') {
+ // result = new RegExp(pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), flags);
+ result = new RegExp(pattern, flags);
+ } else if (pattern) {
+ result = new RegExp(pattern.source, flags);
+ }
+ return result;
+};
+
+InputScanner.prototype.get_literal_regexp = function(literal_string) {
+ return RegExp(literal_string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
+};
+
+/* css beautifier legacy helpers */
+InputScanner.prototype.peekUntilAfter = function(pattern) {
+ var start = this.__position;
+ var val = this.readUntilAfter(pattern);
+ this.__position = start;
+ return val;
+};
+
+InputScanner.prototype.lookBack = function(testVal) {
+ var start = this.__position - 1;
+ return start >= testVal.length && this.__input.substring(start - testVal.length, start)
+ .toLowerCase() === testVal;
+};
+
+module.exports.InputScanner = InputScanner;
+
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var InputScanner = __webpack_require__(8).InputScanner;
+var Token = __webpack_require__(3).Token;
+var TokenStream = __webpack_require__(10).TokenStream;
+var WhitespacePattern = __webpack_require__(11).WhitespacePattern;
+
+var TOKEN = {
+ START: 'TK_START',
+ RAW: 'TK_RAW',
+ EOF: 'TK_EOF'
+};
+
+var Tokenizer = function(input_string, options) {
+ this._input = new InputScanner(input_string);
+ this._options = options || {};
+ this.__tokens = null;
+
+ this._patterns = {};
+ this._patterns.whitespace = new WhitespacePattern(this._input);
+};
+
+Tokenizer.prototype.tokenize = function() {
+ this._input.restart();
+ this.__tokens = new TokenStream();
+
+ this._reset();
+
+ var current;
+ var previous = new Token(TOKEN.START, '');
+ var open_token = null;
+ var open_stack = [];
+ var comments = new TokenStream();
+
+ while (previous.type !== TOKEN.EOF) {
+ current = this._get_next_token(previous, open_token);
+ while (this._is_comment(current)) {
+ comments.add(current);
+ current = this._get_next_token(previous, open_token);
+ }
+
+ if (!comments.isEmpty()) {
+ current.comments_before = comments;
+ comments = new TokenStream();
+ }
+
+ current.parent = open_token;
+
+ if (this._is_opening(current)) {
+ open_stack.push(open_token);
+ open_token = current;
+ } else if (open_token && this._is_closing(current, open_token)) {
+ current.opened = open_token;
+ open_token.closed = current;
+ open_token = open_stack.pop();
+ current.parent = open_token;
+ }
+
+ current.previous = previous;
+ previous.next = current;
+
+ this.__tokens.add(current);
+ previous = current;
+ }
+
+ return this.__tokens;
+};
+
+
+Tokenizer.prototype._is_first_token = function() {
+ return this.__tokens.isEmpty();
+};
+
+Tokenizer.prototype._reset = function() {};
+
+Tokenizer.prototype._get_next_token = function(previous_token, open_token) { // jshint unused:false
+ this._readWhitespace();
+ var resulting_string = this._input.read(/.+/g);
+ if (resulting_string) {
+ return this._create_token(TOKEN.RAW, resulting_string);
+ } else {
+ return this._create_token(TOKEN.EOF, '');
+ }
+};
+
+Tokenizer.prototype._is_comment = function(current_token) { // jshint unused:false
+ return false;
+};
+
+Tokenizer.prototype._is_opening = function(current_token) { // jshint unused:false
+ return false;
+};
+
+Tokenizer.prototype._is_closing = function(current_token, open_token) { // jshint unused:false
+ return false;
+};
+
+Tokenizer.prototype._create_token = function(type, text) {
+ var token = new Token(type, text,
+ this._patterns.whitespace.newline_count,
+ this._patterns.whitespace.whitespace_before_token);
+ return token;
+};
+
+Tokenizer.prototype._readWhitespace = function() {
+ return this._patterns.whitespace.read();
+};
+
+
+
+module.exports.Tokenizer = Tokenizer;
+module.exports.TOKEN = TOKEN;
+
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function TokenStream(parent_token) {
+ // private
+ this.__tokens = [];
+ this.__tokens_length = this.__tokens.length;
+ this.__position = 0;
+ this.__parent_token = parent_token;
+}
+
+TokenStream.prototype.restart = function() {
+ this.__position = 0;
+};
+
+TokenStream.prototype.isEmpty = function() {
+ return this.__tokens_length === 0;
+};
+
+TokenStream.prototype.hasNext = function() {
+ return this.__position < this.__tokens_length;
+};
+
+TokenStream.prototype.next = function() {
+ var val = null;
+ if (this.hasNext()) {
+ val = this.__tokens[this.__position];
+ this.__position += 1;
+ }
+ return val;
+};
+
+TokenStream.prototype.peek = function(index) {
+ var val = null;
+ index = index || 0;
+ index += this.__position;
+ if (index >= 0 && index < this.__tokens_length) {
+ val = this.__tokens[index];
+ }
+ return val;
+};
+
+TokenStream.prototype.add = function(token) {
+ if (this.__parent_token) {
+ token.parent = this.__parent_token;
+ }
+ this.__tokens.push(token);
+ this.__tokens_length += 1;
+};
+
+module.exports.TokenStream = TokenStream;
+
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Pattern = __webpack_require__(12).Pattern;
+
+function WhitespacePattern(input_scanner, parent) {
+ Pattern.call(this, input_scanner, parent);
+ if (parent) {
+ this._line_regexp = this._input.get_regexp(parent._line_regexp);
+ } else {
+ this.__set_whitespace_patterns('', '');
+ }
+
+ this.newline_count = 0;
+ this.whitespace_before_token = '';
+}
+WhitespacePattern.prototype = new Pattern();
+
+WhitespacePattern.prototype.__set_whitespace_patterns = function(whitespace_chars, newline_chars) {
+ whitespace_chars += '\\t ';
+ newline_chars += '\\n\\r';
+
+ this._match_pattern = this._input.get_regexp(
+ '[' + whitespace_chars + newline_chars + ']+', true);
+ this._newline_regexp = this._input.get_regexp(
+ '\\r\\n|[' + newline_chars + ']');
+};
+
+WhitespacePattern.prototype.read = function() {
+ this.newline_count = 0;
+ this.whitespace_before_token = '';
+
+ var resulting_string = this._input.read(this._match_pattern);
+ if (resulting_string === ' ') {
+ this.whitespace_before_token = ' ';
+ } else if (resulting_string) {
+ var matches = this.__split(this._newline_regexp, resulting_string);
+ this.newline_count = matches.length - 1;
+ this.whitespace_before_token = matches[this.newline_count];
+ }
+
+ return resulting_string;
+};
+
+WhitespacePattern.prototype.matching = function(whitespace_chars, newline_chars) {
+ var result = this._create();
+ result.__set_whitespace_patterns(whitespace_chars, newline_chars);
+ result._update();
+ return result;
+};
+
+WhitespacePattern.prototype._create = function() {
+ return new WhitespacePattern(this._input, this);
+};
+
+WhitespacePattern.prototype.__split = function(regexp, input_string) {
+ regexp.lastIndex = 0;
+ var start_index = 0;
+ var result = [];
+ var next_match = regexp.exec(input_string);
+ while (next_match) {
+ result.push(input_string.substring(start_index, next_match.index));
+ start_index = next_match.index + next_match[0].length;
+ next_match = regexp.exec(input_string);
+ }
+
+ if (start_index < input_string.length) {
+ result.push(input_string.substring(start_index, input_string.length));
+ } else {
+ result.push('');
+ }
+
+ return result;
+};
+
+
+
+module.exports.WhitespacePattern = WhitespacePattern;
+
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Pattern(input_scanner, parent) {
+ this._input = input_scanner;
+ this._starting_pattern = null;
+ this._match_pattern = null;
+ this._until_pattern = null;
+ this._until_after = false;
+
+ if (parent) {
+ this._starting_pattern = this._input.get_regexp(parent._starting_pattern, true);
+ this._match_pattern = this._input.get_regexp(parent._match_pattern, true);
+ this._until_pattern = this._input.get_regexp(parent._until_pattern);
+ this._until_after = parent._until_after;
+ }
+}
+
+Pattern.prototype.read = function() {
+ var result = this._input.read(this._starting_pattern);
+ if (!this._starting_pattern || result) {
+ result += this._input.read(this._match_pattern, this._until_pattern, this._until_after);
+ }
+ return result;
+};
+
+Pattern.prototype.read_match = function() {
+ return this._input.match(this._match_pattern);
+};
+
+Pattern.prototype.until_after = function(pattern) {
+ var result = this._create();
+ result._until_after = true;
+ result._until_pattern = this._input.get_regexp(pattern);
+ result._update();
+ return result;
+};
+
+Pattern.prototype.until = function(pattern) {
+ var result = this._create();
+ result._until_after = false;
+ result._until_pattern = this._input.get_regexp(pattern);
+ result._update();
+ return result;
+};
+
+Pattern.prototype.starting_with = function(pattern) {
+ var result = this._create();
+ result._starting_pattern = this._input.get_regexp(pattern, true);
+ result._update();
+ return result;
+};
+
+Pattern.prototype.matching = function(pattern) {
+ var result = this._create();
+ result._match_pattern = this._input.get_regexp(pattern, true);
+ result._update();
+ return result;
+};
+
+Pattern.prototype._create = function() {
+ return new Pattern(this._input, this);
+};
+
+Pattern.prototype._update = function() {};
+
+module.exports.Pattern = Pattern;
+
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Directives(start_block_pattern, end_block_pattern) {
+ start_block_pattern = typeof start_block_pattern === 'string' ? start_block_pattern : start_block_pattern.source;
+ end_block_pattern = typeof end_block_pattern === 'string' ? end_block_pattern : end_block_pattern.source;
+ this.__directives_block_pattern = new RegExp(start_block_pattern + / beautify( \w+[:]\w+)+ /.source + end_block_pattern, 'g');
+ this.__directive_pattern = / (\w+)[:](\w+)/g;
+
+ this.__directives_end_ignore_pattern = new RegExp(start_block_pattern + /\sbeautify\signore:end\s/.source + end_block_pattern, 'g');
+}
+
+Directives.prototype.get_directives = function(text) {
+ if (!text.match(this.__directives_block_pattern)) {
+ return null;
+ }
+
+ var directives = {};
+ this.__directive_pattern.lastIndex = 0;
+ var directive_match = this.__directive_pattern.exec(text);
+
+ while (directive_match) {
+ directives[directive_match[1]] = directive_match[2];
+ directive_match = this.__directive_pattern.exec(text);
+ }
+
+ return directives;
+};
+
+Directives.prototype.readIgnored = function(input) {
+ return input.readUntilAfter(this.__directives_end_ignore_pattern);
+};
+
+
+module.exports.Directives = Directives;
+
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Pattern = __webpack_require__(12).Pattern;
+
+
+var template_names = {
+ django: false,
+ erb: false,
+ handlebars: false,
+ php: false
+};
+
+// This lets templates appear anywhere we would do a readUntil
+// The cost is higher but it is pay to play.
+function TemplatablePattern(input_scanner, parent) {
+ Pattern.call(this, input_scanner, parent);
+ this.__template_pattern = null;
+ this._disabled = Object.assign({}, template_names);
+ this._excluded = Object.assign({}, template_names);
+
+ if (parent) {
+ this.__template_pattern = this._input.get_regexp(parent.__template_pattern);
+ this._excluded = Object.assign(this._excluded, parent._excluded);
+ this._disabled = Object.assign(this._disabled, parent._disabled);
+ }
+ var pattern = new Pattern(input_scanner);
+ this.__patterns = {
+ handlebars_comment: pattern.starting_with(/{{!--/).until_after(/--}}/),
+ handlebars_unescaped: pattern.starting_with(/{{{/).until_after(/}}}/),
+ handlebars: pattern.starting_with(/{{/).until_after(/}}/),
+ php: pattern.starting_with(/<\?(?:[=]|php)/).until_after(/\?>/),
+ erb: pattern.starting_with(/<%[^%]/).until_after(/[^%]%>/),
+ // django coflicts with handlebars a bit.
+ django: pattern.starting_with(/{%/).until_after(/%}/),
+ django_value: pattern.starting_with(/{{/).until_after(/}}/),
+ django_comment: pattern.starting_with(/{#/).until_after(/#}/)
+ };
+}
+TemplatablePattern.prototype = new Pattern();
+
+TemplatablePattern.prototype._create = function() {
+ return new TemplatablePattern(this._input, this);
+};
+
+TemplatablePattern.prototype._update = function() {
+ this.__set_templated_pattern();
+};
+
+TemplatablePattern.prototype.disable = function(language) {
+ var result = this._create();
+ result._disabled[language] = true;
+ result._update();
+ return result;
+};
+
+TemplatablePattern.prototype.read_options = function(options) {
+ var result = this._create();
+ for (var language in template_names) {
+ result._disabled[language] = options.templating.indexOf(language) === -1;
+ }
+ result._update();
+ return result;
+};
+
+TemplatablePattern.prototype.exclude = function(language) {
+ var result = this._create();
+ result._excluded[language] = true;
+ result._update();
+ return result;
+};
+
+TemplatablePattern.prototype.read = function() {
+ var result = '';
+ if (this._match_pattern) {
+ result = this._input.read(this._starting_pattern);
+ } else {
+ result = this._input.read(this._starting_pattern, this.__template_pattern);
+ }
+ var next = this._read_template();
+ while (next) {
+ if (this._match_pattern) {
+ next += this._input.read(this._match_pattern);
+ } else {
+ next += this._input.readUntil(this.__template_pattern);
+ }
+ result += next;
+ next = this._read_template();
+ }
+
+ if (this._until_after) {
+ result += this._input.readUntilAfter(this._until_pattern);
+ }
+ return result;
+};
+
+TemplatablePattern.prototype.__set_templated_pattern = function() {
+ var items = [];
+
+ if (!this._disabled.php) {
+ items.push(this.__patterns.php._starting_pattern.source);
+ }
+ if (!this._disabled.handlebars) {
+ items.push(this.__patterns.handlebars._starting_pattern.source);
+ }
+ if (!this._disabled.erb) {
+ items.push(this.__patterns.erb._starting_pattern.source);
+ }
+ if (!this._disabled.django) {
+ items.push(this.__patterns.django._starting_pattern.source);
+ items.push(this.__patterns.django_value._starting_pattern.source);
+ items.push(this.__patterns.django_comment._starting_pattern.source);
+ }
+
+ if (this._until_pattern) {
+ items.push(this._until_pattern.source);
+ }
+ this.__template_pattern = this._input.get_regexp('(?:' + items.join('|') + ')');
+};
+
+TemplatablePattern.prototype._read_template = function() {
+ var resulting_string = '';
+ var c = this._input.peek();
+ if (c === '<') {
+ var peek1 = this._input.peek(1);
+ //if we're in a comment, do something special
+ // We treat all comments as literals, even more than preformatted tags
+ // we just look for the appropriate close tag
+ if (!this._disabled.php && !this._excluded.php && peek1 === '?') {
+ resulting_string = resulting_string ||
+ this.__patterns.php.read();
+ }
+ if (!this._disabled.erb && !this._excluded.erb && peek1 === '%') {
+ resulting_string = resulting_string ||
+ this.__patterns.erb.read();
+ }
+ } else if (c === '{') {
+ if (!this._disabled.handlebars && !this._excluded.handlebars) {
+ resulting_string = resulting_string ||
+ this.__patterns.handlebars_comment.read();
+ resulting_string = resulting_string ||
+ this.__patterns.handlebars_unescaped.read();
+ resulting_string = resulting_string ||
+ this.__patterns.handlebars.read();
+ }
+ if (!this._disabled.django) {
+ // django coflicts with handlebars a bit.
+ if (!this._excluded.django && !this._excluded.handlebars) {
+ resulting_string = resulting_string ||
+ this.__patterns.django_value.read();
+ }
+ if (!this._excluded.django) {
+ resulting_string = resulting_string ||
+ this.__patterns.django_comment.read();
+ resulting_string = resulting_string ||
+ this.__patterns.django.read();
+ }
+ }
+ }
+ return resulting_string;
+};
+
+
+module.exports.TemplatablePattern = TemplatablePattern;
+
+
+/***/ }),
+/* 15 */,
+/* 16 */,
+/* 17 */,
+/* 18 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Beautifier = __webpack_require__(19).Beautifier,
+ Options = __webpack_require__(20).Options;
+
+function style_html(html_source, options, js_beautify, css_beautify) {
+ var beautifier = new Beautifier(html_source, options, js_beautify, css_beautify);
+ return beautifier.beautify();
+}
+
+module.exports = style_html;
+module.exports.defaultOptions = function() {
+ return new Options();
+};
+
+
+/***/ }),
+/* 19 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Options = __webpack_require__(20).Options;
+var Output = __webpack_require__(2).Output;
+var Tokenizer = __webpack_require__(21).Tokenizer;
+var TOKEN = __webpack_require__(21).TOKEN;
+
+var lineBreak = /\r\n|[\r\n]/;
+var allLineBreaks = /\r\n|[\r\n]/g;
+
+var Printer = function(options, base_indent_string) { //handles input/output and some other printing functions
+
+ this.indent_level = 0;
+ this.alignment_size = 0;
+ this.max_preserve_newlines = options.max_preserve_newlines;
+ this.preserve_newlines = options.preserve_newlines;
+
+ this._output = new Output(options, base_indent_string);
+
+};
+
+Printer.prototype.current_line_has_match = function(pattern) {
+ return this._output.current_line.has_match(pattern);
+};
+
+Printer.prototype.set_space_before_token = function(value, non_breaking) {
+ this._output.space_before_token = value;
+ this._output.non_breaking_space = non_breaking;
+};
+
+Printer.prototype.set_wrap_point = function() {
+ this._output.set_indent(this.indent_level, this.alignment_size);
+ this._output.set_wrap_point();
+};
+
+
+Printer.prototype.add_raw_token = function(token) {
+ this._output.add_raw_token(token);
+};
+
+Printer.prototype.print_preserved_newlines = function(raw_token) {
+ var newlines = 0;
+ if (raw_token.type !== TOKEN.TEXT && raw_token.previous.type !== TOKEN.TEXT) {
+ newlines = raw_token.newlines ? 1 : 0;
+ }
+
+ if (this.preserve_newlines) {
+ newlines = raw_token.newlines < this.max_preserve_newlines + 1 ? raw_token.newlines : this.max_preserve_newlines + 1;
+ }
+ for (var n = 0; n < newlines; n++) {
+ this.print_newline(n > 0);
+ }
+
+ return newlines !== 0;
+};
+
+Printer.prototype.traverse_whitespace = function(raw_token) {
+ if (raw_token.whitespace_before || raw_token.newlines) {
+ if (!this.print_preserved_newlines(raw_token)) {
+ this._output.space_before_token = true;
+ }
+ return true;
+ }
+ return false;
+};
+
+Printer.prototype.previous_token_wrapped = function() {
+ return this._output.previous_token_wrapped;
+};
+
+Printer.prototype.print_newline = function(force) {
+ this._output.add_new_line(force);
+};
+
+Printer.prototype.print_token = function(token) {
+ if (token.text) {
+ this._output.set_indent(this.indent_level, this.alignment_size);
+ this._output.add_token(token.text);
+ }
+};
+
+Printer.prototype.indent = function() {
+ this.indent_level++;
+};
+
+Printer.prototype.get_full_indent = function(level) {
+ level = this.indent_level + (level || 0);
+ if (level < 1) {
+ return '';
+ }
+
+ return this._output.get_indent_string(level);
+};
+
+var get_type_attribute = function(start_token) {
+ var result = null;
+ var raw_token = start_token.next;
+
+ // Search attributes for a type attribute
+ while (raw_token.type !== TOKEN.EOF && start_token.closed !== raw_token) {
+ if (raw_token.type === TOKEN.ATTRIBUTE && raw_token.text === 'type') {
+ if (raw_token.next && raw_token.next.type === TOKEN.EQUALS &&
+ raw_token.next.next && raw_token.next.next.type === TOKEN.VALUE) {
+ result = raw_token.next.next.text;
+ }
+ break;
+ }
+ raw_token = raw_token.next;
+ }
+
+ return result;
+};
+
+var get_custom_beautifier_name = function(tag_check, raw_token) {
+ var typeAttribute = null;
+ var result = null;
+
+ if (!raw_token.closed) {
+ return null;
+ }
+
+ if (tag_check === 'script') {
+ typeAttribute = 'text/javascript';
+ } else if (tag_check === 'style') {
+ typeAttribute = 'text/css';
+ }
+
+ typeAttribute = get_type_attribute(raw_token) || typeAttribute;
+
+ // For script and style tags that have a type attribute, only enable custom beautifiers for matching values
+ // For those without a type attribute use default;
+ if (typeAttribute.search('text/css') > -1) {
+ result = 'css';
+ } else if (typeAttribute.search(/module|((text|application|dojo)\/(x-)?(javascript|ecmascript|jscript|livescript|(ld\+)?json|method|aspect))/) > -1) {
+ result = 'javascript';
+ } else if (typeAttribute.search(/(text|application|dojo)\/(x-)?(html)/) > -1) {
+ result = 'html';
+ } else if (typeAttribute.search(/test\/null/) > -1) {
+ // Test only mime-type for testing the beautifier when null is passed as beautifing function
+ result = 'null';
+ }
+
+ return result;
+};
+
+function in_array(what, arr) {
+ return arr.indexOf(what) !== -1;
+}
+
+function TagFrame(parent, parser_token, indent_level) {
+ this.parent = parent || null;
+ this.tag = parser_token ? parser_token.tag_name : '';
+ this.indent_level = indent_level || 0;
+ this.parser_token = parser_token || null;
+}
+
+function TagStack(printer) {
+ this._printer = printer;
+ this._current_frame = null;
+}
+
+TagStack.prototype.get_parser_token = function() {
+ return this._current_frame ? this._current_frame.parser_token : null;
+};
+
+TagStack.prototype.record_tag = function(parser_token) { //function to record a tag and its parent in this.tags Object
+ var new_frame = new TagFrame(this._current_frame, parser_token, this._printer.indent_level);
+ this._current_frame = new_frame;
+};
+
+TagStack.prototype._try_pop_frame = function(frame) { //function to retrieve the opening tag to the corresponding closer
+ var parser_token = null;
+
+ if (frame) {
+ parser_token = frame.parser_token;
+ this._printer.indent_level = frame.indent_level;
+ this._current_frame = frame.parent;
+ }
+
+ return parser_token;
+};
+
+TagStack.prototype._get_frame = function(tag_list, stop_list) { //function to retrieve the opening tag to the corresponding closer
+ var frame = this._current_frame;
+
+ while (frame) { //till we reach '' (the initial value);
+ if (tag_list.indexOf(frame.tag) !== -1) { //if this is it use it
+ break;
+ } else if (stop_list && stop_list.indexOf(frame.tag) !== -1) {
+ frame = null;
+ break;
+ }
+ frame = frame.parent;
+ }
+
+ return frame;
+};
+
+TagStack.prototype.try_pop = function(tag, stop_list) { //function to retrieve the opening tag to the corresponding closer
+ var frame = this._get_frame([tag], stop_list);
+ return this._try_pop_frame(frame);
+};
+
+TagStack.prototype.indent_to_tag = function(tag_list) {
+ var frame = this._get_frame(tag_list);
+ if (frame) {
+ this._printer.indent_level = frame.indent_level;
+ }
+};
+
+function Beautifier(source_text, options, js_beautify, css_beautify) {
+ //Wrapper function to invoke all the necessary constructors and deal with the output.
+ this._source_text = source_text || '';
+ options = options || {};
+ this._js_beautify = js_beautify;
+ this._css_beautify = css_beautify;
+ this._tag_stack = null;
+
+ // Allow the setting of language/file-type specific options
+ // with inheritance of overall settings
+ var optionHtml = new Options(options, 'html');
+
+ this._options = optionHtml;
+
+ this._is_wrap_attributes_force = this._options.wrap_attributes.substr(0, 'force'.length) === 'force';
+ this._is_wrap_attributes_force_expand_multiline = (this._options.wrap_attributes === 'force-expand-multiline');
+ this._is_wrap_attributes_force_aligned = (this._options.wrap_attributes === 'force-aligned');
+ this._is_wrap_attributes_aligned_multiple = (this._options.wrap_attributes === 'aligned-multiple');
+ this._is_wrap_attributes_preserve = this._options.wrap_attributes.substr(0, 'preserve'.length) === 'preserve';
+ this._is_wrap_attributes_preserve_aligned = (this._options.wrap_attributes === 'preserve-aligned');
+}
+
+Beautifier.prototype.beautify = function() {
+
+ // if disabled, return the input unchanged.
+ if (this._options.disabled) {
+ return this._source_text;
+ }
+
+ var source_text = this._source_text;
+ var eol = this._options.eol;
+ if (this._options.eol === 'auto') {
+ eol = '\n';
+ if (source_text && lineBreak.test(source_text)) {
+ eol = source_text.match(lineBreak)[0];
+ }
+ }
+
+ // HACK: newline parsing inconsistent. This brute force normalizes the input.
+ source_text = source_text.replace(allLineBreaks, '\n');
+
+ var baseIndentString = source_text.match(/^[\t ]*/)[0];
+
+ var last_token = {
+ text: '',
+ type: ''
+ };
+
+ var last_tag_token = new TagOpenParserToken();
+
+ var printer = new Printer(this._options, baseIndentString);
+ var tokens = new Tokenizer(source_text, this._options).tokenize();
+
+ this._tag_stack = new TagStack(printer);
+
+ var parser_token = null;
+ var raw_token = tokens.next();
+ while (raw_token.type !== TOKEN.EOF) {
+
+ if (raw_token.type === TOKEN.TAG_OPEN || raw_token.type === TOKEN.COMMENT) {
+ parser_token = this._handle_tag_open(printer, raw_token, last_tag_token, last_token);
+ last_tag_token = parser_token;
+ } else if ((raw_token.type === TOKEN.ATTRIBUTE || raw_token.type === TOKEN.EQUALS || raw_token.type === TOKEN.VALUE) ||
+ (raw_token.type === TOKEN.TEXT && !last_tag_token.tag_complete)) {
+ parser_token = this._handle_inside_tag(printer, raw_token, last_tag_token, tokens);
+ } else if (raw_token.type === TOKEN.TAG_CLOSE) {
+ parser_token = this._handle_tag_close(printer, raw_token, last_tag_token);
+ } else if (raw_token.type === TOKEN.TEXT) {
+ parser_token = this._handle_text(printer, raw_token, last_tag_token);
+ } else {
+ // This should never happen, but if it does. Print the raw token
+ printer.add_raw_token(raw_token);
+ }
+
+ last_token = parser_token;
+
+ raw_token = tokens.next();
+ }
+ var sweet_code = printer._output.get_code(eol);
+
+ return sweet_code;
+};
+
+Beautifier.prototype._handle_tag_close = function(printer, raw_token, last_tag_token) {
+ var parser_token = {
+ text: raw_token.text,
+ type: raw_token.type
+ };
+ printer.alignment_size = 0;
+ last_tag_token.tag_complete = true;
+
+ printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
+ if (last_tag_token.is_unformatted) {
+ printer.add_raw_token(raw_token);
+ } else {
+ if (last_tag_token.tag_start_char === '<') {
+ printer.set_space_before_token(raw_token.text[0] === '/', true); // space before />, no space before >
+ if (this._is_wrap_attributes_force_expand_multiline && last_tag_token.has_wrapped_attrs) {
+ printer.print_newline(false);
+ }
+ }
+ printer.print_token(raw_token);
+
+ }
+
+ if (last_tag_token.indent_content &&
+ !(last_tag_token.is_unformatted || last_tag_token.is_content_unformatted)) {
+ printer.indent();
+
+ // only indent once per opened tag
+ last_tag_token.indent_content = false;
+ }
+
+ if (!last_tag_token.is_inline_element &&
+ !(last_tag_token.is_unformatted || last_tag_token.is_content_unformatted)) {
+ printer.set_wrap_point();
+ }
+
+ return parser_token;
+};
+
+Beautifier.prototype._handle_inside_tag = function(printer, raw_token, last_tag_token, tokens) {
+ var wrapped = last_tag_token.has_wrapped_attrs;
+ var parser_token = {
+ text: raw_token.text,
+ type: raw_token.type
+ };
+
+ printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
+ if (last_tag_token.is_unformatted) {
+ printer.add_raw_token(raw_token);
+ } else if (last_tag_token.tag_start_char === '{' && raw_token.type === TOKEN.TEXT) {
+ // For the insides of handlebars allow newlines or a single space between open and contents
+ if (printer.print_preserved_newlines(raw_token)) {
+ raw_token.newlines = 0;
+ printer.add_raw_token(raw_token);
+ } else {
+ printer.print_token(raw_token);
+ }
+ } else {
+ if (raw_token.type === TOKEN.ATTRIBUTE) {
+ printer.set_space_before_token(true);
+ last_tag_token.attr_count += 1;
+ } else if (raw_token.type === TOKEN.EQUALS) { //no space before =
+ printer.set_space_before_token(false);
+ } else if (raw_token.type === TOKEN.VALUE && raw_token.previous.type === TOKEN.EQUALS) { //no space before value
+ printer.set_space_before_token(false);
+ }
+
+ if (raw_token.type === TOKEN.ATTRIBUTE && last_tag_token.tag_start_char === '<') {
+ if (this._is_wrap_attributes_preserve || this._is_wrap_attributes_preserve_aligned) {
+ printer.traverse_whitespace(raw_token);
+ wrapped = wrapped || raw_token.newlines !== 0;
+ }
+
+
+ if (this._is_wrap_attributes_force) {
+ var force_attr_wrap = last_tag_token.attr_count > 1;
+ if (this._is_wrap_attributes_force_expand_multiline && last_tag_token.attr_count === 1) {
+ var is_only_attribute = true;
+ var peek_index = 0;
+ var peek_token;
+ do {
+ peek_token = tokens.peek(peek_index);
+ if (peek_token.type === TOKEN.ATTRIBUTE) {
+ is_only_attribute = false;
+ break;
+ }
+ peek_index += 1;
+ } while (peek_index < 4 && peek_token.type !== TOKEN.EOF && peek_token.type !== TOKEN.TAG_CLOSE);
+
+ force_attr_wrap = !is_only_attribute;
+ }
+
+ if (force_attr_wrap) {
+ printer.print_newline(false);
+ wrapped = true;
+ }
+ }
+ }
+ printer.print_token(raw_token);
+ wrapped = wrapped || printer.previous_token_wrapped();
+ last_tag_token.has_wrapped_attrs = wrapped;
+ }
+ return parser_token;
+};
+
+Beautifier.prototype._handle_text = function(printer, raw_token, last_tag_token) {
+ var parser_token = {
+ text: raw_token.text,
+ type: 'TK_CONTENT'
+ };
+ if (last_tag_token.custom_beautifier_name) { //check if we need to format javascript
+ this._print_custom_beatifier_text(printer, raw_token, last_tag_token);
+ } else if (last_tag_token.is_unformatted || last_tag_token.is_content_unformatted) {
+ printer.add_raw_token(raw_token);
+ } else {
+ printer.traverse_whitespace(raw_token);
+ printer.print_token(raw_token);
+ }
+ return parser_token;
+};
+
+Beautifier.prototype._print_custom_beatifier_text = function(printer, raw_token, last_tag_token) {
+ var local = this;
+ if (raw_token.text !== '') {
+
+ var text = raw_token.text,
+ _beautifier,
+ script_indent_level = 1,
+ pre = '',
+ post = '';
+ if (last_tag_token.custom_beautifier_name === 'javascript' && typeof this._js_beautify === 'function') {
+ _beautifier = this._js_beautify;
+ } else if (last_tag_token.custom_beautifier_name === 'css' && typeof this._css_beautify === 'function') {
+ _beautifier = this._css_beautify;
+ } else if (last_tag_token.custom_beautifier_name === 'html') {
+ _beautifier = function(html_source, options) {
+ var beautifier = new Beautifier(html_source, options, local._js_beautify, local._css_beautify);
+ return beautifier.beautify();
+ };
+ }
+
+ if (this._options.indent_scripts === "keep") {
+ script_indent_level = 0;
+ } else if (this._options.indent_scripts === "separate") {
+ script_indent_level = -printer.indent_level;
+ }
+
+ var indentation = printer.get_full_indent(script_indent_level);
+
+ // if there is at least one empty line at the end of this text, strip it
+ // we'll be adding one back after the text but before the containing tag.
+ text = text.replace(/\n[ \t]*$/, '');
+
+ // Handle the case where content is wrapped in a comment or cdata.
+ if (last_tag_token.custom_beautifier_name !== 'html' &&
+ text[0] === '<' && text.match(/^(<!--|<!\[CDATA\[)/)) {
+ var matched = /^(<!--[^\n]*|<!\[CDATA\[)(\n?)([ \t\n]*)([\s\S]*)(-->|]]>)$/.exec(text);
+
+ // if we start to wrap but don't finish, print raw
+ if (!matched) {
+ printer.add_raw_token(raw_token);
+ return;
+ }
+
+ pre = indentation + matched[1] + '\n';
+ text = matched[4];
+ if (matched[5]) {
+ post = indentation + matched[5];
+ }
+
+ // if there is at least one empty line at the end of this text, strip it
+ // we'll be adding one back after the text but before the containing tag.
+ text = text.replace(/\n[ \t]*$/, '');
+
+ if (matched[2] || matched[3].indexOf('\n') !== -1) {
+ // if the first line of the non-comment text has spaces
+ // use that as the basis for indenting in null case.
+ matched = matched[3].match(/[ \t]+$/);
+ if (matched) {
+ raw_token.whitespace_before = matched[0];
+ }
+ }
+ }
+
+ if (text) {
+ if (_beautifier) {
+
+ // call the Beautifier if avaliable
+ var Child_options = function() {
+ this.eol = '\n';
+ };
+ Child_options.prototype = this._options.raw_options;
+ var child_options = new Child_options();
+ text = _beautifier(indentation + text, child_options);
+ } else {
+ // simply indent the string otherwise
+ var white = raw_token.whitespace_before;
+ if (white) {
+ text = text.replace(new RegExp('\n(' + white + ')?', 'g'), '\n');
+ }
+
+ text = indentation + text.replace(/\n/g, '\n' + indentation);
+ }
+ }
+
+ if (pre) {
+ if (!text) {
+ text = pre + post;
+ } else {
+ text = pre + text + '\n' + post;
+ }
+ }
+
+ printer.print_newline(false);
+ if (text) {
+ raw_token.text = text;
+ raw_token.whitespace_before = '';
+ raw_token.newlines = 0;
+ printer.add_raw_token(raw_token);
+ printer.print_newline(true);
+ }
+ }
+};
+
+Beautifier.prototype._handle_tag_open = function(printer, raw_token, last_tag_token, last_token) {
+ var parser_token = this._get_tag_open_token(raw_token);
+
+ if ((last_tag_token.is_unformatted || last_tag_token.is_content_unformatted) &&
+ !last_tag_token.is_empty_element &&
+ raw_token.type === TOKEN.TAG_OPEN && raw_token.text.indexOf('</') === 0) {
+ // End element tags for unformatted or content_unformatted elements
+ // are printed raw to keep any newlines inside them exactly the same.
+ printer.add_raw_token(raw_token);
+ parser_token.start_tag_token = this._tag_stack.try_pop(parser_token.tag_name);
+ } else {
+ printer.traverse_whitespace(raw_token);
+ this._set_tag_position(printer, raw_token, parser_token, last_tag_token, last_token);
+ if (!parser_token.is_inline_element) {
+ printer.set_wrap_point();
+ }
+ printer.print_token(raw_token);
+ }
+
+ //indent attributes an auto, forced, aligned or forced-align line-wrap
+ if (this._is_wrap_attributes_force_aligned || this._is_wrap_attributes_aligned_multiple || this._is_wrap_attributes_preserve_aligned) {
+ parser_token.alignment_size = raw_token.text.length + 1;
+ }
+
+ if (!parser_token.tag_complete && !parser_token.is_unformatted) {
+ printer.alignment_size = parser_token.alignment_size;
+ }
+
+ return parser_token;
+};
+
+var TagOpenParserToken = function(parent, raw_token) {
+ this.parent = parent || null;
+ this.text = '';
+ this.type = 'TK_TAG_OPEN';
+ this.tag_name = '';
+ this.is_inline_element = false;
+ this.is_unformatted = false;
+ this.is_content_unformatted = false;
+ this.is_empty_element = false;
+ this.is_start_tag = false;
+ this.is_end_tag = false;
+ this.indent_content = false;
+ this.multiline_content = false;
+ this.custom_beautifier_name = null;
+ this.start_tag_token = null;
+ this.attr_count = 0;
+ this.has_wrapped_attrs = false;
+ this.alignment_size = 0;
+ this.tag_complete = false;
+ this.tag_start_char = '';
+ this.tag_check = '';
+
+ if (!raw_token) {
+ this.tag_complete = true;
+ } else {
+ var tag_check_match;
+
+ this.tag_start_char = raw_token.text[0];
+ this.text = raw_token.text;
+
+ if (this.tag_start_char === '<') {
+ tag_check_match = raw_token.text.match(/^<([^\s>]*)/);
+ this.tag_check = tag_check_match ? tag_check_match[1] : '';
+ } else {
+ tag_check_match = raw_token.text.match(/^{{(?:[\^]|#\*?)?([^\s}]+)/);
+ this.tag_check = tag_check_match ? tag_check_match[1] : '';
+
+ // handle "{{#> myPartial}}
+ if (raw_token.text === '{{#>' && this.tag_check === '>' && raw_token.next !== null) {
+ this.tag_check = raw_token.next.text;
+ }
+ }
+ this.tag_check = this.tag_check.toLowerCase();
+
+ if (raw_token.type === TOKEN.COMMENT) {
+ this.tag_complete = true;
+ }
+
+ this.is_start_tag = this.tag_check.charAt(0) !== '/';
+ this.tag_name = !this.is_start_tag ? this.tag_check.substr(1) : this.tag_check;
+ this.is_end_tag = !this.is_start_tag ||
+ (raw_token.closed && raw_token.closed.text === '/>');
+
+ // handlebars tags that don't start with # or ^ are single_tags, and so also start and end.
+ this.is_end_tag = this.is_end_tag ||
+ (this.tag_start_char === '{' && (this.text.length < 3 || (/[^#\^]/.test(this.text.charAt(2)))));
+ }
+};
+
+Beautifier.prototype._get_tag_open_token = function(raw_token) { //function to get a full tag and parse its type
+ var parser_token = new TagOpenParserToken(this._tag_stack.get_parser_token(), raw_token);
+
+ parser_token.alignment_size = this._options.wrap_attributes_indent_size;
+
+ parser_token.is_end_tag = parser_token.is_end_tag ||
+ in_array(parser_token.tag_check, this._options.void_elements);
+
+ parser_token.is_empty_element = parser_token.tag_complete ||
+ (parser_token.is_start_tag && parser_token.is_end_tag);
+
+ parser_token.is_unformatted = !parser_token.tag_complete && in_array(parser_token.tag_check, this._options.unformatted);
+ parser_token.is_content_unformatted = !parser_token.is_empty_element && in_array(parser_token.tag_check, this._options.content_unformatted);
+ parser_token.is_inline_element = in_array(parser_token.tag_name, this._options.inline) || parser_token.tag_start_char === '{';
+
+ return parser_token;
+};
+
+Beautifier.prototype._set_tag_position = function(printer, raw_token, parser_token, last_tag_token, last_token) {
+
+ if (!parser_token.is_empty_element) {
+ if (parser_token.is_end_tag) { //this tag is a double tag so check for tag-ending
+ parser_token.start_tag_token = this._tag_stack.try_pop(parser_token.tag_name); //remove it and all ancestors
+ } else { // it's a start-tag
+ // check if this tag is starting an element that has optional end element
+ // and do an ending needed
+ if (this._do_optional_end_element(parser_token)) {
+ if (!parser_token.is_inline_element) {
+ printer.print_newline(false);
+ }
+ }
+
+ this._tag_stack.record_tag(parser_token); //push it on the tag stack
+
+ if ((parser_token.tag_name === 'script' || parser_token.tag_name === 'style') &&
+ !(parser_token.is_unformatted || parser_token.is_content_unformatted)) {
+ parser_token.custom_beautifier_name = get_custom_beautifier_name(parser_token.tag_check, raw_token);
+ }
+ }
+ }
+
+ if (in_array(parser_token.tag_check, this._options.extra_liners)) { //check if this double needs an extra line
+ printer.print_newline(false);
+ if (!printer._output.just_added_blankline()) {
+ printer.print_newline(true);
+ }
+ }
+
+ if (parser_token.is_empty_element) { //if this tag name is a single tag type (either in the list or has a closing /)
+
+ // if you hit an else case, reset the indent level if you are inside an:
+ // 'if', 'unless', or 'each' block.
+ if (parser_token.tag_start_char === '{' && parser_token.tag_check === 'else') {
+ this._tag_stack.indent_to_tag(['if', 'unless', 'each']);
+ parser_token.indent_content = true;
+ // Don't add a newline if opening {{#if}} tag is on the current line
+ var foundIfOnCurrentLine = printer.current_line_has_match(/{{#if/);
+ if (!foundIfOnCurrentLine) {
+ printer.print_newline(false);
+ }
+ }
+
+ // Don't add a newline before elements that should remain where they are.
+ if (parser_token.tag_name === '!--' && last_token.type === TOKEN.TAG_CLOSE &&
+ last_tag_token.is_end_tag && parser_token.text.indexOf('\n') === -1) {
+ //Do nothing. Leave comments on same line.
+ } else {
+ if (!(parser_token.is_inline_element || parser_token.is_unformatted)) {
+ printer.print_newline(false);
+ }
+ this._calcluate_parent_multiline(printer, parser_token);
+ }
+ } else if (parser_token.is_end_tag) { //this tag is a double tag so check for tag-ending
+ var do_end_expand = false;
+
+ // deciding whether a block is multiline should not be this hard
+ do_end_expand = parser_token.start_tag_token && parser_token.start_tag_token.multiline_content;
+ do_end_expand = do_end_expand || (!parser_token.is_inline_element &&
+ !(last_tag_token.is_inline_element || last_tag_token.is_unformatted) &&
+ !(last_token.type === TOKEN.TAG_CLOSE && parser_token.start_tag_token === last_tag_token) &&
+ last_token.type !== 'TK_CONTENT'
+ );
+
+ if (parser_token.is_content_unformatted || parser_token.is_unformatted) {
+ do_end_expand = false;
+ }
+
+ if (do_end_expand) {
+ printer.print_newline(false);
+ }
+ } else { // it's a start-tag
+ parser_token.indent_content = !parser_token.custom_beautifier_name;
+
+ if (parser_token.tag_start_char === '<') {
+ if (parser_token.tag_name === 'html') {
+ parser_token.indent_content = this._options.indent_inner_html;
+ } else if (parser_token.tag_name === 'head') {
+ parser_token.indent_content = this._options.indent_head_inner_html;
+ } else if (parser_token.tag_name === 'body') {
+ parser_token.indent_content = this._options.indent_body_inner_html;
+ }
+ }
+
+ if (!(parser_token.is_inline_element || parser_token.is_unformatted) &&
+ (last_token.type !== 'TK_CONTENT' || parser_token.is_content_unformatted)) {
+ printer.print_newline(false);
+ }
+
+ this._calcluate_parent_multiline(printer, parser_token);
+ }
+};
+
+Beautifier.prototype._calcluate_parent_multiline = function(printer, parser_token) {
+ if (parser_token.parent && printer._output.just_added_newline() &&
+ !((parser_token.is_inline_element || parser_token.is_unformatted) && parser_token.parent.is_inline_element)) {
+ parser_token.parent.multiline_content = true;
+ }
+};
+
+//To be used for <p> tag special case:
+var p_closers = ['address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'];
+var p_parent_excludes = ['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video'];
+
+Beautifier.prototype._do_optional_end_element = function(parser_token) {
+ var result = null;
+ // NOTE: cases of "if there is no more content in the parent element"
+ // are handled automatically by the beautifier.
+ // It assumes parent or ancestor close tag closes all children.
+ // https://www.w3.org/TR/html5/syntax.html#optional-tags
+ if (parser_token.is_empty_element || !parser_token.is_start_tag || !parser_token.parent) {
+ return;
+
+ }
+
+ if (parser_token.tag_name === 'body') {
+ // A head element’s end tag may be omitted if the head element is not immediately followed by a space character or a comment.
+ result = result || this._tag_stack.try_pop('head');
+
+ //} else if (parser_token.tag_name === 'body') {
+ // DONE: A body element’s end tag may be omitted if the body element is not immediately followed by a comment.
+
+ } else if (parser_token.tag_name === 'li') {
+ // An li element’s end tag may be omitted if the li element is immediately followed by another li element or if there is no more content in the parent element.
+ result = result || this._tag_stack.try_pop('li', ['ol', 'ul']);
+
+ } else if (parser_token.tag_name === 'dd' || parser_token.tag_name === 'dt') {
+ // A dd element’s end tag may be omitted if the dd element is immediately followed by another dd element or a dt element, or if there is no more content in the parent element.
+ // A dt element’s end tag may be omitted if the dt element is immediately followed by another dt element or a dd element.
+ result = result || this._tag_stack.try_pop('dt', ['dl']);
+ result = result || this._tag_stack.try_pop('dd', ['dl']);
+
+
+ } else if (parser_token.parent.tag_name === 'p' && p_closers.indexOf(parser_token.tag_name) !== -1) {
+ // IMPORTANT: this else-if works because p_closers has no overlap with any other element we look for in this method
+ // check for the parent element is an HTML element that is not an <a>, <audio>, <del>, <ins>, <map>, <noscript>, or <video> element, or an autonomous custom element.
+ // To do this right, this needs to be coded as an inclusion of the inverse of the exclusion above.
+ // But to start with (if we ignore "autonomous custom elements") the exclusion would be fine.
+ var p_parent = parser_token.parent.parent;
+ if (!p_parent || p_parent_excludes.indexOf(p_parent.tag_name) === -1) {
+ result = result || this._tag_stack.try_pop('p');
+ }
+ } else if (parser_token.tag_name === 'rp' || parser_token.tag_name === 'rt') {
+ // An rt element’s end tag may be omitted if the rt element is immediately followed by an rt or rp element, or if there is no more content in the parent element.
+ // An rp element’s end tag may be omitted if the rp element is immediately followed by an rt or rp element, or if there is no more content in the parent element.
+ result = result || this._tag_stack.try_pop('rt', ['ruby', 'rtc']);
+ result = result || this._tag_stack.try_pop('rp', ['ruby', 'rtc']);
+
+ } else if (parser_token.tag_name === 'optgroup') {
+ // An optgroup element’s end tag may be omitted if the optgroup element is immediately followed by another optgroup element, or if there is no more content in the parent element.
+ // An option element’s end tag may be omitted if the option element is immediately followed by another option element, or if it is immediately followed by an optgroup element, or if there is no more content in the parent element.
+ result = result || this._tag_stack.try_pop('optgroup', ['select']);
+ //result = result || this._tag_stack.try_pop('option', ['select']);
+
+ } else if (parser_token.tag_name === 'option') {
+ // An option element’s end tag may be omitted if the option element is immediately followed by another option element, or if it is immediately followed by an optgroup element, or if there is no more content in the parent element.
+ result = result || this._tag_stack.try_pop('option', ['select', 'datalist', 'optgroup']);
+
+ } else if (parser_token.tag_name === 'colgroup') {
+ // DONE: A colgroup element’s end tag may be omitted if the colgroup element is not immediately followed by a space character or a comment.
+ // A caption element's end tag may be ommitted if a colgroup, thead, tfoot, tbody, or tr element is started.
+ result = result || this._tag_stack.try_pop('caption', ['table']);
+
+ } else if (parser_token.tag_name === 'thead') {
+ // A colgroup element's end tag may be ommitted if a thead, tfoot, tbody, or tr element is started.
+ // A caption element's end tag may be ommitted if a colgroup, thead, tfoot, tbody, or tr element is started.
+ result = result || this._tag_stack.try_pop('caption', ['table']);
+ result = result || this._tag_stack.try_pop('colgroup', ['table']);
+
+ //} else if (parser_token.tag_name === 'caption') {
+ // DONE: A caption element’s end tag may be omitted if the caption element is not immediately followed by a space character or a comment.
+
+ } else if (parser_token.tag_name === 'tbody' || parser_token.tag_name === 'tfoot') {
+ // A thead element’s end tag may be omitted if the thead element is immediately followed by a tbody or tfoot element.
+ // A tbody element’s end tag may be omitted if the tbody element is immediately followed by a tbody or tfoot element, or if there is no more content in the parent element.
+ // A colgroup element's end tag may be ommitted if a thead, tfoot, tbody, or tr element is started.
+ // A caption element's end tag may be ommitted if a colgroup, thead, tfoot, tbody, or tr element is started.
+ result = result || this._tag_stack.try_pop('caption', ['table']);
+ result = result || this._tag_stack.try_pop('colgroup', ['table']);
+ result = result || this._tag_stack.try_pop('thead', ['table']);
+ result = result || this._tag_stack.try_pop('tbody', ['table']);
+
+ //} else if (parser_token.tag_name === 'tfoot') {
+ // DONE: A tfoot element’s end tag may be omitted if there is no more content in the parent element.
+
+ } else if (parser_token.tag_name === 'tr') {
+ // A tr element’s end tag may be omitted if the tr element is immediately followed by another tr element, or if there is no more content in the parent element.
+ // A colgroup element's end tag may be ommitted if a thead, tfoot, tbody, or tr element is started.
+ // A caption element's end tag may be ommitted if a colgroup, thead, tfoot, tbody, or tr element is started.
+ result = result || this._tag_stack.try_pop('caption', ['table']);
+ result = result || this._tag_stack.try_pop('colgroup', ['table']);
+ result = result || this._tag_stack.try_pop('tr', ['table', 'thead', 'tbody', 'tfoot']);
+
+ } else if (parser_token.tag_name === 'th' || parser_token.tag_name === 'td') {
+ // A td element’s end tag may be omitted if the td element is immediately followed by a td or th element, or if there is no more content in the parent element.
+ // A th element’s end tag may be omitted if the th element is immediately followed by a td or th element, or if there is no more content in the parent element.
+ result = result || this._tag_stack.try_pop('td', ['table', 'thead', 'tbody', 'tfoot', 'tr']);
+ result = result || this._tag_stack.try_pop('th', ['table', 'thead', 'tbody', 'tfoot', 'tr']);
+ }
+
+ // Start element omission not handled currently
+ // A head element’s start tag may be omitted if the element is empty, or if the first thing inside the head element is an element.
+ // A tbody element’s start tag may be omitted if the first thing inside the tbody element is a tr element, and if the element is not immediately preceded by a tbody, thead, or tfoot element whose end tag has been omitted. (It can’t be omitted if the element is empty.)
+ // A colgroup element’s start tag may be omitted if the first thing inside the colgroup element is a col element, and if the element is not immediately preceded by another colgroup element whose end tag has been omitted. (It can’t be omitted if the element is empty.)
+
+ // Fix up the parent of the parser token
+ parser_token.parent = this._tag_stack.get_parser_token();
+
+ return result;
+};
+
+module.exports.Beautifier = Beautifier;
+
+
+/***/ }),
+/* 20 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var BaseOptions = __webpack_require__(6).Options;
+
+function Options(options) {
+ BaseOptions.call(this, options, 'html');
+ if (this.templating.length === 1 && this.templating[0] === 'auto') {
+ this.templating = ['django', 'erb', 'handlebars', 'php'];
+ }
+
+ this.indent_inner_html = this._get_boolean('indent_inner_html');
+ this.indent_body_inner_html = this._get_boolean('indent_body_inner_html', true);
+ this.indent_head_inner_html = this._get_boolean('indent_head_inner_html', true);
+
+ this.indent_handlebars = this._get_boolean('indent_handlebars', true);
+ this.wrap_attributes = this._get_selection('wrap_attributes',
+ ['auto', 'force', 'force-aligned', 'force-expand-multiline', 'aligned-multiple', 'preserve', 'preserve-aligned']);
+ this.wrap_attributes_indent_size = this._get_number('wrap_attributes_indent_size', this.indent_size);
+ this.extra_liners = this._get_array('extra_liners', ['head', 'body', '/html']);
+
+ // Block vs inline elements
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
+ // https://www.w3.org/TR/html5/dom.html#phrasing-content
+ this.inline = this._get_array('inline', [
+ 'a', 'abbr', 'area', 'audio', 'b', 'bdi', 'bdo', 'br', 'button', 'canvas', 'cite',
+ 'code', 'data', 'datalist', 'del', 'dfn', 'em', 'embed', 'i', 'iframe', 'img',
+ 'input', 'ins', 'kbd', 'keygen', 'label', 'map', 'mark', 'math', 'meter', 'noscript',
+ 'object', 'output', 'progress', 'q', 'ruby', 's', 'samp', /* 'script', */ 'select', 'small',
+ 'span', 'strong', 'sub', 'sup', 'svg', 'template', 'textarea', 'time', 'u', 'var',
+ 'video', 'wbr', 'text',
+ // obsolete inline tags
+ 'acronym', 'big', 'strike', 'tt'
+ ]);
+ this.void_elements = this._get_array('void_elements', [
+ // HTLM void elements - aka self-closing tags - aka singletons
+ // https://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
+ 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
+ // NOTE: Optional tags are too complex for a simple list
+ // they are hard coded in _do_optional_end_element
+
+ // Doctype and xml elements
+ '!doctype', '?xml',
+
+ // obsolete tags
+ // basefont: https://www.computerhope.com/jargon/h/html-basefont-tag.htm
+ // isndex: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/isindex
+ 'basefont', 'isindex'
+ ]);
+ this.unformatted = this._get_array('unformatted', []);
+ this.content_unformatted = this._get_array('content_unformatted', [
+ 'pre', 'textarea'
+ ]);
+ this.unformatted_content_delimiter = this._get_characters('unformatted_content_delimiter');
+ this.indent_scripts = this._get_selection('indent_scripts', ['normal', 'keep', 'separate']);
+
+}
+Options.prototype = new BaseOptions();
+
+
+
+module.exports.Options = Options;
+
+
+/***/ }),
+/* 21 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var BaseTokenizer = __webpack_require__(9).Tokenizer;
+var BASETOKEN = __webpack_require__(9).TOKEN;
+var Directives = __webpack_require__(13).Directives;
+var TemplatablePattern = __webpack_require__(14).TemplatablePattern;
+var Pattern = __webpack_require__(12).Pattern;
+
+var TOKEN = {
+ TAG_OPEN: 'TK_TAG_OPEN',
+ TAG_CLOSE: 'TK_TAG_CLOSE',
+ ATTRIBUTE: 'TK_ATTRIBUTE',
+ EQUALS: 'TK_EQUALS',
+ VALUE: 'TK_VALUE',
+ COMMENT: 'TK_COMMENT',
+ TEXT: 'TK_TEXT',
+ UNKNOWN: 'TK_UNKNOWN',
+ START: BASETOKEN.START,
+ RAW: BASETOKEN.RAW,
+ EOF: BASETOKEN.EOF
+};
+
+var directives_core = new Directives(/<\!--/, /-->/);
+
+var Tokenizer = function(input_string, options) {
+ BaseTokenizer.call(this, input_string, options);
+ this._current_tag_name = '';
+
+ // Words end at whitespace or when a tag starts
+ // if we are indenting handlebars, they are considered tags
+ var templatable_reader = new TemplatablePattern(this._input).read_options(this._options);
+ var pattern_reader = new Pattern(this._input);
+
+ this.__patterns = {
+ word: templatable_reader.until(/[\n\r\t <]/),
+ single_quote: templatable_reader.until_after(/'/),
+ double_quote: templatable_reader.until_after(/"/),
+ attribute: templatable_reader.until(/[\n\r\t =>]|\/>/),
+ element_name: templatable_reader.until(/[\n\r\t >\/]/),
+
+ handlebars_comment: pattern_reader.starting_with(/{{!--/).until_after(/--}}/),
+ handlebars: pattern_reader.starting_with(/{{/).until_after(/}}/),
+ handlebars_open: pattern_reader.until(/[\n\r\t }]/),
+ handlebars_raw_close: pattern_reader.until(/}}/),
+ comment: pattern_reader.starting_with(/<!--/).until_after(/-->/),
+ cdata: pattern_reader.starting_with(/<!\[CDATA\[/).until_after(/]]>/),
+ // https://en.wikipedia.org/wiki/Conditional_comment
+ conditional_comment: pattern_reader.starting_with(/<!\[/).until_after(/]>/),
+ processing: pattern_reader.starting_with(/<\?/).until_after(/\?>/)
+ };
+
+ if (this._options.indent_handlebars) {
+ this.__patterns.word = this.__patterns.word.exclude('handlebars');
+ }
+
+ this._unformatted_content_delimiter = null;
+
+ if (this._options.unformatted_content_delimiter) {
+ var literal_regexp = this._input.get_literal_regexp(this._options.unformatted_content_delimiter);
+ this.__patterns.unformatted_content_delimiter =
+ pattern_reader.matching(literal_regexp)
+ .until_after(literal_regexp);
+ }
+};
+Tokenizer.prototype = new BaseTokenizer();
+
+Tokenizer.prototype._is_comment = function(current_token) { // jshint unused:false
+ return false; //current_token.type === TOKEN.COMMENT || current_token.type === TOKEN.UNKNOWN;
+};
+
+Tokenizer.prototype._is_opening = function(current_token) {
+ return current_token.type === TOKEN.TAG_OPEN;
+};
+
+Tokenizer.prototype._is_closing = function(current_token, open_token) {
+ return current_token.type === TOKEN.TAG_CLOSE &&
+ (open_token && (
+ ((current_token.text === '>' || current_token.text === '/>') && open_token.text[0] === '<') ||
+ (current_token.text === '}}' && open_token.text[0] === '{' && open_token.text[1] === '{')));
+};
+
+Tokenizer.prototype._reset = function() {
+ this._current_tag_name = '';
+};
+
+Tokenizer.prototype._get_next_token = function(previous_token, open_token) { // jshint unused:false
+ var token = null;
+ this._readWhitespace();
+ var c = this._input.peek();
+
+ if (c === null) {
+ return this._create_token(TOKEN.EOF, '');
+ }
+
+ token = token || this._read_open_handlebars(c, open_token);
+ token = token || this._read_attribute(c, previous_token, open_token);
+ token = token || this._read_close(c, open_token);
+ token = token || this._read_raw_content(c, previous_token, open_token);
+ token = token || this._read_content_word(c);
+ token = token || this._read_comment_or_cdata(c);
+ token = token || this._read_processing(c);
+ token = token || this._read_open(c, open_token);
+ token = token || this._create_token(TOKEN.UNKNOWN, this._input.next());
+
+ return token;
+};
+
+Tokenizer.prototype._read_comment_or_cdata = function(c) { // jshint unused:false
+ var token = null;
+ var resulting_string = null;
+ var directives = null;
+
+ if (c === '<') {
+ var peek1 = this._input.peek(1);
+ // We treat all comments as literals, even more than preformatted tags
+ // we only look for the appropriate closing marker
+ if (peek1 === '!') {
+ resulting_string = this.__patterns.comment.read();
+
+ // only process directive on html comments
+ if (resulting_string) {
+ directives = directives_core.get_directives(resulting_string);
+ if (directives && directives.ignore === 'start') {
+ resulting_string += directives_core.readIgnored(this._input);
+ }
+ } else {
+ resulting_string = this.__patterns.cdata.read();
+ }
+ }
+
+ if (resulting_string) {
+ token = this._create_token(TOKEN.COMMENT, resulting_string);
+ token.directives = directives;
+ }
+ }
+
+ return token;
+};
+
+Tokenizer.prototype._read_processing = function(c) { // jshint unused:false
+ var token = null;
+ var resulting_string = null;
+ var directives = null;
+
+ if (c === '<') {
+ var peek1 = this._input.peek(1);
+ if (peek1 === '!' || peek1 === '?') {
+ resulting_string = this.__patterns.conditional_comment.read();
+ resulting_string = resulting_string || this.__patterns.processing.read();
+ }
+
+ if (resulting_string) {
+ token = this._create_token(TOKEN.COMMENT, resulting_string);
+ token.directives = directives;
+ }
+ }
+
+ return token;
+};
+
+Tokenizer.prototype._read_open = function(c, open_token) {
+ var resulting_string = null;
+ var token = null;
+ if (!open_token) {
+ if (c === '<') {
+
+ resulting_string = this._input.next();
+ if (this._input.peek() === '/') {
+ resulting_string += this._input.next();
+ }
+ resulting_string += this.__patterns.element_name.read();
+ token = this._create_token(TOKEN.TAG_OPEN, resulting_string);
+ }
+ }
+ return token;
+};
+
+Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
+ var resulting_string = null;
+ var token = null;
+ if (!open_token) {
+ if (this._options.indent_handlebars && c === '{' && this._input.peek(1) === '{') {
+ if (this._input.peek(2) === '!') {
+ resulting_string = this.__patterns.handlebars_comment.read();
+ resulting_string = resulting_string || this.__patterns.handlebars.read();
+ token = this._create_token(TOKEN.COMMENT, resulting_string);
+ } else {
+ resulting_string = this.__patterns.handlebars_open.read();
+ token = this._create_token(TOKEN.TAG_OPEN, resulting_string);
+ }
+ }
+ }
+ return token;
+};
+
+
+Tokenizer.prototype._read_close = function(c, open_token) {
+ var resulting_string = null;
+ var token = null;
+ if (open_token) {
+ if (open_token.text[0] === '<' && (c === '>' || (c === '/' && this._input.peek(1) === '>'))) {
+ resulting_string = this._input.next();
+ if (c === '/') { // for close tag "/>"
+ resulting_string += this._input.next();
+ }
+ token = this._create_token(TOKEN.TAG_CLOSE, resulting_string);
+ } else if (open_token.text[0] === '{' && c === '}' && this._input.peek(1) === '}') {
+ this._input.next();
+ this._input.next();
+ token = this._create_token(TOKEN.TAG_CLOSE, '}}');
+ }
+ }
+
+ return token;
+};
+
+Tokenizer.prototype._read_attribute = function(c, previous_token, open_token) {
+ var token = null;
+ var resulting_string = '';
+ if (open_token && open_token.text[0] === '<') {
+
+ if (c === '=') {
+ token = this._create_token(TOKEN.EQUALS, this._input.next());
+ } else if (c === '"' || c === "'") {
+ var content = this._input.next();
+ if (c === '"') {
+ content += this.__patterns.double_quote.read();
+ } else {
+ content += this.__patterns.single_quote.read();
+ }
+ token = this._create_token(TOKEN.VALUE, content);
+ } else {
+ resulting_string = this.__patterns.attribute.read();
+
+ if (resulting_string) {
+ if (previous_token.type === TOKEN.EQUALS) {
+ token = this._create_token(TOKEN.VALUE, resulting_string);
+ } else {
+ token = this._create_token(TOKEN.ATTRIBUTE, resulting_string);
+ }
+ }
+ }
+ }
+ return token;
+};
+
+Tokenizer.prototype._is_content_unformatted = function(tag_name) {
+ // void_elements have no content and so cannot have unformatted content
+ // script and style tags should always be read as unformatted content
+ // finally content_unformatted and unformatted element contents are unformatted
+ return this._options.void_elements.indexOf(tag_name) === -1 &&
+ (this._options.content_unformatted.indexOf(tag_name) !== -1 ||
+ this._options.unformatted.indexOf(tag_name) !== -1);
+};
+
+
+Tokenizer.prototype._read_raw_content = function(c, previous_token, open_token) { // jshint unused:false
+ var resulting_string = '';
+ if (open_token && open_token.text[0] === '{') {
+ resulting_string = this.__patterns.handlebars_raw_close.read();
+ } else if (previous_token.type === TOKEN.TAG_CLOSE &&
+ previous_token.opened.text[0] === '<' && previous_token.text[0] !== '/') {
+ // ^^ empty tag has no content
+ var tag_name = previous_token.opened.text.substr(1).toLowerCase();
+ if (tag_name === 'script' || tag_name === 'style') {
+ // Script and style tags are allowed to have comments wrapping their content
+ // or just have regular content.
+ var token = this._read_comment_or_cdata(c);
+ if (token) {
+ token.type = TOKEN.TEXT;
+ return token;
+ }
+ resulting_string = this._input.readUntil(new RegExp('</' + tag_name + '[\\n\\r\\t ]*?>', 'ig'));
+ } else if (this._is_content_unformatted(tag_name)) {
+
+ resulting_string = this._input.readUntil(new RegExp('</' + tag_name + '[\\n\\r\\t ]*?>', 'ig'));
+ }
+ }
+
+ if (resulting_string) {
+ return this._create_token(TOKEN.TEXT, resulting_string);
+ }
+
+ return null;
+};
+
+Tokenizer.prototype._read_content_word = function(c) {
+ var resulting_string = '';
+ if (this._options.unformatted_content_delimiter) {
+ if (c === this._options.unformatted_content_delimiter[0]) {
+ resulting_string = this.__patterns.unformatted_content_delimiter.read();
+ }
+ }
+
+ if (!resulting_string) {
+ resulting_string = this.__patterns.word.read();
+ }
+ if (resulting_string) {
+ return this._create_token(TOKEN.TEXT, resulting_string);
+ }
+};
+
+module.exports.Tokenizer = Tokenizer;
+module.exports.TOKEN = TOKEN;
+
+
+/***/ })
+/******/ ]);
+var style_html = legacy_beautify_html;
+/* Footer */
+if (typeof define === "function" && define.amd) {
+ // Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
+ define(["require", "./beautify", "./beautify-css"], function(requireamd) {
+ var js_beautify = requireamd("./beautify");
+ var css_beautify = requireamd("./beautify-css");
+
+ return {
+ html_beautify: function(html_source, options) {
+ return style_html(html_source, options, js_beautify.js_beautify, css_beautify.css_beautify);
+ }
+ };
+ });
+} else if (typeof exports !== "undefined") {
+ // Add support for CommonJS. Just put this file somewhere on your require.paths
+ // and you will be able to `var html_beautify = require("beautify").html_beautify`.
+ var js_beautify = require('./beautify-js.js');
+ var css_beautify = require('./beautify-css.js');
+
+ exports.html_beautify = function(html_source, options) {
+ return style_html(html_source, options, js_beautify.js_beautify, css_beautify.css_beautify);
+ };
+} else if (typeof window !== "undefined") {
+ // If we're running a web page and don't have either of the above, add our one global
+ window.html_beautify = function(html_source, options) {
+ return style_html(html_source, options, window.js_beautify, window.css_beautify);
+ };
+} else if (typeof global !== "undefined") {
+ // If we don't even have window, try global.
+ global.html_beautify = function(html_source, options) {
+ return style_html(html_source, options, global.js_beautify, global.css_beautify);
+ };
+}
+
+}());
diff --git a/devtools/shared/jsbeautify/src/beautify-js.js b/devtools/shared/jsbeautify/src/beautify-js.js
new file mode 100644
index 0000000000..f0e1e54295
--- /dev/null
+++ b/devtools/shared/jsbeautify/src/beautify-js.js
@@ -0,0 +1,4045 @@
+/* AUTO-GENERATED. DO NOT MODIFY. */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ JS Beautifier
+---------------
+
+
+ Written by Einar Lielmanis, <einar@beautifier.io>
+ https://beautifier.io/
+
+ Originally converted to javascript by Vital, <vital76@gmail.com>
+ "End braces on own line" added by Chris J. Shull, <chrisjshull@gmail.com>
+ Parsing improvements for brace-less statements by Liam Newman <bitwiseman@beautifier.io>
+
+
+ Usage:
+ js_beautify(js_source_text);
+ js_beautify(js_source_text, options);
+
+ The options are:
+ indent_size (default 4) - indentation size,
+ indent_char (default space) - character to indent with,
+ preserve_newlines (default true) - whether existing line breaks should be preserved,
+ max_preserve_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk,
+
+ jslint_happy (default false) - if true, then jslint-stricter mode is enforced.
+
+ jslint_happy !jslint_happy
+ ---------------------------------
+ function () function()
+
+ switch () { switch() {
+ case 1: case 1:
+ break; break;
+ } }
+
+ space_after_anon_function (default false) - should the space before an anonymous function's parens be added, "function()" vs "function ()",
+ NOTE: This option is overriden by jslint_happy (i.e. if jslint_happy is true, space_after_anon_function is true by design)
+
+ brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | "none" | any of the former + ",preserve-inline"
+ put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line, or attempt to keep them where they are.
+ preserve-inline will try to preserve inline blocks of curly braces
+
+ space_before_conditional (default true) - should the space before conditional statement be added, "if(true)" vs "if (true)",
+
+ unescape_strings (default false) - should printable characters in strings encoded in \xNN notation be unescaped, "example" vs "\x65\x78\x61\x6d\x70\x6c\x65"
+
+ wrap_line_length (default unlimited) - lines should wrap at next opportunity after this number of characters.
+ NOTE: This is not a hard limit. Lines will continue until a point where a newline would
+ be preserved if it were present.
+
+ end_with_newline (default false) - end output with a newline
+
+
+ e.g
+
+ js_beautify(js_source_text, {
+ 'indent_size': 1,
+ 'indent_char': '\t'
+ });
+
+*/
+
+(function() {
+
+/* GENERATED_BUILD_OUTPUT */
+var legacy_beautify_js =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Beautifier = __webpack_require__(1).Beautifier,
+ Options = __webpack_require__(5).Options;
+
+function js_beautify(js_source_text, options) {
+ var beautifier = new Beautifier(js_source_text, options);
+ return beautifier.beautify();
+}
+
+module.exports = js_beautify;
+module.exports.defaultOptions = function() {
+ return new Options();
+};
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Output = __webpack_require__(2).Output;
+var Token = __webpack_require__(3).Token;
+var acorn = __webpack_require__(4);
+var Options = __webpack_require__(5).Options;
+var Tokenizer = __webpack_require__(7).Tokenizer;
+var line_starters = __webpack_require__(7).line_starters;
+var positionable_operators = __webpack_require__(7).positionable_operators;
+var TOKEN = __webpack_require__(7).TOKEN;
+
+
+function in_array(what, arr) {
+ return arr.indexOf(what) !== -1;
+}
+
+function ltrim(s) {
+ return s.replace(/^\s+/g, '');
+}
+
+function generateMapFromStrings(list) {
+ var result = {};
+ for (var x = 0; x < list.length; x++) {
+ // make the mapped names underscored instead of dash
+ result[list[x].replace(/-/g, '_')] = list[x];
+ }
+ return result;
+}
+
+function reserved_word(token, word) {
+ return token && token.type === TOKEN.RESERVED && token.text === word;
+}
+
+function reserved_array(token, words) {
+ return token && token.type === TOKEN.RESERVED && in_array(token.text, words);
+}
+// Unsure of what they mean, but they work. Worth cleaning up in future.
+var special_words = ['case', 'return', 'do', 'if', 'throw', 'else', 'await', 'break', 'continue', 'async'];
+
+var validPositionValues = ['before-newline', 'after-newline', 'preserve-newline'];
+
+// Generate map from array
+var OPERATOR_POSITION = generateMapFromStrings(validPositionValues);
+
+var OPERATOR_POSITION_BEFORE_OR_PRESERVE = [OPERATOR_POSITION.before_newline, OPERATOR_POSITION.preserve_newline];
+
+var MODE = {
+ BlockStatement: 'BlockStatement', // 'BLOCK'
+ Statement: 'Statement', // 'STATEMENT'
+ ObjectLiteral: 'ObjectLiteral', // 'OBJECT',
+ ArrayLiteral: 'ArrayLiteral', //'[EXPRESSION]',
+ ForInitializer: 'ForInitializer', //'(FOR-EXPRESSION)',
+ Conditional: 'Conditional', //'(COND-EXPRESSION)',
+ Expression: 'Expression' //'(EXPRESSION)'
+};
+
+function remove_redundant_indentation(output, frame) {
+ // This implementation is effective but has some issues:
+ // - can cause line wrap to happen too soon due to indent removal
+ // after wrap points are calculated
+ // These issues are minor compared to ugly indentation.
+
+ if (frame.multiline_frame ||
+ frame.mode === MODE.ForInitializer ||
+ frame.mode === MODE.Conditional) {
+ return;
+ }
+
+ // remove one indent from each line inside this section
+ output.remove_indent(frame.start_line_index);
+}
+
+// we could use just string.split, but
+// IE doesn't like returning empty strings
+function split_linebreaks(s) {
+ //return s.split(/\x0d\x0a|\x0a/);
+
+ s = s.replace(acorn.allLineBreaks, '\n');
+ var out = [],
+ idx = s.indexOf("\n");
+ while (idx !== -1) {
+ out.push(s.substring(0, idx));
+ s = s.substring(idx + 1);
+ idx = s.indexOf("\n");
+ }
+ if (s.length) {
+ out.push(s);
+ }
+ return out;
+}
+
+function is_array(mode) {
+ return mode === MODE.ArrayLiteral;
+}
+
+function is_expression(mode) {
+ return in_array(mode, [MODE.Expression, MODE.ForInitializer, MODE.Conditional]);
+}
+
+function all_lines_start_with(lines, c) {
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i].trim();
+ if (line.charAt(0) !== c) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function each_line_matches_indent(lines, indent) {
+ var i = 0,
+ len = lines.length,
+ line;
+ for (; i < len; i++) {
+ line = lines[i];
+ // allow empty lines to pass through
+ if (line && line.indexOf(indent) !== 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+
+function Beautifier(source_text, options) {
+ options = options || {};
+ this._source_text = source_text || '';
+
+ this._output = null;
+ this._tokens = null;
+ this._last_last_text = null;
+ this._flags = null;
+ this._previous_flags = null;
+
+ this._flag_store = null;
+ this._options = new Options(options);
+}
+
+Beautifier.prototype.create_flags = function(flags_base, mode) {
+ var next_indent_level = 0;
+ if (flags_base) {
+ next_indent_level = flags_base.indentation_level;
+ if (!this._output.just_added_newline() &&
+ flags_base.line_indent_level > next_indent_level) {
+ next_indent_level = flags_base.line_indent_level;
+ }
+ }
+
+ var next_flags = {
+ mode: mode,
+ parent: flags_base,
+ last_token: flags_base ? flags_base.last_token : new Token(TOKEN.START_BLOCK, ''), // last token text
+ last_word: flags_base ? flags_base.last_word : '', // last TOKEN.WORD passed
+ declaration_statement: false,
+ declaration_assignment: false,
+ multiline_frame: false,
+ inline_frame: false,
+ if_block: false,
+ else_block: false,
+ do_block: false,
+ do_while: false,
+ import_block: false,
+ in_case_statement: false, // switch(..){ INSIDE HERE }
+ in_case: false, // we're on the exact line with "case 0:"
+ case_body: false, // the indented case-action block
+ indentation_level: next_indent_level,
+ alignment: 0,
+ line_indent_level: flags_base ? flags_base.line_indent_level : next_indent_level,
+ start_line_index: this._output.get_line_number(),
+ ternary_depth: 0
+ };
+ return next_flags;
+};
+
+Beautifier.prototype._reset = function(source_text) {
+ var baseIndentString = source_text.match(/^[\t ]*/)[0];
+
+ this._last_last_text = ''; // pre-last token text
+ this._output = new Output(this._options, baseIndentString);
+
+ // If testing the ignore directive, start with output disable set to true
+ this._output.raw = this._options.test_output_raw;
+
+
+ // Stack of parsing/formatting states, including MODE.
+ // We tokenize, parse, and output in an almost purely a forward-only stream of token input
+ // and formatted output. This makes the beautifier less accurate than full parsers
+ // but also far more tolerant of syntax errors.
+ //
+ // For example, the default mode is MODE.BlockStatement. If we see a '{' we push a new frame of type
+ // MODE.BlockStatement on the the stack, even though it could be object literal. If we later
+ // encounter a ":", we'll switch to to MODE.ObjectLiteral. If we then see a ";",
+ // most full parsers would die, but the beautifier gracefully falls back to
+ // MODE.BlockStatement and continues on.
+ this._flag_store = [];
+ this.set_mode(MODE.BlockStatement);
+ var tokenizer = new Tokenizer(source_text, this._options);
+ this._tokens = tokenizer.tokenize();
+ return source_text;
+};
+
+Beautifier.prototype.beautify = function() {
+ // if disabled, return the input unchanged.
+ if (this._options.disabled) {
+ return this._source_text;
+ }
+
+ var sweet_code;
+ var source_text = this._reset(this._source_text);
+
+ var eol = this._options.eol;
+ if (this._options.eol === 'auto') {
+ eol = '\n';
+ if (source_text && acorn.lineBreak.test(source_text || '')) {
+ eol = source_text.match(acorn.lineBreak)[0];
+ }
+ }
+
+ var current_token = this._tokens.next();
+ while (current_token) {
+ this.handle_token(current_token);
+
+ this._last_last_text = this._flags.last_token.text;
+ this._flags.last_token = current_token;
+
+ current_token = this._tokens.next();
+ }
+
+ sweet_code = this._output.get_code(eol);
+
+ return sweet_code;
+};
+
+Beautifier.prototype.handle_token = function(current_token, preserve_statement_flags) {
+ if (current_token.type === TOKEN.START_EXPR) {
+ this.handle_start_expr(current_token);
+ } else if (current_token.type === TOKEN.END_EXPR) {
+ this.handle_end_expr(current_token);
+ } else if (current_token.type === TOKEN.START_BLOCK) {
+ this.handle_start_block(current_token);
+ } else if (current_token.type === TOKEN.END_BLOCK) {
+ this.handle_end_block(current_token);
+ } else if (current_token.type === TOKEN.WORD) {
+ this.handle_word(current_token);
+ } else if (current_token.type === TOKEN.RESERVED) {
+ this.handle_word(current_token);
+ } else if (current_token.type === TOKEN.SEMICOLON) {
+ this.handle_semicolon(current_token);
+ } else if (current_token.type === TOKEN.STRING) {
+ this.handle_string(current_token);
+ } else if (current_token.type === TOKEN.EQUALS) {
+ this.handle_equals(current_token);
+ } else if (current_token.type === TOKEN.OPERATOR) {
+ this.handle_operator(current_token);
+ } else if (current_token.type === TOKEN.COMMA) {
+ this.handle_comma(current_token);
+ } else if (current_token.type === TOKEN.BLOCK_COMMENT) {
+ this.handle_block_comment(current_token, preserve_statement_flags);
+ } else if (current_token.type === TOKEN.COMMENT) {
+ this.handle_comment(current_token, preserve_statement_flags);
+ } else if (current_token.type === TOKEN.DOT) {
+ this.handle_dot(current_token);
+ } else if (current_token.type === TOKEN.EOF) {
+ this.handle_eof(current_token);
+ } else if (current_token.type === TOKEN.UNKNOWN) {
+ this.handle_unknown(current_token, preserve_statement_flags);
+ } else {
+ this.handle_unknown(current_token, preserve_statement_flags);
+ }
+};
+
+Beautifier.prototype.handle_whitespace_and_comments = function(current_token, preserve_statement_flags) {
+ var newlines = current_token.newlines;
+ var keep_whitespace = this._options.keep_array_indentation && is_array(this._flags.mode);
+
+ if (current_token.comments_before) {
+ var comment_token = current_token.comments_before.next();
+ while (comment_token) {
+ // The cleanest handling of inline comments is to treat them as though they aren't there.
+ // Just continue formatting and the behavior should be logical.
+ // Also ignore unknown tokens. Again, this should result in better behavior.
+ this.handle_whitespace_and_comments(comment_token, preserve_statement_flags);
+ this.handle_token(comment_token, preserve_statement_flags);
+ comment_token = current_token.comments_before.next();
+ }
+ }
+
+ if (keep_whitespace) {
+ for (var i = 0; i < newlines; i += 1) {
+ this.print_newline(i > 0, preserve_statement_flags);
+ }
+ } else {
+ if (this._options.max_preserve_newlines && newlines > this._options.max_preserve_newlines) {
+ newlines = this._options.max_preserve_newlines;
+ }
+
+ if (this._options.preserve_newlines) {
+ if (newlines > 1) {
+ this.print_newline(false, preserve_statement_flags);
+ for (var j = 1; j < newlines; j += 1) {
+ this.print_newline(true, preserve_statement_flags);
+ }
+ }
+ }
+ }
+
+};
+
+var newline_restricted_tokens = ['async', 'break', 'continue', 'return', 'throw', 'yield'];
+
+Beautifier.prototype.allow_wrap_or_preserved_newline = function(current_token, force_linewrap) {
+ force_linewrap = (force_linewrap === undefined) ? false : force_linewrap;
+
+ // Never wrap the first token on a line
+ if (this._output.just_added_newline()) {
+ return;
+ }
+
+ var shouldPreserveOrForce = (this._options.preserve_newlines && current_token.newlines) || force_linewrap;
+ var operatorLogicApplies = in_array(this._flags.last_token.text, positionable_operators) ||
+ in_array(current_token.text, positionable_operators);
+
+ if (operatorLogicApplies) {
+ var shouldPrintOperatorNewline = (
+ in_array(this._flags.last_token.text, positionable_operators) &&
+ in_array(this._options.operator_position, OPERATOR_POSITION_BEFORE_OR_PRESERVE)
+ ) ||
+ in_array(current_token.text, positionable_operators);
+ shouldPreserveOrForce = shouldPreserveOrForce && shouldPrintOperatorNewline;
+ }
+
+ if (shouldPreserveOrForce) {
+ this.print_newline(false, true);
+ } else if (this._options.wrap_line_length) {
+ if (reserved_array(this._flags.last_token, newline_restricted_tokens)) {
+ // These tokens should never have a newline inserted
+ // between them and the following expression.
+ return;
+ }
+ this._output.set_wrap_point();
+ }
+};
+
+Beautifier.prototype.print_newline = function(force_newline, preserve_statement_flags) {
+ if (!preserve_statement_flags) {
+ if (this._flags.last_token.text !== ';' && this._flags.last_token.text !== ',' && this._flags.last_token.text !== '=' && (this._flags.last_token.type !== TOKEN.OPERATOR || this._flags.last_token.text === '--' || this._flags.last_token.text === '++')) {
+ var next_token = this._tokens.peek();
+ while (this._flags.mode === MODE.Statement &&
+ !(this._flags.if_block && reserved_word(next_token, 'else')) &&
+ !this._flags.do_block) {
+ this.restore_mode();
+ }
+ }
+ }
+
+ if (this._output.add_new_line(force_newline)) {
+ this._flags.multiline_frame = true;
+ }
+};
+
+Beautifier.prototype.print_token_line_indentation = function(current_token) {
+ if (this._output.just_added_newline()) {
+ if (this._options.keep_array_indentation &&
+ current_token.newlines &&
+ (current_token.text === '[' || is_array(this._flags.mode))) {
+ this._output.current_line.set_indent(-1);
+ this._output.current_line.push(current_token.whitespace_before);
+ this._output.space_before_token = false;
+ } else if (this._output.set_indent(this._flags.indentation_level, this._flags.alignment)) {
+ this._flags.line_indent_level = this._flags.indentation_level;
+ }
+ }
+};
+
+Beautifier.prototype.print_token = function(current_token) {
+ if (this._output.raw) {
+ this._output.add_raw_token(current_token);
+ return;
+ }
+
+ if (this._options.comma_first && current_token.previous && current_token.previous.type === TOKEN.COMMA &&
+ this._output.just_added_newline()) {
+ if (this._output.previous_line.last() === ',') {
+ var popped = this._output.previous_line.pop();
+ // if the comma was already at the start of the line,
+ // pull back onto that line and reprint the indentation
+ if (this._output.previous_line.is_empty()) {
+ this._output.previous_line.push(popped);
+ this._output.trim(true);
+ this._output.current_line.pop();
+ this._output.trim();
+ }
+
+ // add the comma in front of the next token
+ this.print_token_line_indentation(current_token);
+ this._output.add_token(',');
+ this._output.space_before_token = true;
+ }
+ }
+
+ this.print_token_line_indentation(current_token);
+ this._output.non_breaking_space = true;
+ this._output.add_token(current_token.text);
+ if (this._output.previous_token_wrapped) {
+ this._flags.multiline_frame = true;
+ }
+};
+
+Beautifier.prototype.indent = function() {
+ this._flags.indentation_level += 1;
+ this._output.set_indent(this._flags.indentation_level, this._flags.alignment);
+};
+
+Beautifier.prototype.deindent = function() {
+ if (this._flags.indentation_level > 0 &&
+ ((!this._flags.parent) || this._flags.indentation_level > this._flags.parent.indentation_level)) {
+ this._flags.indentation_level -= 1;
+ this._output.set_indent(this._flags.indentation_level, this._flags.alignment);
+ }
+};
+
+Beautifier.prototype.set_mode = function(mode) {
+ if (this._flags) {
+ this._flag_store.push(this._flags);
+ this._previous_flags = this._flags;
+ } else {
+ this._previous_flags = this.create_flags(null, mode);
+ }
+
+ this._flags = this.create_flags(this._previous_flags, mode);
+ this._output.set_indent(this._flags.indentation_level, this._flags.alignment);
+};
+
+
+Beautifier.prototype.restore_mode = function() {
+ if (this._flag_store.length > 0) {
+ this._previous_flags = this._flags;
+ this._flags = this._flag_store.pop();
+ if (this._previous_flags.mode === MODE.Statement) {
+ remove_redundant_indentation(this._output, this._previous_flags);
+ }
+ this._output.set_indent(this._flags.indentation_level, this._flags.alignment);
+ }
+};
+
+Beautifier.prototype.start_of_object_property = function() {
+ return this._flags.parent.mode === MODE.ObjectLiteral && this._flags.mode === MODE.Statement && (
+ (this._flags.last_token.text === ':' && this._flags.ternary_depth === 0) || (reserved_array(this._flags.last_token, ['get', 'set'])));
+};
+
+Beautifier.prototype.start_of_statement = function(current_token) {
+ var start = false;
+ start = start || reserved_array(this._flags.last_token, ['var', 'let', 'const']) && current_token.type === TOKEN.WORD;
+ start = start || reserved_word(this._flags.last_token, 'do');
+ start = start || (!(this._flags.parent.mode === MODE.ObjectLiteral && this._flags.mode === MODE.Statement)) && reserved_array(this._flags.last_token, newline_restricted_tokens) && !current_token.newlines;
+ start = start || reserved_word(this._flags.last_token, 'else') &&
+ !(reserved_word(current_token, 'if') && !current_token.comments_before);
+ start = start || (this._flags.last_token.type === TOKEN.END_EXPR && (this._previous_flags.mode === MODE.ForInitializer || this._previous_flags.mode === MODE.Conditional));
+ start = start || (this._flags.last_token.type === TOKEN.WORD && this._flags.mode === MODE.BlockStatement &&
+ !this._flags.in_case &&
+ !(current_token.text === '--' || current_token.text === '++') &&
+ this._last_last_text !== 'function' &&
+ current_token.type !== TOKEN.WORD && current_token.type !== TOKEN.RESERVED);
+ start = start || (this._flags.mode === MODE.ObjectLiteral && (
+ (this._flags.last_token.text === ':' && this._flags.ternary_depth === 0) || reserved_array(this._flags.last_token, ['get', 'set'])));
+
+ if (start) {
+ this.set_mode(MODE.Statement);
+ this.indent();
+
+ this.handle_whitespace_and_comments(current_token, true);
+
+ // Issue #276:
+ // If starting a new statement with [if, for, while, do], push to a new line.
+ // if (a) if (b) if(c) d(); else e(); else f();
+ if (!this.start_of_object_property()) {
+ this.allow_wrap_or_preserved_newline(current_token,
+ reserved_array(current_token, ['do', 'for', 'if', 'while']));
+ }
+ return true;
+ }
+ return false;
+};
+
+Beautifier.prototype.handle_start_expr = function(current_token) {
+ // The conditional starts the statement if appropriate.
+ if (!this.start_of_statement(current_token)) {
+ this.handle_whitespace_and_comments(current_token);
+ }
+
+ var next_mode = MODE.Expression;
+ if (current_token.text === '[') {
+
+ if (this._flags.last_token.type === TOKEN.WORD || this._flags.last_token.text === ')') {
+ // this is array index specifier, break immediately
+ // a[x], fn()[x]
+ if (reserved_array(this._flags.last_token, line_starters)) {
+ this._output.space_before_token = true;
+ }
+ this.print_token(current_token);
+ this.set_mode(next_mode);
+ this.indent();
+ if (this._options.space_in_paren) {
+ this._output.space_before_token = true;
+ }
+ return;
+ }
+
+ next_mode = MODE.ArrayLiteral;
+ if (is_array(this._flags.mode)) {
+ if (this._flags.last_token.text === '[' ||
+ (this._flags.last_token.text === ',' && (this._last_last_text === ']' || this._last_last_text === '}'))) {
+ // ], [ goes to new line
+ // }, [ goes to new line
+ if (!this._options.keep_array_indentation) {
+ this.print_newline();
+ }
+ }
+ }
+
+ if (!in_array(this._flags.last_token.type, [TOKEN.START_EXPR, TOKEN.END_EXPR, TOKEN.WORD, TOKEN.OPERATOR])) {
+ this._output.space_before_token = true;
+ }
+ } else {
+ if (this._flags.last_token.type === TOKEN.RESERVED) {
+ if (this._flags.last_token.text === 'for') {
+ this._output.space_before_token = this._options.space_before_conditional;
+ next_mode = MODE.ForInitializer;
+ } else if (in_array(this._flags.last_token.text, ['if', 'while'])) {
+ this._output.space_before_token = this._options.space_before_conditional;
+ next_mode = MODE.Conditional;
+ } else if (in_array(this._flags.last_word, ['await', 'async'])) {
+ // Should be a space between await and an IIFE, or async and an arrow function
+ this._output.space_before_token = true;
+ } else if (this._flags.last_token.text === 'import' && current_token.whitespace_before === '') {
+ this._output.space_before_token = false;
+ } else if (in_array(this._flags.last_token.text, line_starters) || this._flags.last_token.text === 'catch') {
+ this._output.space_before_token = true;
+ }
+ } else if (this._flags.last_token.type === TOKEN.EQUALS || this._flags.last_token.type === TOKEN.OPERATOR) {
+ // Support of this kind of newline preservation.
+ // a = (b &&
+ // (c || d));
+ if (!this.start_of_object_property()) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+ } else if (this._flags.last_token.type === TOKEN.WORD) {
+ this._output.space_before_token = false;
+
+ // function name() vs function name ()
+ // function* name() vs function* name ()
+ // async name() vs async name ()
+ // In ES6, you can also define the method properties of an object
+ // var obj = {a: function() {}}
+ // It can be abbreviated
+ // var obj = {a() {}}
+ // var obj = { a() {}} vs var obj = { a () {}}
+ // var obj = { * a() {}} vs var obj = { * a () {}}
+ var peek_back_two = this._tokens.peek(-3);
+ if (this._options.space_after_named_function && peek_back_two) {
+ // peek starts at next character so -1 is current token
+ var peek_back_three = this._tokens.peek(-4);
+ if (reserved_array(peek_back_two, ['async', 'function']) ||
+ (peek_back_two.text === '*' && reserved_array(peek_back_three, ['async', 'function']))) {
+ this._output.space_before_token = true;
+ } else if (this._flags.mode === MODE.ObjectLiteral) {
+ if ((peek_back_two.text === '{' || peek_back_two.text === ',') ||
+ (peek_back_two.text === '*' && (peek_back_three.text === '{' || peek_back_three.text === ','))) {
+ this._output.space_before_token = true;
+ }
+ }
+ }
+ } else {
+ // Support preserving wrapped arrow function expressions
+ // a.b('c',
+ // () => d.e
+ // )
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+
+ // function() vs function ()
+ // yield*() vs yield* ()
+ // function*() vs function* ()
+ if ((this._flags.last_token.type === TOKEN.RESERVED && (this._flags.last_word === 'function' || this._flags.last_word === 'typeof')) ||
+ (this._flags.last_token.text === '*' &&
+ (in_array(this._last_last_text, ['function', 'yield']) ||
+ (this._flags.mode === MODE.ObjectLiteral && in_array(this._last_last_text, ['{', ',']))))) {
+ this._output.space_before_token = this._options.space_after_anon_function;
+ }
+ }
+
+ if (this._flags.last_token.text === ';' || this._flags.last_token.type === TOKEN.START_BLOCK) {
+ this.print_newline();
+ } else if (this._flags.last_token.type === TOKEN.END_EXPR || this._flags.last_token.type === TOKEN.START_EXPR || this._flags.last_token.type === TOKEN.END_BLOCK || this._flags.last_token.text === '.' || this._flags.last_token.type === TOKEN.COMMA) {
+ // do nothing on (( and )( and ][ and ]( and .(
+ // TODO: Consider whether forcing this is required. Review failing tests when removed.
+ this.allow_wrap_or_preserved_newline(current_token, current_token.newlines);
+ }
+
+ this.print_token(current_token);
+ this.set_mode(next_mode);
+ if (this._options.space_in_paren) {
+ this._output.space_before_token = true;
+ }
+
+ // In all cases, if we newline while inside an expression it should be indented.
+ this.indent();
+};
+
+Beautifier.prototype.handle_end_expr = function(current_token) {
+ // statements inside expressions are not valid syntax, but...
+ // statements must all be closed when their container closes
+ while (this._flags.mode === MODE.Statement) {
+ this.restore_mode();
+ }
+
+ this.handle_whitespace_and_comments(current_token);
+
+ if (this._flags.multiline_frame) {
+ this.allow_wrap_or_preserved_newline(current_token,
+ current_token.text === ']' && is_array(this._flags.mode) && !this._options.keep_array_indentation);
+ }
+
+ if (this._options.space_in_paren) {
+ if (this._flags.last_token.type === TOKEN.START_EXPR && !this._options.space_in_empty_paren) {
+ // () [] no inner space in empty parens like these, ever, ref #320
+ this._output.trim();
+ this._output.space_before_token = false;
+ } else {
+ this._output.space_before_token = true;
+ }
+ }
+ this.deindent();
+ this.print_token(current_token);
+ this.restore_mode();
+
+ remove_redundant_indentation(this._output, this._previous_flags);
+
+ // do {} while () // no statement required after
+ if (this._flags.do_while && this._previous_flags.mode === MODE.Conditional) {
+ this._previous_flags.mode = MODE.Expression;
+ this._flags.do_block = false;
+ this._flags.do_while = false;
+
+ }
+};
+
+Beautifier.prototype.handle_start_block = function(current_token) {
+ this.handle_whitespace_and_comments(current_token);
+
+ // Check if this is should be treated as a ObjectLiteral
+ var next_token = this._tokens.peek();
+ var second_token = this._tokens.peek(1);
+ if (this._flags.last_word === 'switch' && this._flags.last_token.type === TOKEN.END_EXPR) {
+ this.set_mode(MODE.BlockStatement);
+ this._flags.in_case_statement = true;
+ } else if (this._flags.case_body) {
+ this.set_mode(MODE.BlockStatement);
+ } else if (second_token && (
+ (in_array(second_token.text, [':', ',']) && in_array(next_token.type, [TOKEN.STRING, TOKEN.WORD, TOKEN.RESERVED])) ||
+ (in_array(next_token.text, ['get', 'set', '...']) && in_array(second_token.type, [TOKEN.WORD, TOKEN.RESERVED]))
+ )) {
+ // We don't support TypeScript,but we didn't break it for a very long time.
+ // We'll try to keep not breaking it.
+ if (!in_array(this._last_last_text, ['class', 'interface'])) {
+ this.set_mode(MODE.ObjectLiteral);
+ } else {
+ this.set_mode(MODE.BlockStatement);
+ }
+ } else if (this._flags.last_token.type === TOKEN.OPERATOR && this._flags.last_token.text === '=>') {
+ // arrow function: (param1, paramN) => { statements }
+ this.set_mode(MODE.BlockStatement);
+ } else if (in_array(this._flags.last_token.type, [TOKEN.EQUALS, TOKEN.START_EXPR, TOKEN.COMMA, TOKEN.OPERATOR]) ||
+ reserved_array(this._flags.last_token, ['return', 'throw', 'import', 'default'])
+ ) {
+ // Detecting shorthand function syntax is difficult by scanning forward,
+ // so check the surrounding context.
+ // If the block is being returned, imported, export default, passed as arg,
+ // assigned with = or assigned in a nested object, treat as an ObjectLiteral.
+ this.set_mode(MODE.ObjectLiteral);
+ } else {
+ this.set_mode(MODE.BlockStatement);
+ }
+
+ var empty_braces = !next_token.comments_before && next_token.text === '}';
+ var empty_anonymous_function = empty_braces && this._flags.last_word === 'function' &&
+ this._flags.last_token.type === TOKEN.END_EXPR;
+
+ if (this._options.brace_preserve_inline) // check for inline, set inline_frame if so
+ {
+ // search forward for a newline wanted inside this block
+ var index = 0;
+ var check_token = null;
+ this._flags.inline_frame = true;
+ do {
+ index += 1;
+ check_token = this._tokens.peek(index - 1);
+ if (check_token.newlines) {
+ this._flags.inline_frame = false;
+ break;
+ }
+ } while (check_token.type !== TOKEN.EOF &&
+ !(check_token.type === TOKEN.END_BLOCK && check_token.opened === current_token));
+ }
+
+ if ((this._options.brace_style === "expand" ||
+ (this._options.brace_style === "none" && current_token.newlines)) &&
+ !this._flags.inline_frame) {
+ if (this._flags.last_token.type !== TOKEN.OPERATOR &&
+ (empty_anonymous_function ||
+ this._flags.last_token.type === TOKEN.EQUALS ||
+ (reserved_array(this._flags.last_token, special_words) && this._flags.last_token.text !== 'else'))) {
+ this._output.space_before_token = true;
+ } else {
+ this.print_newline(false, true);
+ }
+ } else { // collapse || inline_frame
+ if (is_array(this._previous_flags.mode) && (this._flags.last_token.type === TOKEN.START_EXPR || this._flags.last_token.type === TOKEN.COMMA)) {
+ if (this._flags.last_token.type === TOKEN.COMMA || this._options.space_in_paren) {
+ this._output.space_before_token = true;
+ }
+
+ if (this._flags.last_token.type === TOKEN.COMMA || (this._flags.last_token.type === TOKEN.START_EXPR && this._flags.inline_frame)) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ this._previous_flags.multiline_frame = this._previous_flags.multiline_frame || this._flags.multiline_frame;
+ this._flags.multiline_frame = false;
+ }
+ }
+ if (this._flags.last_token.type !== TOKEN.OPERATOR && this._flags.last_token.type !== TOKEN.START_EXPR) {
+ if (this._flags.last_token.type === TOKEN.START_BLOCK && !this._flags.inline_frame) {
+ this.print_newline();
+ } else {
+ this._output.space_before_token = true;
+ }
+ }
+ }
+ this.print_token(current_token);
+ this.indent();
+
+ // Except for specific cases, open braces are followed by a new line.
+ if (!empty_braces && !(this._options.brace_preserve_inline && this._flags.inline_frame)) {
+ this.print_newline();
+ }
+};
+
+Beautifier.prototype.handle_end_block = function(current_token) {
+ // statements must all be closed when their container closes
+ this.handle_whitespace_and_comments(current_token);
+
+ while (this._flags.mode === MODE.Statement) {
+ this.restore_mode();
+ }
+
+ var empty_braces = this._flags.last_token.type === TOKEN.START_BLOCK;
+
+ if (this._flags.inline_frame && !empty_braces) { // try inline_frame (only set if this._options.braces-preserve-inline) first
+ this._output.space_before_token = true;
+ } else if (this._options.brace_style === "expand") {
+ if (!empty_braces) {
+ this.print_newline();
+ }
+ } else {
+ // skip {}
+ if (!empty_braces) {
+ if (is_array(this._flags.mode) && this._options.keep_array_indentation) {
+ // we REALLY need a newline here, but newliner would skip that
+ this._options.keep_array_indentation = false;
+ this.print_newline();
+ this._options.keep_array_indentation = true;
+
+ } else {
+ this.print_newline();
+ }
+ }
+ }
+ this.restore_mode();
+ this.print_token(current_token);
+};
+
+Beautifier.prototype.handle_word = function(current_token) {
+ if (current_token.type === TOKEN.RESERVED) {
+ if (in_array(current_token.text, ['set', 'get']) && this._flags.mode !== MODE.ObjectLiteral) {
+ current_token.type = TOKEN.WORD;
+ } else if (current_token.text === 'import' && this._tokens.peek().text === '(') {
+ current_token.type = TOKEN.WORD;
+ } else if (in_array(current_token.text, ['as', 'from']) && !this._flags.import_block) {
+ current_token.type = TOKEN.WORD;
+ } else if (this._flags.mode === MODE.ObjectLiteral) {
+ var next_token = this._tokens.peek();
+ if (next_token.text === ':') {
+ current_token.type = TOKEN.WORD;
+ }
+ }
+ }
+
+ if (this.start_of_statement(current_token)) {
+ // The conditional starts the statement if appropriate.
+ if (reserved_array(this._flags.last_token, ['var', 'let', 'const']) && current_token.type === TOKEN.WORD) {
+ this._flags.declaration_statement = true;
+ }
+ } else if (current_token.newlines && !is_expression(this._flags.mode) &&
+ (this._flags.last_token.type !== TOKEN.OPERATOR || (this._flags.last_token.text === '--' || this._flags.last_token.text === '++')) &&
+ this._flags.last_token.type !== TOKEN.EQUALS &&
+ (this._options.preserve_newlines || !reserved_array(this._flags.last_token, ['var', 'let', 'const', 'set', 'get']))) {
+ this.handle_whitespace_and_comments(current_token);
+ this.print_newline();
+ } else {
+ this.handle_whitespace_and_comments(current_token);
+ }
+
+ if (this._flags.do_block && !this._flags.do_while) {
+ if (reserved_word(current_token, 'while')) {
+ // do {} ## while ()
+ this._output.space_before_token = true;
+ this.print_token(current_token);
+ this._output.space_before_token = true;
+ this._flags.do_while = true;
+ return;
+ } else {
+ // do {} should always have while as the next word.
+ // if we don't see the expected while, recover
+ this.print_newline();
+ this._flags.do_block = false;
+ }
+ }
+
+ // if may be followed by else, or not
+ // Bare/inline ifs are tricky
+ // Need to unwind the modes correctly: if (a) if (b) c(); else d(); else e();
+ if (this._flags.if_block) {
+ if (!this._flags.else_block && reserved_word(current_token, 'else')) {
+ this._flags.else_block = true;
+ } else {
+ while (this._flags.mode === MODE.Statement) {
+ this.restore_mode();
+ }
+ this._flags.if_block = false;
+ this._flags.else_block = false;
+ }
+ }
+
+ if (this._flags.in_case_statement && reserved_array(current_token, ['case', 'default'])) {
+ this.print_newline();
+ if (this._flags.last_token.type !== TOKEN.END_BLOCK && (this._flags.case_body || this._options.jslint_happy)) {
+ // switch cases following one another
+ this.deindent();
+ }
+ this._flags.case_body = false;
+
+ this.print_token(current_token);
+ this._flags.in_case = true;
+ return;
+ }
+
+ if (this._flags.last_token.type === TOKEN.COMMA || this._flags.last_token.type === TOKEN.START_EXPR || this._flags.last_token.type === TOKEN.EQUALS || this._flags.last_token.type === TOKEN.OPERATOR) {
+ if (!this.start_of_object_property()) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+ }
+
+ if (reserved_word(current_token, 'function')) {
+ if (in_array(this._flags.last_token.text, ['}', ';']) ||
+ (this._output.just_added_newline() && !(in_array(this._flags.last_token.text, ['(', '[', '{', ':', '=', ',']) || this._flags.last_token.type === TOKEN.OPERATOR))) {
+ // make sure there is a nice clean space of at least one blank line
+ // before a new function definition
+ if (!this._output.just_added_blankline() && !current_token.comments_before) {
+ this.print_newline();
+ this.print_newline(true);
+ }
+ }
+ if (this._flags.last_token.type === TOKEN.RESERVED || this._flags.last_token.type === TOKEN.WORD) {
+ if (reserved_array(this._flags.last_token, ['get', 'set', 'new', 'export']) ||
+ reserved_array(this._flags.last_token, newline_restricted_tokens)) {
+ this._output.space_before_token = true;
+ } else if (reserved_word(this._flags.last_token, 'default') && this._last_last_text === 'export') {
+ this._output.space_before_token = true;
+ } else if (this._flags.last_token.text === 'declare') {
+ // accomodates Typescript declare function formatting
+ this._output.space_before_token = true;
+ } else {
+ this.print_newline();
+ }
+ } else if (this._flags.last_token.type === TOKEN.OPERATOR || this._flags.last_token.text === '=') {
+ // foo = function
+ this._output.space_before_token = true;
+ } else if (!this._flags.multiline_frame && (is_expression(this._flags.mode) || is_array(this._flags.mode))) {
+ // (function
+ } else {
+ this.print_newline();
+ }
+
+ this.print_token(current_token);
+ this._flags.last_word = current_token.text;
+ return;
+ }
+
+ var prefix = 'NONE';
+
+ if (this._flags.last_token.type === TOKEN.END_BLOCK) {
+
+ if (this._previous_flags.inline_frame) {
+ prefix = 'SPACE';
+ } else if (!reserved_array(current_token, ['else', 'catch', 'finally', 'from'])) {
+ prefix = 'NEWLINE';
+ } else {
+ if (this._options.brace_style === "expand" ||
+ this._options.brace_style === "end-expand" ||
+ (this._options.brace_style === "none" && current_token.newlines)) {
+ prefix = 'NEWLINE';
+ } else {
+ prefix = 'SPACE';
+ this._output.space_before_token = true;
+ }
+ }
+ } else if (this._flags.last_token.type === TOKEN.SEMICOLON && this._flags.mode === MODE.BlockStatement) {
+ // TODO: Should this be for STATEMENT as well?
+ prefix = 'NEWLINE';
+ } else if (this._flags.last_token.type === TOKEN.SEMICOLON && is_expression(this._flags.mode)) {
+ prefix = 'SPACE';
+ } else if (this._flags.last_token.type === TOKEN.STRING) {
+ prefix = 'NEWLINE';
+ } else if (this._flags.last_token.type === TOKEN.RESERVED || this._flags.last_token.type === TOKEN.WORD ||
+ (this._flags.last_token.text === '*' &&
+ (in_array(this._last_last_text, ['function', 'yield']) ||
+ (this._flags.mode === MODE.ObjectLiteral && in_array(this._last_last_text, ['{', ',']))))) {
+ prefix = 'SPACE';
+ } else if (this._flags.last_token.type === TOKEN.START_BLOCK) {
+ if (this._flags.inline_frame) {
+ prefix = 'SPACE';
+ } else {
+ prefix = 'NEWLINE';
+ }
+ } else if (this._flags.last_token.type === TOKEN.END_EXPR) {
+ this._output.space_before_token = true;
+ prefix = 'NEWLINE';
+ }
+
+ if (reserved_array(current_token, line_starters) && this._flags.last_token.text !== ')') {
+ if (this._flags.inline_frame || this._flags.last_token.text === 'else' || this._flags.last_token.text === 'export') {
+ prefix = 'SPACE';
+ } else {
+ prefix = 'NEWLINE';
+ }
+
+ }
+
+ if (reserved_array(current_token, ['else', 'catch', 'finally'])) {
+ if ((!(this._flags.last_token.type === TOKEN.END_BLOCK && this._previous_flags.mode === MODE.BlockStatement) ||
+ this._options.brace_style === "expand" ||
+ this._options.brace_style === "end-expand" ||
+ (this._options.brace_style === "none" && current_token.newlines)) &&
+ !this._flags.inline_frame) {
+ this.print_newline();
+ } else {
+ this._output.trim(true);
+ var line = this._output.current_line;
+ // If we trimmed and there's something other than a close block before us
+ // put a newline back in. Handles '} // comment' scenario.
+ if (line.last() !== '}') {
+ this.print_newline();
+ }
+ this._output.space_before_token = true;
+ }
+ } else if (prefix === 'NEWLINE') {
+ if (reserved_array(this._flags.last_token, special_words)) {
+ // no newline between 'return nnn'
+ this._output.space_before_token = true;
+ } else if (this._flags.last_token.text === 'declare' && reserved_array(current_token, ['var', 'let', 'const'])) {
+ // accomodates Typescript declare formatting
+ this._output.space_before_token = true;
+ } else if (this._flags.last_token.type !== TOKEN.END_EXPR) {
+ if ((this._flags.last_token.type !== TOKEN.START_EXPR || !reserved_array(current_token, ['var', 'let', 'const'])) && this._flags.last_token.text !== ':') {
+ // no need to force newline on 'var': for (var x = 0...)
+ if (reserved_word(current_token, 'if') && reserved_word(current_token.previous, 'else')) {
+ // no newline for } else if {
+ this._output.space_before_token = true;
+ } else {
+ this.print_newline();
+ }
+ }
+ } else if (reserved_array(current_token, line_starters) && this._flags.last_token.text !== ')') {
+ this.print_newline();
+ }
+ } else if (this._flags.multiline_frame && is_array(this._flags.mode) && this._flags.last_token.text === ',' && this._last_last_text === '}') {
+ this.print_newline(); // }, in lists get a newline treatment
+ } else if (prefix === 'SPACE') {
+ this._output.space_before_token = true;
+ }
+ if (current_token.previous && (current_token.previous.type === TOKEN.WORD || current_token.previous.type === TOKEN.RESERVED)) {
+ this._output.space_before_token = true;
+ }
+ this.print_token(current_token);
+ this._flags.last_word = current_token.text;
+
+ if (current_token.type === TOKEN.RESERVED) {
+ if (current_token.text === 'do') {
+ this._flags.do_block = true;
+ } else if (current_token.text === 'if') {
+ this._flags.if_block = true;
+ } else if (current_token.text === 'import') {
+ this._flags.import_block = true;
+ } else if (this._flags.import_block && reserved_word(current_token, 'from')) {
+ this._flags.import_block = false;
+ }
+ }
+};
+
+Beautifier.prototype.handle_semicolon = function(current_token) {
+ if (this.start_of_statement(current_token)) {
+ // The conditional starts the statement if appropriate.
+ // Semicolon can be the start (and end) of a statement
+ this._output.space_before_token = false;
+ } else {
+ this.handle_whitespace_and_comments(current_token);
+ }
+
+ var next_token = this._tokens.peek();
+ while (this._flags.mode === MODE.Statement &&
+ !(this._flags.if_block && reserved_word(next_token, 'else')) &&
+ !this._flags.do_block) {
+ this.restore_mode();
+ }
+
+ // hacky but effective for the moment
+ if (this._flags.import_block) {
+ this._flags.import_block = false;
+ }
+ this.print_token(current_token);
+};
+
+Beautifier.prototype.handle_string = function(current_token) {
+ if (this.start_of_statement(current_token)) {
+ // The conditional starts the statement if appropriate.
+ // One difference - strings want at least a space before
+ this._output.space_before_token = true;
+ } else {
+ this.handle_whitespace_and_comments(current_token);
+ if (this._flags.last_token.type === TOKEN.RESERVED || this._flags.last_token.type === TOKEN.WORD || this._flags.inline_frame) {
+ this._output.space_before_token = true;
+ } else if (this._flags.last_token.type === TOKEN.COMMA || this._flags.last_token.type === TOKEN.START_EXPR || this._flags.last_token.type === TOKEN.EQUALS || this._flags.last_token.type === TOKEN.OPERATOR) {
+ if (!this.start_of_object_property()) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+ } else {
+ this.print_newline();
+ }
+ }
+ this.print_token(current_token);
+};
+
+Beautifier.prototype.handle_equals = function(current_token) {
+ if (this.start_of_statement(current_token)) {
+ // The conditional starts the statement if appropriate.
+ } else {
+ this.handle_whitespace_and_comments(current_token);
+ }
+
+ if (this._flags.declaration_statement) {
+ // just got an '=' in a var-line, different formatting/line-breaking, etc will now be done
+ this._flags.declaration_assignment = true;
+ }
+ this._output.space_before_token = true;
+ this.print_token(current_token);
+ this._output.space_before_token = true;
+};
+
+Beautifier.prototype.handle_comma = function(current_token) {
+ this.handle_whitespace_and_comments(current_token, true);
+
+ this.print_token(current_token);
+ this._output.space_before_token = true;
+ if (this._flags.declaration_statement) {
+ if (is_expression(this._flags.parent.mode)) {
+ // do not break on comma, for(var a = 1, b = 2)
+ this._flags.declaration_assignment = false;
+ }
+
+ if (this._flags.declaration_assignment) {
+ this._flags.declaration_assignment = false;
+ this.print_newline(false, true);
+ } else if (this._options.comma_first) {
+ // for comma-first, we want to allow a newline before the comma
+ // to turn into a newline after the comma, which we will fixup later
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+ } else if (this._flags.mode === MODE.ObjectLiteral ||
+ (this._flags.mode === MODE.Statement && this._flags.parent.mode === MODE.ObjectLiteral)) {
+ if (this._flags.mode === MODE.Statement) {
+ this.restore_mode();
+ }
+
+ if (!this._flags.inline_frame) {
+ this.print_newline();
+ }
+ } else if (this._options.comma_first) {
+ // EXPR or DO_BLOCK
+ // for comma-first, we want to allow a newline before the comma
+ // to turn into a newline after the comma, which we will fixup later
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+};
+
+Beautifier.prototype.handle_operator = function(current_token) {
+ var isGeneratorAsterisk = current_token.text === '*' &&
+ (reserved_array(this._flags.last_token, ['function', 'yield']) ||
+ (in_array(this._flags.last_token.type, [TOKEN.START_BLOCK, TOKEN.COMMA, TOKEN.END_BLOCK, TOKEN.SEMICOLON]))
+ );
+ var isUnary = in_array(current_token.text, ['-', '+']) && (
+ in_array(this._flags.last_token.type, [TOKEN.START_BLOCK, TOKEN.START_EXPR, TOKEN.EQUALS, TOKEN.OPERATOR]) ||
+ in_array(this._flags.last_token.text, line_starters) ||
+ this._flags.last_token.text === ','
+ );
+
+ if (this.start_of_statement(current_token)) {
+ // The conditional starts the statement if appropriate.
+ } else {
+ var preserve_statement_flags = !isGeneratorAsterisk;
+ this.handle_whitespace_and_comments(current_token, preserve_statement_flags);
+ }
+
+ if (reserved_array(this._flags.last_token, special_words)) {
+ // "return" had a special handling in TK_WORD. Now we need to return the favor
+ this._output.space_before_token = true;
+ this.print_token(current_token);
+ return;
+ }
+
+ // hack for actionscript's import .*;
+ if (current_token.text === '*' && this._flags.last_token.type === TOKEN.DOT) {
+ this.print_token(current_token);
+ return;
+ }
+
+ if (current_token.text === '::') {
+ // no spaces around exotic namespacing syntax operator
+ this.print_token(current_token);
+ return;
+ }
+
+ // Allow line wrapping between operators when operator_position is
+ // set to before or preserve
+ if (this._flags.last_token.type === TOKEN.OPERATOR && in_array(this._options.operator_position, OPERATOR_POSITION_BEFORE_OR_PRESERVE)) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+
+ if (current_token.text === ':' && this._flags.in_case) {
+ this.print_token(current_token);
+
+ this._flags.in_case = false;
+ this._flags.case_body = true;
+ if (this._tokens.peek().type !== TOKEN.START_BLOCK) {
+ this.indent();
+ this.print_newline();
+ } else {
+ this._output.space_before_token = true;
+ }
+ return;
+ }
+
+ var space_before = true;
+ var space_after = true;
+ var in_ternary = false;
+ if (current_token.text === ':') {
+ if (this._flags.ternary_depth === 0) {
+ // Colon is invalid javascript outside of ternary and object, but do our best to guess what was meant.
+ space_before = false;
+ } else {
+ this._flags.ternary_depth -= 1;
+ in_ternary = true;
+ }
+ } else if (current_token.text === '?') {
+ this._flags.ternary_depth += 1;
+ }
+
+ // let's handle the operator_position option prior to any conflicting logic
+ if (!isUnary && !isGeneratorAsterisk && this._options.preserve_newlines && in_array(current_token.text, positionable_operators)) {
+ var isColon = current_token.text === ':';
+ var isTernaryColon = (isColon && in_ternary);
+ var isOtherColon = (isColon && !in_ternary);
+
+ switch (this._options.operator_position) {
+ case OPERATOR_POSITION.before_newline:
+ // if the current token is : and it's not a ternary statement then we set space_before to false
+ this._output.space_before_token = !isOtherColon;
+
+ this.print_token(current_token);
+
+ if (!isColon || isTernaryColon) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+
+ this._output.space_before_token = true;
+ return;
+
+ case OPERATOR_POSITION.after_newline:
+ // if the current token is anything but colon, or (via deduction) it's a colon and in a ternary statement,
+ // then print a newline.
+
+ this._output.space_before_token = true;
+
+ if (!isColon || isTernaryColon) {
+ if (this._tokens.peek().newlines) {
+ this.print_newline(false, true);
+ } else {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+ } else {
+ this._output.space_before_token = false;
+ }
+
+ this.print_token(current_token);
+
+ this._output.space_before_token = true;
+ return;
+
+ case OPERATOR_POSITION.preserve_newline:
+ if (!isOtherColon) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+
+ // if we just added a newline, or the current token is : and it's not a ternary statement,
+ // then we set space_before to false
+ space_before = !(this._output.just_added_newline() || isOtherColon);
+
+ this._output.space_before_token = space_before;
+ this.print_token(current_token);
+ this._output.space_before_token = true;
+ return;
+ }
+ }
+
+ if (isGeneratorAsterisk) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ space_before = false;
+ var next_token = this._tokens.peek();
+ space_after = next_token && in_array(next_token.type, [TOKEN.WORD, TOKEN.RESERVED]);
+ } else if (current_token.text === '...') {
+ this.allow_wrap_or_preserved_newline(current_token);
+ space_before = this._flags.last_token.type === TOKEN.START_BLOCK;
+ space_after = false;
+ } else if (in_array(current_token.text, ['--', '++', '!', '~']) || isUnary) {
+ // unary operators (and binary +/- pretending to be unary) special cases
+ if (this._flags.last_token.type === TOKEN.COMMA || this._flags.last_token.type === TOKEN.START_EXPR) {
+ this.allow_wrap_or_preserved_newline(current_token);
+ }
+
+ space_before = false;
+ space_after = false;
+
+ // http://www.ecma-international.org/ecma-262/5.1/#sec-7.9.1
+ // if there is a newline between -- or ++ and anything else we should preserve it.
+ if (current_token.newlines && (current_token.text === '--' || current_token.text === '++')) {
+ this.print_newline(false, true);
+ }
+
+ if (this._flags.last_token.text === ';' && is_expression(this._flags.mode)) {
+ // for (;; ++i)
+ // ^^^
+ space_before = true;
+ }
+
+ if (this._flags.last_token.type === TOKEN.RESERVED) {
+ space_before = true;
+ } else if (this._flags.last_token.type === TOKEN.END_EXPR) {
+ space_before = !(this._flags.last_token.text === ']' && (current_token.text === '--' || current_token.text === '++'));
+ } else if (this._flags.last_token.type === TOKEN.OPERATOR) {
+ // a++ + ++b;
+ // a - -b
+ space_before = in_array(current_token.text, ['--', '-', '++', '+']) && in_array(this._flags.last_token.text, ['--', '-', '++', '+']);
+ // + and - are not unary when preceeded by -- or ++ operator
+ // a-- + b
+ // a * +b
+ // a - -b
+ if (in_array(current_token.text, ['+', '-']) && in_array(this._flags.last_token.text, ['--', '++'])) {
+ space_after = true;
+ }
+ }
+
+
+ if (((this._flags.mode === MODE.BlockStatement && !this._flags.inline_frame) || this._flags.mode === MODE.Statement) &&
+ (this._flags.last_token.text === '{' || this._flags.last_token.text === ';')) {
+ // { foo; --i }
+ // foo(); --bar;
+ this.print_newline();
+ }
+ }
+
+ this._output.space_before_token = this._output.space_before_token || space_before;
+ this.print_token(current_token);
+ this._output.space_before_token = space_after;
+};
+
+Beautifier.prototype.handle_block_comment = function(current_token, preserve_statement_flags) {
+ if (this._output.raw) {
+ this._output.add_raw_token(current_token);
+ if (current_token.directives && current_token.directives.preserve === 'end') {
+ // If we're testing the raw output behavior, do not allow a directive to turn it off.
+ this._output.raw = this._options.test_output_raw;
+ }
+ return;
+ }
+
+ if (current_token.directives) {
+ this.print_newline(false, preserve_statement_flags);
+ this.print_token(current_token);
+ if (current_token.directives.preserve === 'start') {
+ this._output.raw = true;
+ }
+ this.print_newline(false, true);
+ return;
+ }
+
+ // inline block
+ if (!acorn.newline.test(current_token.text) && !current_token.newlines) {
+ this._output.space_before_token = true;
+ this.print_token(current_token);
+ this._output.space_before_token = true;
+ return;
+ } else {
+ this.print_block_commment(current_token, preserve_statement_flags);
+ }
+};
+
+Beautifier.prototype.print_block_commment = function(current_token, preserve_statement_flags) {
+ var lines = split_linebreaks(current_token.text);
+ var j; // iterator for this case
+ var javadoc = false;
+ var starless = false;
+ var lastIndent = current_token.whitespace_before;
+ var lastIndentLength = lastIndent.length;
+
+ // block comment starts with a new line
+ this.print_newline(false, preserve_statement_flags);
+
+ // first line always indented
+ this.print_token_line_indentation(current_token);
+ this._output.add_token(lines[0]);
+ this.print_newline(false, preserve_statement_flags);
+
+
+ if (lines.length > 1) {
+ lines = lines.slice(1);
+ javadoc = all_lines_start_with(lines, '*');
+ starless = each_line_matches_indent(lines, lastIndent);
+
+ if (javadoc) {
+ this._flags.alignment = 1;
+ }
+
+ for (j = 0; j < lines.length; j++) {
+ if (javadoc) {
+ // javadoc: reformat and re-indent
+ this.print_token_line_indentation(current_token);
+ this._output.add_token(ltrim(lines[j]));
+ } else if (starless && lines[j]) {
+ // starless: re-indent non-empty content, avoiding trim
+ this.print_token_line_indentation(current_token);
+ this._output.add_token(lines[j].substring(lastIndentLength));
+ } else {
+ // normal comments output raw
+ this._output.current_line.set_indent(-1);
+ this._output.add_token(lines[j]);
+ }
+
+ // for comments on their own line or more than one line, make sure there's a new line after
+ this.print_newline(false, preserve_statement_flags);
+ }
+
+ this._flags.alignment = 0;
+ }
+};
+
+
+Beautifier.prototype.handle_comment = function(current_token, preserve_statement_flags) {
+ if (current_token.newlines) {
+ this.print_newline(false, preserve_statement_flags);
+ } else {
+ this._output.trim(true);
+ }
+
+ this._output.space_before_token = true;
+ this.print_token(current_token);
+ this.print_newline(false, preserve_statement_flags);
+};
+
+Beautifier.prototype.handle_dot = function(current_token) {
+ if (this.start_of_statement(current_token)) {
+ // The conditional starts the statement if appropriate.
+ } else {
+ this.handle_whitespace_and_comments(current_token, true);
+ }
+
+ if (reserved_array(this._flags.last_token, special_words)) {
+ this._output.space_before_token = false;
+ } else {
+ // allow preserved newlines before dots in general
+ // force newlines on dots after close paren when break_chained - for bar().baz()
+ this.allow_wrap_or_preserved_newline(current_token,
+ this._flags.last_token.text === ')' && this._options.break_chained_methods);
+ }
+
+ // Only unindent chained method dot if this dot starts a new line.
+ // Otherwise the automatic extra indentation removal will handle the over indent
+ if (this._options.unindent_chained_methods && this._output.just_added_newline()) {
+ this.deindent();
+ }
+
+ this.print_token(current_token);
+};
+
+Beautifier.prototype.handle_unknown = function(current_token, preserve_statement_flags) {
+ this.print_token(current_token);
+
+ if (current_token.text[current_token.text.length - 1] === '\n') {
+ this.print_newline(false, preserve_statement_flags);
+ }
+};
+
+Beautifier.prototype.handle_eof = function(current_token) {
+ // Unwind any open statements
+ while (this._flags.mode === MODE.Statement) {
+ this.restore_mode();
+ }
+ this.handle_whitespace_and_comments(current_token);
+};
+
+module.exports.Beautifier = Beautifier;
+
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function OutputLine(parent) {
+ this.__parent = parent;
+ this.__character_count = 0;
+ // use indent_count as a marker for this.__lines that have preserved indentation
+ this.__indent_count = -1;
+ this.__alignment_count = 0;
+ this.__wrap_point_index = 0;
+ this.__wrap_point_character_count = 0;
+ this.__wrap_point_indent_count = -1;
+ this.__wrap_point_alignment_count = 0;
+
+ this.__items = [];
+}
+
+OutputLine.prototype.clone_empty = function() {
+ var line = new OutputLine(this.__parent);
+ line.set_indent(this.__indent_count, this.__alignment_count);
+ return line;
+};
+
+OutputLine.prototype.item = function(index) {
+ if (index < 0) {
+ return this.__items[this.__items.length + index];
+ } else {
+ return this.__items[index];
+ }
+};
+
+OutputLine.prototype.has_match = function(pattern) {
+ for (var lastCheckedOutput = this.__items.length - 1; lastCheckedOutput >= 0; lastCheckedOutput--) {
+ if (this.__items[lastCheckedOutput].match(pattern)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+OutputLine.prototype.set_indent = function(indent, alignment) {
+ if (this.is_empty()) {
+ this.__indent_count = indent || 0;
+ this.__alignment_count = alignment || 0;
+ this.__character_count = this.__parent.get_indent_size(this.__indent_count, this.__alignment_count);
+ }
+};
+
+OutputLine.prototype._set_wrap_point = function() {
+ if (this.__parent.wrap_line_length) {
+ this.__wrap_point_index = this.__items.length;
+ this.__wrap_point_character_count = this.__character_count;
+ this.__wrap_point_indent_count = this.__parent.next_line.__indent_count;
+ this.__wrap_point_alignment_count = this.__parent.next_line.__alignment_count;
+ }
+};
+
+OutputLine.prototype._should_wrap = function() {
+ return this.__wrap_point_index &&
+ this.__character_count > this.__parent.wrap_line_length &&
+ this.__wrap_point_character_count > this.__parent.next_line.__character_count;
+};
+
+OutputLine.prototype._allow_wrap = function() {
+ if (this._should_wrap()) {
+ this.__parent.add_new_line();
+ var next = this.__parent.current_line;
+ next.set_indent(this.__wrap_point_indent_count, this.__wrap_point_alignment_count);
+ next.__items = this.__items.slice(this.__wrap_point_index);
+ this.__items = this.__items.slice(0, this.__wrap_point_index);
+
+ next.__character_count += this.__character_count - this.__wrap_point_character_count;
+ this.__character_count = this.__wrap_point_character_count;
+
+ if (next.__items[0] === " ") {
+ next.__items.splice(0, 1);
+ next.__character_count -= 1;
+ }
+ return true;
+ }
+ return false;
+};
+
+OutputLine.prototype.is_empty = function() {
+ return this.__items.length === 0;
+};
+
+OutputLine.prototype.last = function() {
+ if (!this.is_empty()) {
+ return this.__items[this.__items.length - 1];
+ } else {
+ return null;
+ }
+};
+
+OutputLine.prototype.push = function(item) {
+ this.__items.push(item);
+ var last_newline_index = item.lastIndexOf('\n');
+ if (last_newline_index !== -1) {
+ this.__character_count = item.length - last_newline_index;
+ } else {
+ this.__character_count += item.length;
+ }
+};
+
+OutputLine.prototype.pop = function() {
+ var item = null;
+ if (!this.is_empty()) {
+ item = this.__items.pop();
+ this.__character_count -= item.length;
+ }
+ return item;
+};
+
+
+OutputLine.prototype._remove_indent = function() {
+ if (this.__indent_count > 0) {
+ this.__indent_count -= 1;
+ this.__character_count -= this.__parent.indent_size;
+ }
+};
+
+OutputLine.prototype._remove_wrap_indent = function() {
+ if (this.__wrap_point_indent_count > 0) {
+ this.__wrap_point_indent_count -= 1;
+ }
+};
+OutputLine.prototype.trim = function() {
+ while (this.last() === ' ') {
+ this.__items.pop();
+ this.__character_count -= 1;
+ }
+};
+
+OutputLine.prototype.toString = function() {
+ var result = '';
+ if (this.is_empty()) {
+ if (this.__parent.indent_empty_lines) {
+ result = this.__parent.get_indent_string(this.__indent_count);
+ }
+ } else {
+ result = this.__parent.get_indent_string(this.__indent_count, this.__alignment_count);
+ result += this.__items.join('');
+ }
+ return result;
+};
+
+function IndentStringCache(options, baseIndentString) {
+ this.__cache = [''];
+ this.__indent_size = options.indent_size;
+ this.__indent_string = options.indent_char;
+ if (!options.indent_with_tabs) {
+ this.__indent_string = new Array(options.indent_size + 1).join(options.indent_char);
+ }
+
+ // Set to null to continue support for auto detection of base indent
+ baseIndentString = baseIndentString || '';
+ if (options.indent_level > 0) {
+ baseIndentString = new Array(options.indent_level + 1).join(this.__indent_string);
+ }
+
+ this.__base_string = baseIndentString;
+ this.__base_string_length = baseIndentString.length;
+}
+
+IndentStringCache.prototype.get_indent_size = function(indent, column) {
+ var result = this.__base_string_length;
+ column = column || 0;
+ if (indent < 0) {
+ result = 0;
+ }
+ result += indent * this.__indent_size;
+ result += column;
+ return result;
+};
+
+IndentStringCache.prototype.get_indent_string = function(indent_level, column) {
+ var result = this.__base_string;
+ column = column || 0;
+ if (indent_level < 0) {
+ indent_level = 0;
+ result = '';
+ }
+ column += indent_level * this.__indent_size;
+ this.__ensure_cache(column);
+ result += this.__cache[column];
+ return result;
+};
+
+IndentStringCache.prototype.__ensure_cache = function(column) {
+ while (column >= this.__cache.length) {
+ this.__add_column();
+ }
+};
+
+IndentStringCache.prototype.__add_column = function() {
+ var column = this.__cache.length;
+ var indent = 0;
+ var result = '';
+ if (this.__indent_size && column >= this.__indent_size) {
+ indent = Math.floor(column / this.__indent_size);
+ column -= indent * this.__indent_size;
+ result = new Array(indent + 1).join(this.__indent_string);
+ }
+ if (column) {
+ result += new Array(column + 1).join(' ');
+ }
+
+ this.__cache.push(result);
+};
+
+function Output(options, baseIndentString) {
+ this.__indent_cache = new IndentStringCache(options, baseIndentString);
+ this.raw = false;
+ this._end_with_newline = options.end_with_newline;
+ this.indent_size = options.indent_size;
+ this.wrap_line_length = options.wrap_line_length;
+ this.indent_empty_lines = options.indent_empty_lines;
+ this.__lines = [];
+ this.previous_line = null;
+ this.current_line = null;
+ this.next_line = new OutputLine(this);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = false;
+ // initialize
+ this.__add_outputline();
+}
+
+Output.prototype.__add_outputline = function() {
+ this.previous_line = this.current_line;
+ this.current_line = this.next_line.clone_empty();
+ this.__lines.push(this.current_line);
+};
+
+Output.prototype.get_line_number = function() {
+ return this.__lines.length;
+};
+
+Output.prototype.get_indent_string = function(indent, column) {
+ return this.__indent_cache.get_indent_string(indent, column);
+};
+
+Output.prototype.get_indent_size = function(indent, column) {
+ return this.__indent_cache.get_indent_size(indent, column);
+};
+
+Output.prototype.is_empty = function() {
+ return !this.previous_line && this.current_line.is_empty();
+};
+
+Output.prototype.add_new_line = function(force_newline) {
+ // never newline at the start of file
+ // otherwise, newline only if we didn't just add one or we're forced
+ if (this.is_empty() ||
+ (!force_newline && this.just_added_newline())) {
+ return false;
+ }
+
+ // if raw output is enabled, don't print additional newlines,
+ // but still return True as though you had
+ if (!this.raw) {
+ this.__add_outputline();
+ }
+ return true;
+};
+
+Output.prototype.get_code = function(eol) {
+ this.trim(true);
+
+ // handle some edge cases where the last tokens
+ // has text that ends with newline(s)
+ var last_item = this.current_line.pop();
+ if (last_item) {
+ if (last_item[last_item.length - 1] === '\n') {
+ last_item = last_item.replace(/\n+$/g, '');
+ }
+ this.current_line.push(last_item);
+ }
+
+ if (this._end_with_newline) {
+ this.__add_outputline();
+ }
+
+ var sweet_code = this.__lines.join('\n');
+
+ if (eol !== '\n') {
+ sweet_code = sweet_code.replace(/[\n]/g, eol);
+ }
+ return sweet_code;
+};
+
+Output.prototype.set_wrap_point = function() {
+ this.current_line._set_wrap_point();
+};
+
+Output.prototype.set_indent = function(indent, alignment) {
+ indent = indent || 0;
+ alignment = alignment || 0;
+
+ // Next line stores alignment values
+ this.next_line.set_indent(indent, alignment);
+
+ // Never indent your first output indent at the start of the file
+ if (this.__lines.length > 1) {
+ this.current_line.set_indent(indent, alignment);
+ return true;
+ }
+
+ this.current_line.set_indent();
+ return false;
+};
+
+Output.prototype.add_raw_token = function(token) {
+ for (var x = 0; x < token.newlines; x++) {
+ this.__add_outputline();
+ }
+ this.current_line.set_indent(-1);
+ this.current_line.push(token.whitespace_before);
+ this.current_line.push(token.text);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = false;
+};
+
+Output.prototype.add_token = function(printable_token) {
+ this.__add_space_before_token();
+ this.current_line.push(printable_token);
+ this.space_before_token = false;
+ this.non_breaking_space = false;
+ this.previous_token_wrapped = this.current_line._allow_wrap();
+};
+
+Output.prototype.__add_space_before_token = function() {
+ if (this.space_before_token && !this.just_added_newline()) {
+ if (!this.non_breaking_space) {
+ this.set_wrap_point();
+ }
+ this.current_line.push(' ');
+ }
+};
+
+Output.prototype.remove_indent = function(index) {
+ var output_length = this.__lines.length;
+ while (index < output_length) {
+ this.__lines[index]._remove_indent();
+ index++;
+ }
+ this.current_line._remove_wrap_indent();
+};
+
+Output.prototype.trim = function(eat_newlines) {
+ eat_newlines = (eat_newlines === undefined) ? false : eat_newlines;
+
+ this.current_line.trim();
+
+ while (eat_newlines && this.__lines.length > 1 &&
+ this.current_line.is_empty()) {
+ this.__lines.pop();
+ this.current_line = this.__lines[this.__lines.length - 1];
+ this.current_line.trim();
+ }
+
+ this.previous_line = this.__lines.length > 1 ?
+ this.__lines[this.__lines.length - 2] : null;
+};
+
+Output.prototype.just_added_newline = function() {
+ return this.current_line.is_empty();
+};
+
+Output.prototype.just_added_blankline = function() {
+ return this.is_empty() ||
+ (this.current_line.is_empty() && this.previous_line.is_empty());
+};
+
+Output.prototype.ensure_empty_line_above = function(starts_with, ends_with) {
+ var index = this.__lines.length - 2;
+ while (index >= 0) {
+ var potentialEmptyLine = this.__lines[index];
+ if (potentialEmptyLine.is_empty()) {
+ break;
+ } else if (potentialEmptyLine.item(0).indexOf(starts_with) !== 0 &&
+ potentialEmptyLine.item(-1) !== ends_with) {
+ this.__lines.splice(index + 1, 0, new OutputLine(this));
+ this.previous_line = this.__lines[this.__lines.length - 2];
+ break;
+ }
+ index--;
+ }
+};
+
+module.exports.Output = Output;
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Token(type, text, newlines, whitespace_before) {
+ this.type = type;
+ this.text = text;
+
+ // comments_before are
+ // comments that have a new line before them
+ // and may or may not have a newline after
+ // this is a set of comments before
+ this.comments_before = null; /* inline comment*/
+
+
+ // this.comments_after = new TokenStream(); // no new line before and newline after
+ this.newlines = newlines || 0;
+ this.whitespace_before = whitespace_before || '';
+ this.parent = null;
+ this.next = null;
+ this.previous = null;
+ this.opened = null;
+ this.closed = null;
+ this.directives = null;
+}
+
+
+module.exports.Token = Token;
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* jshint node: true, curly: false */
+// Parts of this section of code is taken from acorn.
+//
+// Acorn was written by Marijn Haverbeke and released under an MIT
+// license. The Unicode regexps (for identifiers and whitespace) were
+// taken from [Esprima](http://esprima.org) by Ariya Hidayat.
+//
+// Git repositories for Acorn are available at
+//
+// http://marijnhaverbeke.nl/git/acorn
+// https://github.com/marijnh/acorn.git
+
+// ## Character categories
+
+
+
+
+// acorn used char codes to squeeze the last bit of performance out
+// Beautifier is okay without that, so we're using regex
+// permit # (23), $ (36), and @ (64). @ is used in ES7 decorators.
+// 65 through 91 are uppercase letters.
+// permit _ (95).
+// 97 through 123 are lowercase letters.
+var baseASCIIidentifierStartChars = "\\x23\\x24\\x40\\x41-\\x5a\\x5f\\x61-\\x7a";
+
+// inside an identifier @ is not allowed but 0-9 are.
+var baseASCIIidentifierChars = "\\x24\\x30-\\x39\\x41-\\x5a\\x5f\\x61-\\x7a";
+
+// Big ugly regular expressions that match characters in the
+// whitespace, identifier, and identifier-start categories. These
+// are only applied when a character is found to actually have a
+// code point above 128.
+var nonASCIIidentifierStartChars = "\\xaa\\xb5\\xba\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\u02c1\\u02c6-\\u02d1\\u02e0-\\u02e4\\u02ec\\u02ee\\u0370-\\u0374\\u0376\\u0377\\u037a-\\u037d\\u0386\\u0388-\\u038a\\u038c\\u038e-\\u03a1\\u03a3-\\u03f5\\u03f7-\\u0481\\u048a-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05d0-\\u05ea\\u05f0-\\u05f2\\u0620-\\u064a\\u066e\\u066f\\u0671-\\u06d3\\u06d5\\u06e5\\u06e6\\u06ee\\u06ef\\u06fa-\\u06fc\\u06ff\\u0710\\u0712-\\u072f\\u074d-\\u07a5\\u07b1\\u07ca-\\u07ea\\u07f4\\u07f5\\u07fa\\u0800-\\u0815\\u081a\\u0824\\u0828\\u0840-\\u0858\\u08a0\\u08a2-\\u08ac\\u0904-\\u0939\\u093d\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097f\\u0985-\\u098c\\u098f\\u0990\\u0993-\\u09a8\\u09aa-\\u09b0\\u09b2\\u09b6-\\u09b9\\u09bd\\u09ce\\u09dc\\u09dd\\u09df-\\u09e1\\u09f0\\u09f1\\u0a05-\\u0a0a\\u0a0f\\u0a10\\u0a13-\\u0a28\\u0a2a-\\u0a30\\u0a32\\u0a33\\u0a35\\u0a36\\u0a38\\u0a39\\u0a59-\\u0a5c\\u0a5e\\u0a72-\\u0a74\\u0a85-\\u0a8d\\u0a8f-\\u0a91\\u0a93-\\u0aa8\\u0aaa-\\u0ab0\\u0ab2\\u0ab3\\u0ab5-\\u0ab9\\u0abd\\u0ad0\\u0ae0\\u0ae1\\u0b05-\\u0b0c\\u0b0f\\u0b10\\u0b13-\\u0b28\\u0b2a-\\u0b30\\u0b32\\u0b33\\u0b35-\\u0b39\\u0b3d\\u0b5c\\u0b5d\\u0b5f-\\u0b61\\u0b71\\u0b83\\u0b85-\\u0b8a\\u0b8e-\\u0b90\\u0b92-\\u0b95\\u0b99\\u0b9a\\u0b9c\\u0b9e\\u0b9f\\u0ba3\\u0ba4\\u0ba8-\\u0baa\\u0bae-\\u0bb9\\u0bd0\\u0c05-\\u0c0c\\u0c0e-\\u0c10\\u0c12-\\u0c28\\u0c2a-\\u0c33\\u0c35-\\u0c39\\u0c3d\\u0c58\\u0c59\\u0c60\\u0c61\\u0c85-\\u0c8c\\u0c8e-\\u0c90\\u0c92-\\u0ca8\\u0caa-\\u0cb3\\u0cb5-\\u0cb9\\u0cbd\\u0cde\\u0ce0\\u0ce1\\u0cf1\\u0cf2\\u0d05-\\u0d0c\\u0d0e-\\u0d10\\u0d12-\\u0d3a\\u0d3d\\u0d4e\\u0d60\\u0d61\\u0d7a-\\u0d7f\\u0d85-\\u0d96\\u0d9a-\\u0db1\\u0db3-\\u0dbb\\u0dbd\\u0dc0-\\u0dc6\\u0e01-\\u0e30\\u0e32\\u0e33\\u0e40-\\u0e46\\u0e81\\u0e82\\u0e84\\u0e87\\u0e88\\u0e8a\\u0e8d\\u0e94-\\u0e97\\u0e99-\\u0e9f\\u0ea1-\\u0ea3\\u0ea5\\u0ea7\\u0eaa\\u0eab\\u0ead-\\u0eb0\\u0eb2\\u0eb3\\u0ebd\\u0ec0-\\u0ec4\\u0ec6\\u0edc-\\u0edf\\u0f00\\u0f40-\\u0f47\\u0f49-\\u0f6c\\u0f88-\\u0f8c\\u1000-\\u102a\\u103f\\u1050-\\u1055\\u105a-\\u105d\\u1061\\u1065\\u1066\\u106e-\\u1070\\u1075-\\u1081\\u108e\\u10a0-\\u10c5\\u10c7\\u10cd\\u10d0-\\u10fa\\u10fc-\\u1248\\u124a-\\u124d\\u1250-\\u1256\\u1258\\u125a-\\u125d\\u1260-\\u1288\\u128a-\\u128d\\u1290-\\u12b0\\u12b2-\\u12b5\\u12b8-\\u12be\\u12c0\\u12c2-\\u12c5\\u12c8-\\u12d6\\u12d8-\\u1310\\u1312-\\u1315\\u1318-\\u135a\\u1380-\\u138f\\u13a0-\\u13f4\\u1401-\\u166c\\u166f-\\u167f\\u1681-\\u169a\\u16a0-\\u16ea\\u16ee-\\u16f0\\u1700-\\u170c\\u170e-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176c\\u176e-\\u1770\\u1780-\\u17b3\\u17d7\\u17dc\\u1820-\\u1877\\u1880-\\u18a8\\u18aa\\u18b0-\\u18f5\\u1900-\\u191c\\u1950-\\u196d\\u1970-\\u1974\\u1980-\\u19ab\\u19c1-\\u19c7\\u1a00-\\u1a16\\u1a20-\\u1a54\\u1aa7\\u1b05-\\u1b33\\u1b45-\\u1b4b\\u1b83-\\u1ba0\\u1bae\\u1baf\\u1bba-\\u1be5\\u1c00-\\u1c23\\u1c4d-\\u1c4f\\u1c5a-\\u1c7d\\u1ce9-\\u1cec\\u1cee-\\u1cf1\\u1cf5\\u1cf6\\u1d00-\\u1dbf\\u1e00-\\u1f15\\u1f18-\\u1f1d\\u1f20-\\u1f45\\u1f48-\\u1f4d\\u1f50-\\u1f57\\u1f59\\u1f5b\\u1f5d\\u1f5f-\\u1f7d\\u1f80-\\u1fb4\\u1fb6-\\u1fbc\\u1fbe\\u1fc2-\\u1fc4\\u1fc6-\\u1fcc\\u1fd0-\\u1fd3\\u1fd6-\\u1fdb\\u1fe0-\\u1fec\\u1ff2-\\u1ff4\\u1ff6-\\u1ffc\\u2071\\u207f\\u2090-\\u209c\\u2102\\u2107\\u210a-\\u2113\\u2115\\u2119-\\u211d\\u2124\\u2126\\u2128\\u212a-\\u212d\\u212f-\\u2139\\u213c-\\u213f\\u2145-\\u2149\\u214e\\u2160-\\u2188\\u2c00-\\u2c2e\\u2c30-\\u2c5e\\u2c60-\\u2ce4\\u2ceb-\\u2cee\\u2cf2\\u2cf3\\u2d00-\\u2d25\\u2d27\\u2d2d\\u2d30-\\u2d67\\u2d6f\\u2d80-\\u2d96\\u2da0-\\u2da6\\u2da8-\\u2dae\\u2db0-\\u2db6\\u2db8-\\u2dbe\\u2dc0-\\u2dc6\\u2dc8-\\u2dce\\u2dd0-\\u2dd6\\u2dd8-\\u2dde\\u2e2f\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303c\\u3041-\\u3096\\u309d-\\u309f\\u30a1-\\u30fa\\u30fc-\\u30ff\\u3105-\\u312d\\u3131-\\u318e\\u31a0-\\u31ba\\u31f0-\\u31ff\\u3400-\\u4db5\\u4e00-\\u9fcc\\ua000-\\ua48c\\ua4d0-\\ua4fd\\ua500-\\ua60c\\ua610-\\ua61f\\ua62a\\ua62b\\ua640-\\ua66e\\ua67f-\\ua697\\ua6a0-\\ua6ef\\ua717-\\ua71f\\ua722-\\ua788\\ua78b-\\ua78e\\ua790-\\ua793\\ua7a0-\\ua7aa\\ua7f8-\\ua801\\ua803-\\ua805\\ua807-\\ua80a\\ua80c-\\ua822\\ua840-\\ua873\\ua882-\\ua8b3\\ua8f2-\\ua8f7\\ua8fb\\ua90a-\\ua925\\ua930-\\ua946\\ua960-\\ua97c\\ua984-\\ua9b2\\ua9cf\\uaa00-\\uaa28\\uaa40-\\uaa42\\uaa44-\\uaa4b\\uaa60-\\uaa76\\uaa7a\\uaa80-\\uaaaf\\uaab1\\uaab5\\uaab6\\uaab9-\\uaabd\\uaac0\\uaac2\\uaadb-\\uaadd\\uaae0-\\uaaea\\uaaf2-\\uaaf4\\uab01-\\uab06\\uab09-\\uab0e\\uab11-\\uab16\\uab20-\\uab26\\uab28-\\uab2e\\uabc0-\\uabe2\\uac00-\\ud7a3\\ud7b0-\\ud7c6\\ud7cb-\\ud7fb\\uf900-\\ufa6d\\ufa70-\\ufad9\\ufb00-\\ufb06\\ufb13-\\ufb17\\ufb1d\\ufb1f-\\ufb28\\ufb2a-\\ufb36\\ufb38-\\ufb3c\\ufb3e\\ufb40\\ufb41\\ufb43\\ufb44\\ufb46-\\ufbb1\\ufbd3-\\ufd3d\\ufd50-\\ufd8f\\ufd92-\\ufdc7\\ufdf0-\\ufdfb\\ufe70-\\ufe74\\ufe76-\\ufefc\\uff21-\\uff3a\\uff41-\\uff5a\\uff66-\\uffbe\\uffc2-\\uffc7\\uffca-\\uffcf\\uffd2-\\uffd7\\uffda-\\uffdc";
+var nonASCIIidentifierChars = "\\u0300-\\u036f\\u0483-\\u0487\\u0591-\\u05bd\\u05bf\\u05c1\\u05c2\\u05c4\\u05c5\\u05c7\\u0610-\\u061a\\u0620-\\u0649\\u0672-\\u06d3\\u06e7-\\u06e8\\u06fb-\\u06fc\\u0730-\\u074a\\u0800-\\u0814\\u081b-\\u0823\\u0825-\\u0827\\u0829-\\u082d\\u0840-\\u0857\\u08e4-\\u08fe\\u0900-\\u0903\\u093a-\\u093c\\u093e-\\u094f\\u0951-\\u0957\\u0962-\\u0963\\u0966-\\u096f\\u0981-\\u0983\\u09bc\\u09be-\\u09c4\\u09c7\\u09c8\\u09d7\\u09df-\\u09e0\\u0a01-\\u0a03\\u0a3c\\u0a3e-\\u0a42\\u0a47\\u0a48\\u0a4b-\\u0a4d\\u0a51\\u0a66-\\u0a71\\u0a75\\u0a81-\\u0a83\\u0abc\\u0abe-\\u0ac5\\u0ac7-\\u0ac9\\u0acb-\\u0acd\\u0ae2-\\u0ae3\\u0ae6-\\u0aef\\u0b01-\\u0b03\\u0b3c\\u0b3e-\\u0b44\\u0b47\\u0b48\\u0b4b-\\u0b4d\\u0b56\\u0b57\\u0b5f-\\u0b60\\u0b66-\\u0b6f\\u0b82\\u0bbe-\\u0bc2\\u0bc6-\\u0bc8\\u0bca-\\u0bcd\\u0bd7\\u0be6-\\u0bef\\u0c01-\\u0c03\\u0c46-\\u0c48\\u0c4a-\\u0c4d\\u0c55\\u0c56\\u0c62-\\u0c63\\u0c66-\\u0c6f\\u0c82\\u0c83\\u0cbc\\u0cbe-\\u0cc4\\u0cc6-\\u0cc8\\u0cca-\\u0ccd\\u0cd5\\u0cd6\\u0ce2-\\u0ce3\\u0ce6-\\u0cef\\u0d02\\u0d03\\u0d46-\\u0d48\\u0d57\\u0d62-\\u0d63\\u0d66-\\u0d6f\\u0d82\\u0d83\\u0dca\\u0dcf-\\u0dd4\\u0dd6\\u0dd8-\\u0ddf\\u0df2\\u0df3\\u0e34-\\u0e3a\\u0e40-\\u0e45\\u0e50-\\u0e59\\u0eb4-\\u0eb9\\u0ec8-\\u0ecd\\u0ed0-\\u0ed9\\u0f18\\u0f19\\u0f20-\\u0f29\\u0f35\\u0f37\\u0f39\\u0f41-\\u0f47\\u0f71-\\u0f84\\u0f86-\\u0f87\\u0f8d-\\u0f97\\u0f99-\\u0fbc\\u0fc6\\u1000-\\u1029\\u1040-\\u1049\\u1067-\\u106d\\u1071-\\u1074\\u1082-\\u108d\\u108f-\\u109d\\u135d-\\u135f\\u170e-\\u1710\\u1720-\\u1730\\u1740-\\u1750\\u1772\\u1773\\u1780-\\u17b2\\u17dd\\u17e0-\\u17e9\\u180b-\\u180d\\u1810-\\u1819\\u1920-\\u192b\\u1930-\\u193b\\u1951-\\u196d\\u19b0-\\u19c0\\u19c8-\\u19c9\\u19d0-\\u19d9\\u1a00-\\u1a15\\u1a20-\\u1a53\\u1a60-\\u1a7c\\u1a7f-\\u1a89\\u1a90-\\u1a99\\u1b46-\\u1b4b\\u1b50-\\u1b59\\u1b6b-\\u1b73\\u1bb0-\\u1bb9\\u1be6-\\u1bf3\\u1c00-\\u1c22\\u1c40-\\u1c49\\u1c5b-\\u1c7d\\u1cd0-\\u1cd2\\u1d00-\\u1dbe\\u1e01-\\u1f15\\u200c\\u200d\\u203f\\u2040\\u2054\\u20d0-\\u20dc\\u20e1\\u20e5-\\u20f0\\u2d81-\\u2d96\\u2de0-\\u2dff\\u3021-\\u3028\\u3099\\u309a\\ua640-\\ua66d\\ua674-\\ua67d\\ua69f\\ua6f0-\\ua6f1\\ua7f8-\\ua800\\ua806\\ua80b\\ua823-\\ua827\\ua880-\\ua881\\ua8b4-\\ua8c4\\ua8d0-\\ua8d9\\ua8f3-\\ua8f7\\ua900-\\ua909\\ua926-\\ua92d\\ua930-\\ua945\\ua980-\\ua983\\ua9b3-\\ua9c0\\uaa00-\\uaa27\\uaa40-\\uaa41\\uaa4c-\\uaa4d\\uaa50-\\uaa59\\uaa7b\\uaae0-\\uaae9\\uaaf2-\\uaaf3\\uabc0-\\uabe1\\uabec\\uabed\\uabf0-\\uabf9\\ufb20-\\ufb28\\ufe00-\\ufe0f\\ufe20-\\ufe26\\ufe33\\ufe34\\ufe4d-\\ufe4f\\uff10-\\uff19\\uff3f";
+//var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
+//var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
+
+var identifierStart = "(?:\\\\u[0-9a-fA-F]{4}|[" + baseASCIIidentifierStartChars + nonASCIIidentifierStartChars + "])";
+var identifierChars = "(?:\\\\u[0-9a-fA-F]{4}|[" + baseASCIIidentifierChars + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "])*";
+
+exports.identifier = new RegExp(identifierStart + identifierChars, 'g');
+exports.identifierStart = new RegExp(identifierStart);
+exports.identifierMatch = new RegExp("(?:\\\\u[0-9a-fA-F]{4}|[" + baseASCIIidentifierChars + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "])+");
+
+var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/; // jshint ignore:line
+
+// Whether a single character denotes a newline.
+
+exports.newline = /[\n\r\u2028\u2029]/;
+
+// Matches a whole line break (where CRLF is considered a single
+// line break). Used to count lines.
+
+// in javascript, these two differ
+// in python they are the same, different methods are called on them
+exports.lineBreak = new RegExp('\r\n|' + exports.newline.source);
+exports.allLineBreaks = new RegExp(exports.lineBreak.source, 'g');
+
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var BaseOptions = __webpack_require__(6).Options;
+
+var validPositionValues = ['before-newline', 'after-newline', 'preserve-newline'];
+
+function Options(options) {
+ BaseOptions.call(this, options, 'js');
+
+ // compatibility, re
+ var raw_brace_style = this.raw_options.brace_style || null;
+ if (raw_brace_style === "expand-strict") { //graceful handling of deprecated option
+ this.raw_options.brace_style = "expand";
+ } else if (raw_brace_style === "collapse-preserve-inline") { //graceful handling of deprecated option
+ this.raw_options.brace_style = "collapse,preserve-inline";
+ } else if (this.raw_options.braces_on_own_line !== undefined) { //graceful handling of deprecated option
+ this.raw_options.brace_style = this.raw_options.braces_on_own_line ? "expand" : "collapse";
+ // } else if (!raw_brace_style) { //Nothing exists to set it
+ // raw_brace_style = "collapse";
+ }
+
+ //preserve-inline in delimited string will trigger brace_preserve_inline, everything
+ //else is considered a brace_style and the last one only will have an effect
+
+ var brace_style_split = this._get_selection_list('brace_style', ['collapse', 'expand', 'end-expand', 'none', 'preserve-inline']);
+
+ this.brace_preserve_inline = false; //Defaults in case one or other was not specified in meta-option
+ this.brace_style = "collapse";
+
+ for (var bs = 0; bs < brace_style_split.length; bs++) {
+ if (brace_style_split[bs] === "preserve-inline") {
+ this.brace_preserve_inline = true;
+ } else {
+ this.brace_style = brace_style_split[bs];
+ }
+ }
+
+ this.unindent_chained_methods = this._get_boolean('unindent_chained_methods');
+ this.break_chained_methods = this._get_boolean('break_chained_methods');
+ this.space_in_paren = this._get_boolean('space_in_paren');
+ this.space_in_empty_paren = this._get_boolean('space_in_empty_paren');
+ this.jslint_happy = this._get_boolean('jslint_happy');
+ this.space_after_anon_function = this._get_boolean('space_after_anon_function');
+ this.space_after_named_function = this._get_boolean('space_after_named_function');
+ this.keep_array_indentation = this._get_boolean('keep_array_indentation');
+ this.space_before_conditional = this._get_boolean('space_before_conditional', true);
+ this.unescape_strings = this._get_boolean('unescape_strings');
+ this.e4x = this._get_boolean('e4x');
+ this.comma_first = this._get_boolean('comma_first');
+ this.operator_position = this._get_selection('operator_position', validPositionValues);
+
+ // For testing of beautify preserve:start directive
+ this.test_output_raw = this._get_boolean('test_output_raw');
+
+ // force this._options.space_after_anon_function to true if this._options.jslint_happy
+ if (this.jslint_happy) {
+ this.space_after_anon_function = true;
+ }
+
+}
+Options.prototype = new BaseOptions();
+
+
+
+module.exports.Options = Options;
+
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Options(options, merge_child_field) {
+ this.raw_options = _mergeOpts(options, merge_child_field);
+
+ // Support passing the source text back with no change
+ this.disabled = this._get_boolean('disabled');
+
+ this.eol = this._get_characters('eol', 'auto');
+ this.end_with_newline = this._get_boolean('end_with_newline');
+ this.indent_size = this._get_number('indent_size', 4);
+ this.indent_char = this._get_characters('indent_char', ' ');
+ this.indent_level = this._get_number('indent_level');
+
+ this.preserve_newlines = this._get_boolean('preserve_newlines', true);
+ this.max_preserve_newlines = this._get_number('max_preserve_newlines', 32786);
+ if (!this.preserve_newlines) {
+ this.max_preserve_newlines = 0;
+ }
+
+ this.indent_with_tabs = this._get_boolean('indent_with_tabs', this.indent_char === '\t');
+ if (this.indent_with_tabs) {
+ this.indent_char = '\t';
+
+ // indent_size behavior changed after 1.8.6
+ // It used to be that indent_size would be
+ // set to 1 for indent_with_tabs. That is no longer needed and
+ // actually doesn't make sense - why not use spaces? Further,
+ // that might produce unexpected behavior - tabs being used
+ // for single-column alignment. So, when indent_with_tabs is true
+ // and indent_size is 1, reset indent_size to 4.
+ if (this.indent_size === 1) {
+ this.indent_size = 4;
+ }
+ }
+
+ // Backwards compat with 1.3.x
+ this.wrap_line_length = this._get_number('wrap_line_length', this._get_number('max_char'));
+
+ this.indent_empty_lines = this._get_boolean('indent_empty_lines');
+
+ // valid templating languages ['django', 'erb', 'handlebars', 'php']
+ // For now, 'auto' = all off for javascript, all on for html (and inline javascript).
+ // other values ignored
+ this.templating = this._get_selection_list('templating', ['auto', 'none', 'django', 'erb', 'handlebars', 'php'], ['auto']);
+}
+
+Options.prototype._get_array = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = default_value || [];
+ if (typeof option_value === 'object') {
+ if (option_value !== null && typeof option_value.concat === 'function') {
+ result = option_value.concat();
+ }
+ } else if (typeof option_value === 'string') {
+ result = option_value.split(/[^a-zA-Z0-9_\/\-]+/);
+ }
+ return result;
+};
+
+Options.prototype._get_boolean = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = option_value === undefined ? !!default_value : !!option_value;
+ return result;
+};
+
+Options.prototype._get_characters = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ var result = default_value || '';
+ if (typeof option_value === 'string') {
+ result = option_value.replace(/\\r/, '\r').replace(/\\n/, '\n').replace(/\\t/, '\t');
+ }
+ return result;
+};
+
+Options.prototype._get_number = function(name, default_value) {
+ var option_value = this.raw_options[name];
+ default_value = parseInt(default_value, 10);
+ if (isNaN(default_value)) {
+ default_value = 0;
+ }
+ var result = parseInt(option_value, 10);
+ if (isNaN(result)) {
+ result = default_value;
+ }
+ return result;
+};
+
+Options.prototype._get_selection = function(name, selection_list, default_value) {
+ var result = this._get_selection_list(name, selection_list, default_value);
+ if (result.length !== 1) {
+ throw new Error(
+ "Invalid Option Value: The option '" + name + "' can only be one of the following values:\n" +
+ selection_list + "\nYou passed in: '" + this.raw_options[name] + "'");
+ }
+
+ return result[0];
+};
+
+
+Options.prototype._get_selection_list = function(name, selection_list, default_value) {
+ if (!selection_list || selection_list.length === 0) {
+ throw new Error("Selection list cannot be empty.");
+ }
+
+ default_value = default_value || [selection_list[0]];
+ if (!this._is_valid_selection(default_value, selection_list)) {
+ throw new Error("Invalid Default Value!");
+ }
+
+ var result = this._get_array(name, default_value);
+ if (!this._is_valid_selection(result, selection_list)) {
+ throw new Error(
+ "Invalid Option Value: The option '" + name + "' can contain only the following values:\n" +
+ selection_list + "\nYou passed in: '" + this.raw_options[name] + "'");
+ }
+
+ return result;
+};
+
+Options.prototype._is_valid_selection = function(result, selection_list) {
+ return result.length && selection_list.length &&
+ !result.some(function(item) { return selection_list.indexOf(item) === -1; });
+};
+
+
+// merges child options up with the parent options object
+// Example: obj = {a: 1, b: {a: 2}}
+// mergeOpts(obj, 'b')
+//
+// Returns: {a: 2}
+function _mergeOpts(allOptions, childFieldName) {
+ var finalOpts = {};
+ allOptions = _normalizeOpts(allOptions);
+ var name;
+
+ for (name in allOptions) {
+ if (name !== childFieldName) {
+ finalOpts[name] = allOptions[name];
+ }
+ }
+
+ //merge in the per type settings for the childFieldName
+ if (childFieldName && allOptions[childFieldName]) {
+ for (name in allOptions[childFieldName]) {
+ finalOpts[name] = allOptions[childFieldName][name];
+ }
+ }
+ return finalOpts;
+}
+
+function _normalizeOpts(options) {
+ var convertedOpts = {};
+ var key;
+
+ for (key in options) {
+ var newKey = key.replace(/-/g, "_");
+ convertedOpts[newKey] = options[key];
+ }
+ return convertedOpts;
+}
+
+module.exports.Options = Options;
+module.exports.normalizeOpts = _normalizeOpts;
+module.exports.mergeOpts = _mergeOpts;
+
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var InputScanner = __webpack_require__(8).InputScanner;
+var BaseTokenizer = __webpack_require__(9).Tokenizer;
+var BASETOKEN = __webpack_require__(9).TOKEN;
+var Directives = __webpack_require__(13).Directives;
+var acorn = __webpack_require__(4);
+var Pattern = __webpack_require__(12).Pattern;
+var TemplatablePattern = __webpack_require__(14).TemplatablePattern;
+
+
+function in_array(what, arr) {
+ return arr.indexOf(what) !== -1;
+}
+
+
+var TOKEN = {
+ START_EXPR: 'TK_START_EXPR',
+ END_EXPR: 'TK_END_EXPR',
+ START_BLOCK: 'TK_START_BLOCK',
+ END_BLOCK: 'TK_END_BLOCK',
+ WORD: 'TK_WORD',
+ RESERVED: 'TK_RESERVED',
+ SEMICOLON: 'TK_SEMICOLON',
+ STRING: 'TK_STRING',
+ EQUALS: 'TK_EQUALS',
+ OPERATOR: 'TK_OPERATOR',
+ COMMA: 'TK_COMMA',
+ BLOCK_COMMENT: 'TK_BLOCK_COMMENT',
+ COMMENT: 'TK_COMMENT',
+ DOT: 'TK_DOT',
+ UNKNOWN: 'TK_UNKNOWN',
+ START: BASETOKEN.START,
+ RAW: BASETOKEN.RAW,
+ EOF: BASETOKEN.EOF
+};
+
+
+var directives_core = new Directives(/\/\*/, /\*\//);
+
+var number_pattern = /0[xX][0123456789abcdefABCDEF]*|0[oO][01234567]*|0[bB][01]*|\d+n|(?:\.\d+|\d+\.?\d*)(?:[eE][+-]?\d+)?/;
+
+var digit = /[0-9]/;
+
+// Dot "." must be distinguished from "..." and decimal
+var dot_pattern = /[^\d\.]/;
+
+var positionable_operators = (
+ ">>> === !== " +
+ "<< && >= ** != == <= >> || ?? |> " +
+ "< / - + > : & % ? ^ | *").split(' ');
+
+// IMPORTANT: this must be sorted longest to shortest or tokenizing many not work.
+// Also, you must update possitionable operators separately from punct
+var punct =
+ ">>>= " +
+ "... >>= <<= === >>> !== **= " +
+ "=> ^= :: /= << <= == && -= >= >> != -- += ** || ?? ++ %= &= *= |= |> " +
+ "= ! ? > < : / ^ - + * & % ~ |";
+
+punct = punct.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&");
+// ?. but not if followed by a number
+punct = '\\?\\.(?!\\d) ' + punct;
+punct = punct.replace(/ /g, '|');
+
+var punct_pattern = new RegExp(punct);
+
+// words which should always start on new line.
+var line_starters = 'continue,try,throw,return,var,let,const,if,switch,case,default,for,while,break,function,import,export'.split(',');
+var reserved_words = line_starters.concat(['do', 'in', 'of', 'else', 'get', 'set', 'new', 'catch', 'finally', 'typeof', 'yield', 'async', 'await', 'from', 'as']);
+var reserved_word_pattern = new RegExp('^(?:' + reserved_words.join('|') + ')$');
+
+// var template_pattern = /(?:(?:<\?php|<\?=)[\s\S]*?\?>)|(?:<%[\s\S]*?%>)/g;
+
+var in_html_comment;
+
+var Tokenizer = function(input_string, options) {
+ BaseTokenizer.call(this, input_string, options);
+
+ this._patterns.whitespace = this._patterns.whitespace.matching(
+ /\u00A0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff/.source,
+ /\u2028\u2029/.source);
+
+ var pattern_reader = new Pattern(this._input);
+ var templatable = new TemplatablePattern(this._input)
+ .read_options(this._options);
+
+ this.__patterns = {
+ template: templatable,
+ identifier: templatable.starting_with(acorn.identifier).matching(acorn.identifierMatch),
+ number: pattern_reader.matching(number_pattern),
+ punct: pattern_reader.matching(punct_pattern),
+ // comment ends just before nearest linefeed or end of file
+ comment: pattern_reader.starting_with(/\/\//).until(/[\n\r\u2028\u2029]/),
+ // /* ... */ comment ends with nearest */ or end of file
+ block_comment: pattern_reader.starting_with(/\/\*/).until_after(/\*\//),
+ html_comment_start: pattern_reader.matching(/<!--/),
+ html_comment_end: pattern_reader.matching(/-->/),
+ include: pattern_reader.starting_with(/#include/).until_after(acorn.lineBreak),
+ shebang: pattern_reader.starting_with(/#!/).until_after(acorn.lineBreak),
+ xml: pattern_reader.matching(/[\s\S]*?<(\/?)([-a-zA-Z:0-9_.]+|{[\s\S]+?}|!\[CDATA\[[\s\S]*?\]\])(\s+{[\s\S]+?}|\s+[-a-zA-Z:0-9_.]+|\s+[-a-zA-Z:0-9_.]+\s*=\s*('[^']*'|"[^"]*"|{[\s\S]+?}))*\s*(\/?)\s*>/),
+ single_quote: templatable.until(/['\\\n\r\u2028\u2029]/),
+ double_quote: templatable.until(/["\\\n\r\u2028\u2029]/),
+ template_text: templatable.until(/[`\\$]/),
+ template_expression: templatable.until(/[`}\\]/)
+ };
+
+};
+Tokenizer.prototype = new BaseTokenizer();
+
+Tokenizer.prototype._is_comment = function(current_token) {
+ return current_token.type === TOKEN.COMMENT || current_token.type === TOKEN.BLOCK_COMMENT || current_token.type === TOKEN.UNKNOWN;
+};
+
+Tokenizer.prototype._is_opening = function(current_token) {
+ return current_token.type === TOKEN.START_BLOCK || current_token.type === TOKEN.START_EXPR;
+};
+
+Tokenizer.prototype._is_closing = function(current_token, open_token) {
+ return (current_token.type === TOKEN.END_BLOCK || current_token.type === TOKEN.END_EXPR) &&
+ (open_token && (
+ (current_token.text === ']' && open_token.text === '[') ||
+ (current_token.text === ')' && open_token.text === '(') ||
+ (current_token.text === '}' && open_token.text === '{')));
+};
+
+Tokenizer.prototype._reset = function() {
+ in_html_comment = false;
+};
+
+Tokenizer.prototype._get_next_token = function(previous_token, open_token) { // jshint unused:false
+ var token = null;
+ this._readWhitespace();
+ var c = this._input.peek();
+
+ if (c === null) {
+ return this._create_token(TOKEN.EOF, '');
+ }
+
+ token = token || this._read_non_javascript(c);
+ token = token || this._read_string(c);
+ token = token || this._read_word(previous_token);
+ token = token || this._read_singles(c);
+ token = token || this._read_comment(c);
+ token = token || this._read_regexp(c, previous_token);
+ token = token || this._read_xml(c, previous_token);
+ token = token || this._read_punctuation();
+ token = token || this._create_token(TOKEN.UNKNOWN, this._input.next());
+
+ return token;
+};
+
+Tokenizer.prototype._read_word = function(previous_token) {
+ var resulting_string;
+ resulting_string = this.__patterns.identifier.read();
+ if (resulting_string !== '') {
+ resulting_string = resulting_string.replace(acorn.allLineBreaks, '\n');
+ if (!(previous_token.type === TOKEN.DOT ||
+ (previous_token.type === TOKEN.RESERVED && (previous_token.text === 'set' || previous_token.text === 'get'))) &&
+ reserved_word_pattern.test(resulting_string)) {
+ if (resulting_string === 'in' || resulting_string === 'of') { // hack for 'in' and 'of' operators
+ return this._create_token(TOKEN.OPERATOR, resulting_string);
+ }
+ return this._create_token(TOKEN.RESERVED, resulting_string);
+ }
+ return this._create_token(TOKEN.WORD, resulting_string);
+ }
+
+ resulting_string = this.__patterns.number.read();
+ if (resulting_string !== '') {
+ return this._create_token(TOKEN.WORD, resulting_string);
+ }
+};
+
+Tokenizer.prototype._read_singles = function(c) {
+ var token = null;
+ if (c === '(' || c === '[') {
+ token = this._create_token(TOKEN.START_EXPR, c);
+ } else if (c === ')' || c === ']') {
+ token = this._create_token(TOKEN.END_EXPR, c);
+ } else if (c === '{') {
+ token = this._create_token(TOKEN.START_BLOCK, c);
+ } else if (c === '}') {
+ token = this._create_token(TOKEN.END_BLOCK, c);
+ } else if (c === ';') {
+ token = this._create_token(TOKEN.SEMICOLON, c);
+ } else if (c === '.' && dot_pattern.test(this._input.peek(1))) {
+ token = this._create_token(TOKEN.DOT, c);
+ } else if (c === ',') {
+ token = this._create_token(TOKEN.COMMA, c);
+ }
+
+ if (token) {
+ this._input.next();
+ }
+ return token;
+};
+
+Tokenizer.prototype._read_punctuation = function() {
+ var resulting_string = this.__patterns.punct.read();
+
+ if (resulting_string !== '') {
+ if (resulting_string === '=') {
+ return this._create_token(TOKEN.EQUALS, resulting_string);
+ } else if (resulting_string === '?.') {
+ return this._create_token(TOKEN.DOT, resulting_string);
+ } else {
+ return this._create_token(TOKEN.OPERATOR, resulting_string);
+ }
+ }
+};
+
+Tokenizer.prototype._read_non_javascript = function(c) {
+ var resulting_string = '';
+
+ if (c === '#') {
+ if (this._is_first_token()) {
+ resulting_string = this.__patterns.shebang.read();
+
+ if (resulting_string) {
+ return this._create_token(TOKEN.UNKNOWN, resulting_string.trim() + '\n');
+ }
+ }
+
+ // handles extendscript #includes
+ resulting_string = this.__patterns.include.read();
+
+ if (resulting_string) {
+ return this._create_token(TOKEN.UNKNOWN, resulting_string.trim() + '\n');
+ }
+
+ c = this._input.next();
+
+ // Spidermonkey-specific sharp variables for circular references. Considered obsolete.
+ var sharp = '#';
+ if (this._input.hasNext() && this._input.testChar(digit)) {
+ do {
+ c = this._input.next();
+ sharp += c;
+ } while (this._input.hasNext() && c !== '#' && c !== '=');
+ if (c === '#') {
+ //
+ } else if (this._input.peek() === '[' && this._input.peek(1) === ']') {
+ sharp += '[]';
+ this._input.next();
+ this._input.next();
+ } else if (this._input.peek() === '{' && this._input.peek(1) === '}') {
+ sharp += '{}';
+ this._input.next();
+ this._input.next();
+ }
+ return this._create_token(TOKEN.WORD, sharp);
+ }
+
+ this._input.back();
+
+ } else if (c === '<' && this._is_first_token()) {
+ resulting_string = this.__patterns.html_comment_start.read();
+ if (resulting_string) {
+ while (this._input.hasNext() && !this._input.testChar(acorn.newline)) {
+ resulting_string += this._input.next();
+ }
+ in_html_comment = true;
+ return this._create_token(TOKEN.COMMENT, resulting_string);
+ }
+ } else if (in_html_comment && c === '-') {
+ resulting_string = this.__patterns.html_comment_end.read();
+ if (resulting_string) {
+ in_html_comment = false;
+ return this._create_token(TOKEN.COMMENT, resulting_string);
+ }
+ }
+
+ return null;
+};
+
+Tokenizer.prototype._read_comment = function(c) {
+ var token = null;
+ if (c === '/') {
+ var comment = '';
+ if (this._input.peek(1) === '*') {
+ // peek for comment /* ... */
+ comment = this.__patterns.block_comment.read();
+ var directives = directives_core.get_directives(comment);
+ if (directives && directives.ignore === 'start') {
+ comment += directives_core.readIgnored(this._input);
+ }
+ comment = comment.replace(acorn.allLineBreaks, '\n');
+ token = this._create_token(TOKEN.BLOCK_COMMENT, comment);
+ token.directives = directives;
+ } else if (this._input.peek(1) === '/') {
+ // peek for comment // ...
+ comment = this.__patterns.comment.read();
+ token = this._create_token(TOKEN.COMMENT, comment);
+ }
+ }
+ return token;
+};
+
+Tokenizer.prototype._read_string = function(c) {
+ if (c === '`' || c === "'" || c === '"') {
+ var resulting_string = this._input.next();
+ this.has_char_escapes = false;
+
+ if (c === '`') {
+ resulting_string += this._read_string_recursive('`', true, '${');
+ } else {
+ resulting_string += this._read_string_recursive(c);
+ }
+
+ if (this.has_char_escapes && this._options.unescape_strings) {
+ resulting_string = unescape_string(resulting_string);
+ }
+
+ if (this._input.peek() === c) {
+ resulting_string += this._input.next();
+ }
+
+ resulting_string = resulting_string.replace(acorn.allLineBreaks, '\n');
+
+ return this._create_token(TOKEN.STRING, resulting_string);
+ }
+
+ return null;
+};
+
+Tokenizer.prototype._allow_regexp_or_xml = function(previous_token) {
+ // regex and xml can only appear in specific locations during parsing
+ return (previous_token.type === TOKEN.RESERVED && in_array(previous_token.text, ['return', 'case', 'throw', 'else', 'do', 'typeof', 'yield'])) ||
+ (previous_token.type === TOKEN.END_EXPR && previous_token.text === ')' &&
+ previous_token.opened.previous.type === TOKEN.RESERVED && in_array(previous_token.opened.previous.text, ['if', 'while', 'for'])) ||
+ (in_array(previous_token.type, [TOKEN.COMMENT, TOKEN.START_EXPR, TOKEN.START_BLOCK, TOKEN.START,
+ TOKEN.END_BLOCK, TOKEN.OPERATOR, TOKEN.EQUALS, TOKEN.EOF, TOKEN.SEMICOLON, TOKEN.COMMA
+ ]));
+};
+
+Tokenizer.prototype._read_regexp = function(c, previous_token) {
+
+ if (c === '/' && this._allow_regexp_or_xml(previous_token)) {
+ // handle regexp
+ //
+ var resulting_string = this._input.next();
+ var esc = false;
+
+ var in_char_class = false;
+ while (this._input.hasNext() &&
+ ((esc || in_char_class || this._input.peek() !== c) &&
+ !this._input.testChar(acorn.newline))) {
+ resulting_string += this._input.peek();
+ if (!esc) {
+ esc = this._input.peek() === '\\';
+ if (this._input.peek() === '[') {
+ in_char_class = true;
+ } else if (this._input.peek() === ']') {
+ in_char_class = false;
+ }
+ } else {
+ esc = false;
+ }
+ this._input.next();
+ }
+
+ if (this._input.peek() === c) {
+ resulting_string += this._input.next();
+
+ // regexps may have modifiers /regexp/MOD , so fetch those, too
+ // Only [gim] are valid, but if the user puts in garbage, do what we can to take it.
+ resulting_string += this._input.read(acorn.identifier);
+ }
+ return this._create_token(TOKEN.STRING, resulting_string);
+ }
+ return null;
+};
+
+Tokenizer.prototype._read_xml = function(c, previous_token) {
+
+ if (this._options.e4x && c === "<" && this._allow_regexp_or_xml(previous_token)) {
+ var xmlStr = '';
+ var match = this.__patterns.xml.read_match();
+ // handle e4x xml literals
+ //
+ if (match) {
+ // Trim root tag to attempt to
+ var rootTag = match[2].replace(/^{\s+/, '{').replace(/\s+}$/, '}');
+ var isCurlyRoot = rootTag.indexOf('{') === 0;
+ var depth = 0;
+ while (match) {
+ var isEndTag = !!match[1];
+ var tagName = match[2];
+ var isSingletonTag = (!!match[match.length - 1]) || (tagName.slice(0, 8) === "![CDATA[");
+ if (!isSingletonTag &&
+ (tagName === rootTag || (isCurlyRoot && tagName.replace(/^{\s+/, '{').replace(/\s+}$/, '}')))) {
+ if (isEndTag) {
+ --depth;
+ } else {
+ ++depth;
+ }
+ }
+ xmlStr += match[0];
+ if (depth <= 0) {
+ break;
+ }
+ match = this.__patterns.xml.read_match();
+ }
+ // if we didn't close correctly, keep unformatted.
+ if (!match) {
+ xmlStr += this._input.match(/[\s\S]*/g)[0];
+ }
+ xmlStr = xmlStr.replace(acorn.allLineBreaks, '\n');
+ return this._create_token(TOKEN.STRING, xmlStr);
+ }
+ }
+
+ return null;
+};
+
+function unescape_string(s) {
+ // You think that a regex would work for this
+ // return s.replace(/\\x([0-9a-f]{2})/gi, function(match, val) {
+ // return String.fromCharCode(parseInt(val, 16));
+ // })
+ // However, dealing with '\xff', '\\xff', '\\\xff' makes this more fun.
+ var out = '',
+ escaped = 0;
+
+ var input_scan = new InputScanner(s);
+ var matched = null;
+
+ while (input_scan.hasNext()) {
+ // Keep any whitespace, non-slash characters
+ // also keep slash pairs.
+ matched = input_scan.match(/([\s]|[^\\]|\\\\)+/g);
+
+ if (matched) {
+ out += matched[0];
+ }
+
+ if (input_scan.peek() === '\\') {
+ input_scan.next();
+ if (input_scan.peek() === 'x') {
+ matched = input_scan.match(/x([0-9A-Fa-f]{2})/g);
+ } else if (input_scan.peek() === 'u') {
+ matched = input_scan.match(/u([0-9A-Fa-f]{4})/g);
+ } else {
+ out += '\\';
+ if (input_scan.hasNext()) {
+ out += input_scan.next();
+ }
+ continue;
+ }
+
+ // If there's some error decoding, return the original string
+ if (!matched) {
+ return s;
+ }
+
+ escaped = parseInt(matched[1], 16);
+
+ if (escaped > 0x7e && escaped <= 0xff && matched[0].indexOf('x') === 0) {
+ // we bail out on \x7f..\xff,
+ // leaving whole string escaped,
+ // as it's probably completely binary
+ return s;
+ } else if (escaped >= 0x00 && escaped < 0x20) {
+ // leave 0x00...0x1f escaped
+ out += '\\' + matched[0];
+ continue;
+ } else if (escaped === 0x22 || escaped === 0x27 || escaped === 0x5c) {
+ // single-quote, apostrophe, backslash - escape these
+ out += '\\' + String.fromCharCode(escaped);
+ } else {
+ out += String.fromCharCode(escaped);
+ }
+ }
+ }
+
+ return out;
+}
+
+// handle string
+//
+Tokenizer.prototype._read_string_recursive = function(delimiter, allow_unescaped_newlines, start_sub) {
+ var current_char;
+ var pattern;
+ if (delimiter === '\'') {
+ pattern = this.__patterns.single_quote;
+ } else if (delimiter === '"') {
+ pattern = this.__patterns.double_quote;
+ } else if (delimiter === '`') {
+ pattern = this.__patterns.template_text;
+ } else if (delimiter === '}') {
+ pattern = this.__patterns.template_expression;
+ }
+
+ var resulting_string = pattern.read();
+ var next = '';
+ while (this._input.hasNext()) {
+ next = this._input.next();
+ if (next === delimiter ||
+ (!allow_unescaped_newlines && acorn.newline.test(next))) {
+ this._input.back();
+ break;
+ } else if (next === '\\' && this._input.hasNext()) {
+ current_char = this._input.peek();
+
+ if (current_char === 'x' || current_char === 'u') {
+ this.has_char_escapes = true;
+ } else if (current_char === '\r' && this._input.peek(1) === '\n') {
+ this._input.next();
+ }
+ next += this._input.next();
+ } else if (start_sub) {
+ if (start_sub === '${' && next === '$' && this._input.peek() === '{') {
+ next += this._input.next();
+ }
+
+ if (start_sub === next) {
+ if (delimiter === '`') {
+ next += this._read_string_recursive('}', allow_unescaped_newlines, '`');
+ } else {
+ next += this._read_string_recursive('`', allow_unescaped_newlines, '${');
+ }
+ if (this._input.hasNext()) {
+ next += this._input.next();
+ }
+ }
+ }
+ next += pattern.read();
+ resulting_string += next;
+ }
+
+ return resulting_string;
+};
+
+module.exports.Tokenizer = Tokenizer;
+module.exports.TOKEN = TOKEN;
+module.exports.positionable_operators = positionable_operators.slice();
+module.exports.line_starters = line_starters.slice();
+
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var regexp_has_sticky = RegExp.prototype.hasOwnProperty('sticky');
+
+function InputScanner(input_string) {
+ this.__input = input_string || '';
+ this.__input_length = this.__input.length;
+ this.__position = 0;
+}
+
+InputScanner.prototype.restart = function() {
+ this.__position = 0;
+};
+
+InputScanner.prototype.back = function() {
+ if (this.__position > 0) {
+ this.__position -= 1;
+ }
+};
+
+InputScanner.prototype.hasNext = function() {
+ return this.__position < this.__input_length;
+};
+
+InputScanner.prototype.next = function() {
+ var val = null;
+ if (this.hasNext()) {
+ val = this.__input.charAt(this.__position);
+ this.__position += 1;
+ }
+ return val;
+};
+
+InputScanner.prototype.peek = function(index) {
+ var val = null;
+ index = index || 0;
+ index += this.__position;
+ if (index >= 0 && index < this.__input_length) {
+ val = this.__input.charAt(index);
+ }
+ return val;
+};
+
+// This is a JavaScript only helper function (not in python)
+// Javascript doesn't have a match method
+// and not all implementation support "sticky" flag.
+// If they do not support sticky then both this.match() and this.test() method
+// must get the match and check the index of the match.
+// If sticky is supported and set, this method will use it.
+// Otherwise it will check that global is set, and fall back to the slower method.
+InputScanner.prototype.__match = function(pattern, index) {
+ pattern.lastIndex = index;
+ var pattern_match = pattern.exec(this.__input);
+
+ if (pattern_match && !(regexp_has_sticky && pattern.sticky)) {
+ if (pattern_match.index !== index) {
+ pattern_match = null;
+ }
+ }
+
+ return pattern_match;
+};
+
+InputScanner.prototype.test = function(pattern, index) {
+ index = index || 0;
+ index += this.__position;
+
+ if (index >= 0 && index < this.__input_length) {
+ return !!this.__match(pattern, index);
+ } else {
+ return false;
+ }
+};
+
+InputScanner.prototype.testChar = function(pattern, index) {
+ // test one character regex match
+ var val = this.peek(index);
+ pattern.lastIndex = 0;
+ return val !== null && pattern.test(val);
+};
+
+InputScanner.prototype.match = function(pattern) {
+ var pattern_match = this.__match(pattern, this.__position);
+ if (pattern_match) {
+ this.__position += pattern_match[0].length;
+ } else {
+ pattern_match = null;
+ }
+ return pattern_match;
+};
+
+InputScanner.prototype.read = function(starting_pattern, until_pattern, until_after) {
+ var val = '';
+ var match;
+ if (starting_pattern) {
+ match = this.match(starting_pattern);
+ if (match) {
+ val += match[0];
+ }
+ }
+ if (until_pattern && (match || !starting_pattern)) {
+ val += this.readUntil(until_pattern, until_after);
+ }
+ return val;
+};
+
+InputScanner.prototype.readUntil = function(pattern, until_after) {
+ var val = '';
+ var match_index = this.__position;
+ pattern.lastIndex = this.__position;
+ var pattern_match = pattern.exec(this.__input);
+ if (pattern_match) {
+ match_index = pattern_match.index;
+ if (until_after) {
+ match_index += pattern_match[0].length;
+ }
+ } else {
+ match_index = this.__input_length;
+ }
+
+ val = this.__input.substring(this.__position, match_index);
+ this.__position = match_index;
+ return val;
+};
+
+InputScanner.prototype.readUntilAfter = function(pattern) {
+ return this.readUntil(pattern, true);
+};
+
+InputScanner.prototype.get_regexp = function(pattern, match_from) {
+ var result = null;
+ var flags = 'g';
+ if (match_from && regexp_has_sticky) {
+ flags = 'y';
+ }
+ // strings are converted to regexp
+ if (typeof pattern === "string" && pattern !== '') {
+ // result = new RegExp(pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), flags);
+ result = new RegExp(pattern, flags);
+ } else if (pattern) {
+ result = new RegExp(pattern.source, flags);
+ }
+ return result;
+};
+
+InputScanner.prototype.get_literal_regexp = function(literal_string) {
+ return RegExp(literal_string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
+};
+
+/* css beautifier legacy helpers */
+InputScanner.prototype.peekUntilAfter = function(pattern) {
+ var start = this.__position;
+ var val = this.readUntilAfter(pattern);
+ this.__position = start;
+ return val;
+};
+
+InputScanner.prototype.lookBack = function(testVal) {
+ var start = this.__position - 1;
+ return start >= testVal.length && this.__input.substring(start - testVal.length, start)
+ .toLowerCase() === testVal;
+};
+
+module.exports.InputScanner = InputScanner;
+
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var InputScanner = __webpack_require__(8).InputScanner;
+var Token = __webpack_require__(3).Token;
+var TokenStream = __webpack_require__(10).TokenStream;
+var WhitespacePattern = __webpack_require__(11).WhitespacePattern;
+
+var TOKEN = {
+ START: 'TK_START',
+ RAW: 'TK_RAW',
+ EOF: 'TK_EOF'
+};
+
+var Tokenizer = function(input_string, options) {
+ this._input = new InputScanner(input_string);
+ this._options = options || {};
+ this.__tokens = null;
+
+ this._patterns = {};
+ this._patterns.whitespace = new WhitespacePattern(this._input);
+};
+
+Tokenizer.prototype.tokenize = function() {
+ this._input.restart();
+ this.__tokens = new TokenStream();
+
+ this._reset();
+
+ var current;
+ var previous = new Token(TOKEN.START, '');
+ var open_token = null;
+ var open_stack = [];
+ var comments = new TokenStream();
+
+ while (previous.type !== TOKEN.EOF) {
+ current = this._get_next_token(previous, open_token);
+ while (this._is_comment(current)) {
+ comments.add(current);
+ current = this._get_next_token(previous, open_token);
+ }
+
+ if (!comments.isEmpty()) {
+ current.comments_before = comments;
+ comments = new TokenStream();
+ }
+
+ current.parent = open_token;
+
+ if (this._is_opening(current)) {
+ open_stack.push(open_token);
+ open_token = current;
+ } else if (open_token && this._is_closing(current, open_token)) {
+ current.opened = open_token;
+ open_token.closed = current;
+ open_token = open_stack.pop();
+ current.parent = open_token;
+ }
+
+ current.previous = previous;
+ previous.next = current;
+
+ this.__tokens.add(current);
+ previous = current;
+ }
+
+ return this.__tokens;
+};
+
+
+Tokenizer.prototype._is_first_token = function() {
+ return this.__tokens.isEmpty();
+};
+
+Tokenizer.prototype._reset = function() {};
+
+Tokenizer.prototype._get_next_token = function(previous_token, open_token) { // jshint unused:false
+ this._readWhitespace();
+ var resulting_string = this._input.read(/.+/g);
+ if (resulting_string) {
+ return this._create_token(TOKEN.RAW, resulting_string);
+ } else {
+ return this._create_token(TOKEN.EOF, '');
+ }
+};
+
+Tokenizer.prototype._is_comment = function(current_token) { // jshint unused:false
+ return false;
+};
+
+Tokenizer.prototype._is_opening = function(current_token) { // jshint unused:false
+ return false;
+};
+
+Tokenizer.prototype._is_closing = function(current_token, open_token) { // jshint unused:false
+ return false;
+};
+
+Tokenizer.prototype._create_token = function(type, text) {
+ var token = new Token(type, text,
+ this._patterns.whitespace.newline_count,
+ this._patterns.whitespace.whitespace_before_token);
+ return token;
+};
+
+Tokenizer.prototype._readWhitespace = function() {
+ return this._patterns.whitespace.read();
+};
+
+
+
+module.exports.Tokenizer = Tokenizer;
+module.exports.TOKEN = TOKEN;
+
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function TokenStream(parent_token) {
+ // private
+ this.__tokens = [];
+ this.__tokens_length = this.__tokens.length;
+ this.__position = 0;
+ this.__parent_token = parent_token;
+}
+
+TokenStream.prototype.restart = function() {
+ this.__position = 0;
+};
+
+TokenStream.prototype.isEmpty = function() {
+ return this.__tokens_length === 0;
+};
+
+TokenStream.prototype.hasNext = function() {
+ return this.__position < this.__tokens_length;
+};
+
+TokenStream.prototype.next = function() {
+ var val = null;
+ if (this.hasNext()) {
+ val = this.__tokens[this.__position];
+ this.__position += 1;
+ }
+ return val;
+};
+
+TokenStream.prototype.peek = function(index) {
+ var val = null;
+ index = index || 0;
+ index += this.__position;
+ if (index >= 0 && index < this.__tokens_length) {
+ val = this.__tokens[index];
+ }
+ return val;
+};
+
+TokenStream.prototype.add = function(token) {
+ if (this.__parent_token) {
+ token.parent = this.__parent_token;
+ }
+ this.__tokens.push(token);
+ this.__tokens_length += 1;
+};
+
+module.exports.TokenStream = TokenStream;
+
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Pattern = __webpack_require__(12).Pattern;
+
+function WhitespacePattern(input_scanner, parent) {
+ Pattern.call(this, input_scanner, parent);
+ if (parent) {
+ this._line_regexp = this._input.get_regexp(parent._line_regexp);
+ } else {
+ this.__set_whitespace_patterns('', '');
+ }
+
+ this.newline_count = 0;
+ this.whitespace_before_token = '';
+}
+WhitespacePattern.prototype = new Pattern();
+
+WhitespacePattern.prototype.__set_whitespace_patterns = function(whitespace_chars, newline_chars) {
+ whitespace_chars += '\\t ';
+ newline_chars += '\\n\\r';
+
+ this._match_pattern = this._input.get_regexp(
+ '[' + whitespace_chars + newline_chars + ']+', true);
+ this._newline_regexp = this._input.get_regexp(
+ '\\r\\n|[' + newline_chars + ']');
+};
+
+WhitespacePattern.prototype.read = function() {
+ this.newline_count = 0;
+ this.whitespace_before_token = '';
+
+ var resulting_string = this._input.read(this._match_pattern);
+ if (resulting_string === ' ') {
+ this.whitespace_before_token = ' ';
+ } else if (resulting_string) {
+ var matches = this.__split(this._newline_regexp, resulting_string);
+ this.newline_count = matches.length - 1;
+ this.whitespace_before_token = matches[this.newline_count];
+ }
+
+ return resulting_string;
+};
+
+WhitespacePattern.prototype.matching = function(whitespace_chars, newline_chars) {
+ var result = this._create();
+ result.__set_whitespace_patterns(whitespace_chars, newline_chars);
+ result._update();
+ return result;
+};
+
+WhitespacePattern.prototype._create = function() {
+ return new WhitespacePattern(this._input, this);
+};
+
+WhitespacePattern.prototype.__split = function(regexp, input_string) {
+ regexp.lastIndex = 0;
+ var start_index = 0;
+ var result = [];
+ var next_match = regexp.exec(input_string);
+ while (next_match) {
+ result.push(input_string.substring(start_index, next_match.index));
+ start_index = next_match.index + next_match[0].length;
+ next_match = regexp.exec(input_string);
+ }
+
+ if (start_index < input_string.length) {
+ result.push(input_string.substring(start_index, input_string.length));
+ } else {
+ result.push('');
+ }
+
+ return result;
+};
+
+
+
+module.exports.WhitespacePattern = WhitespacePattern;
+
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Pattern(input_scanner, parent) {
+ this._input = input_scanner;
+ this._starting_pattern = null;
+ this._match_pattern = null;
+ this._until_pattern = null;
+ this._until_after = false;
+
+ if (parent) {
+ this._starting_pattern = this._input.get_regexp(parent._starting_pattern, true);
+ this._match_pattern = this._input.get_regexp(parent._match_pattern, true);
+ this._until_pattern = this._input.get_regexp(parent._until_pattern);
+ this._until_after = parent._until_after;
+ }
+}
+
+Pattern.prototype.read = function() {
+ var result = this._input.read(this._starting_pattern);
+ if (!this._starting_pattern || result) {
+ result += this._input.read(this._match_pattern, this._until_pattern, this._until_after);
+ }
+ return result;
+};
+
+Pattern.prototype.read_match = function() {
+ return this._input.match(this._match_pattern);
+};
+
+Pattern.prototype.until_after = function(pattern) {
+ var result = this._create();
+ result._until_after = true;
+ result._until_pattern = this._input.get_regexp(pattern);
+ result._update();
+ return result;
+};
+
+Pattern.prototype.until = function(pattern) {
+ var result = this._create();
+ result._until_after = false;
+ result._until_pattern = this._input.get_regexp(pattern);
+ result._update();
+ return result;
+};
+
+Pattern.prototype.starting_with = function(pattern) {
+ var result = this._create();
+ result._starting_pattern = this._input.get_regexp(pattern, true);
+ result._update();
+ return result;
+};
+
+Pattern.prototype.matching = function(pattern) {
+ var result = this._create();
+ result._match_pattern = this._input.get_regexp(pattern, true);
+ result._update();
+ return result;
+};
+
+Pattern.prototype._create = function() {
+ return new Pattern(this._input, this);
+};
+
+Pattern.prototype._update = function() {};
+
+module.exports.Pattern = Pattern;
+
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+function Directives(start_block_pattern, end_block_pattern) {
+ start_block_pattern = typeof start_block_pattern === 'string' ? start_block_pattern : start_block_pattern.source;
+ end_block_pattern = typeof end_block_pattern === 'string' ? end_block_pattern : end_block_pattern.source;
+ this.__directives_block_pattern = new RegExp(start_block_pattern + / beautify( \w+[:]\w+)+ /.source + end_block_pattern, 'g');
+ this.__directive_pattern = / (\w+)[:](\w+)/g;
+
+ this.__directives_end_ignore_pattern = new RegExp(start_block_pattern + /\sbeautify\signore:end\s/.source + end_block_pattern, 'g');
+}
+
+Directives.prototype.get_directives = function(text) {
+ if (!text.match(this.__directives_block_pattern)) {
+ return null;
+ }
+
+ var directives = {};
+ this.__directive_pattern.lastIndex = 0;
+ var directive_match = this.__directive_pattern.exec(text);
+
+ while (directive_match) {
+ directives[directive_match[1]] = directive_match[2];
+ directive_match = this.__directive_pattern.exec(text);
+ }
+
+ return directives;
+};
+
+Directives.prototype.readIgnored = function(input) {
+ return input.readUntilAfter(this.__directives_end_ignore_pattern);
+};
+
+
+module.exports.Directives = Directives;
+
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/*jshint node:true */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+
+
+var Pattern = __webpack_require__(12).Pattern;
+
+
+var template_names = {
+ django: false,
+ erb: false,
+ handlebars: false,
+ php: false
+};
+
+// This lets templates appear anywhere we would do a readUntil
+// The cost is higher but it is pay to play.
+function TemplatablePattern(input_scanner, parent) {
+ Pattern.call(this, input_scanner, parent);
+ this.__template_pattern = null;
+ this._disabled = Object.assign({}, template_names);
+ this._excluded = Object.assign({}, template_names);
+
+ if (parent) {
+ this.__template_pattern = this._input.get_regexp(parent.__template_pattern);
+ this._excluded = Object.assign(this._excluded, parent._excluded);
+ this._disabled = Object.assign(this._disabled, parent._disabled);
+ }
+ var pattern = new Pattern(input_scanner);
+ this.__patterns = {
+ handlebars_comment: pattern.starting_with(/{{!--/).until_after(/--}}/),
+ handlebars_unescaped: pattern.starting_with(/{{{/).until_after(/}}}/),
+ handlebars: pattern.starting_with(/{{/).until_after(/}}/),
+ php: pattern.starting_with(/<\?(?:[=]|php)/).until_after(/\?>/),
+ erb: pattern.starting_with(/<%[^%]/).until_after(/[^%]%>/),
+ // django coflicts with handlebars a bit.
+ django: pattern.starting_with(/{%/).until_after(/%}/),
+ django_value: pattern.starting_with(/{{/).until_after(/}}/),
+ django_comment: pattern.starting_with(/{#/).until_after(/#}/)
+ };
+}
+TemplatablePattern.prototype = new Pattern();
+
+TemplatablePattern.prototype._create = function() {
+ return new TemplatablePattern(this._input, this);
+};
+
+TemplatablePattern.prototype._update = function() {
+ this.__set_templated_pattern();
+};
+
+TemplatablePattern.prototype.disable = function(language) {
+ var result = this._create();
+ result._disabled[language] = true;
+ result._update();
+ return result;
+};
+
+TemplatablePattern.prototype.read_options = function(options) {
+ var result = this._create();
+ for (var language in template_names) {
+ result._disabled[language] = options.templating.indexOf(language) === -1;
+ }
+ result._update();
+ return result;
+};
+
+TemplatablePattern.prototype.exclude = function(language) {
+ var result = this._create();
+ result._excluded[language] = true;
+ result._update();
+ return result;
+};
+
+TemplatablePattern.prototype.read = function() {
+ var result = '';
+ if (this._match_pattern) {
+ result = this._input.read(this._starting_pattern);
+ } else {
+ result = this._input.read(this._starting_pattern, this.__template_pattern);
+ }
+ var next = this._read_template();
+ while (next) {
+ if (this._match_pattern) {
+ next += this._input.read(this._match_pattern);
+ } else {
+ next += this._input.readUntil(this.__template_pattern);
+ }
+ result += next;
+ next = this._read_template();
+ }
+
+ if (this._until_after) {
+ result += this._input.readUntilAfter(this._until_pattern);
+ }
+ return result;
+};
+
+TemplatablePattern.prototype.__set_templated_pattern = function() {
+ var items = [];
+
+ if (!this._disabled.php) {
+ items.push(this.__patterns.php._starting_pattern.source);
+ }
+ if (!this._disabled.handlebars) {
+ items.push(this.__patterns.handlebars._starting_pattern.source);
+ }
+ if (!this._disabled.erb) {
+ items.push(this.__patterns.erb._starting_pattern.source);
+ }
+ if (!this._disabled.django) {
+ items.push(this.__patterns.django._starting_pattern.source);
+ items.push(this.__patterns.django_value._starting_pattern.source);
+ items.push(this.__patterns.django_comment._starting_pattern.source);
+ }
+
+ if (this._until_pattern) {
+ items.push(this._until_pattern.source);
+ }
+ this.__template_pattern = this._input.get_regexp('(?:' + items.join('|') + ')');
+};
+
+TemplatablePattern.prototype._read_template = function() {
+ var resulting_string = '';
+ var c = this._input.peek();
+ if (c === '<') {
+ var peek1 = this._input.peek(1);
+ //if we're in a comment, do something special
+ // We treat all comments as literals, even more than preformatted tags
+ // we just look for the appropriate close tag
+ if (!this._disabled.php && !this._excluded.php && peek1 === '?') {
+ resulting_string = resulting_string ||
+ this.__patterns.php.read();
+ }
+ if (!this._disabled.erb && !this._excluded.erb && peek1 === '%') {
+ resulting_string = resulting_string ||
+ this.__patterns.erb.read();
+ }
+ } else if (c === '{') {
+ if (!this._disabled.handlebars && !this._excluded.handlebars) {
+ resulting_string = resulting_string ||
+ this.__patterns.handlebars_comment.read();
+ resulting_string = resulting_string ||
+ this.__patterns.handlebars_unescaped.read();
+ resulting_string = resulting_string ||
+ this.__patterns.handlebars.read();
+ }
+ if (!this._disabled.django) {
+ // django coflicts with handlebars a bit.
+ if (!this._excluded.django && !this._excluded.handlebars) {
+ resulting_string = resulting_string ||
+ this.__patterns.django_value.read();
+ }
+ if (!this._excluded.django) {
+ resulting_string = resulting_string ||
+ this.__patterns.django_comment.read();
+ resulting_string = resulting_string ||
+ this.__patterns.django.read();
+ }
+ }
+ }
+ return resulting_string;
+};
+
+
+module.exports.TemplatablePattern = TemplatablePattern;
+
+
+/***/ })
+/******/ ]);
+var js_beautify = legacy_beautify_js;
+/* Footer */
+if (typeof define === "function" && define.amd) {
+ // Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
+ define([], function() {
+ return { js_beautify: js_beautify };
+ });
+} else if (typeof exports !== "undefined") {
+ // Add support for CommonJS. Just put this file somewhere on your require.paths
+ // and you will be able to `var js_beautify = require("beautify").js_beautify`.
+ exports.js_beautify = js_beautify;
+} else if (typeof window !== "undefined") {
+ // If we're running a web page and don't have either of the above, add our one global
+ window.js_beautify = js_beautify;
+} else if (typeof global !== "undefined") {
+ // If we don't even have window, try global.
+ global.js_beautify = js_beautify;
+}
+
+}());
diff --git a/devtools/shared/jsbeautify/src/moz.build b/devtools/shared/jsbeautify/src/moz.build
new file mode 100644
index 0000000000..96a0204808
--- /dev/null
+++ b/devtools/shared/jsbeautify/src/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(
+ 'beautify-css.js',
+ 'beautify-html.js',
+ 'beautify-js.js'
+)
diff --git a/devtools/shared/l10n.js b/devtools/shared/l10n.js
new file mode 100644
index 0000000000..6f7b0773fb
--- /dev/null
+++ b/devtools/shared/l10n.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
+const { sprintf } = require("devtools/shared/sprintfjs/sprintf");
+
+const propertiesMap = {};
+
+// Map used to memoize Number formatters.
+const numberFormatters = new Map();
+const getNumberFormatter = function (decimals) {
+ let formatter = numberFormatters.get(decimals);
+ if (!formatter) {
+ // Create and memoize a formatter for the provided decimals
+ formatter = Intl.NumberFormat(undefined, {
+ maximumFractionDigits: decimals,
+ minimumFractionDigits: decimals,
+ });
+ numberFormatters.set(decimals, formatter);
+ }
+
+ return formatter;
+};
+
+/**
+ * Memoized getter for properties files that ensures a given url is only required and
+ * parsed once.
+ *
+ * @param {String} url
+ * The URL of the properties file to parse.
+ * @return {Object} parsed properties mapped in an object.
+ */
+function getProperties(url) {
+ if (!propertiesMap[url]) {
+ let propertiesFile;
+ let isNodeEnv = false;
+ try {
+ // eslint-disable-next-line no-undef
+ isNodeEnv = process?.release?.name == "node";
+ } catch (e) {}
+
+ if (isNodeEnv) {
+ // In Node environment (e.g. when running jest test), we need to prepend the en-US
+ // to the filename in order to have the actual location of the file in source.
+ const lastDelimIndex = url.lastIndexOf("/");
+ const defaultLocaleUrl =
+ url.substring(0, lastDelimIndex) +
+ "/en-US" +
+ url.substring(lastDelimIndex);
+
+ const path = require("path");
+ // eslint-disable-next-line no-undef
+ const rootPath = path.join(__dirname, "../../");
+ const absoluteUrl = path.join(rootPath, defaultLocaleUrl);
+ const { readFileSync } = require("fs");
+ // In Node environment we directly use readFileSync to get the file content instead
+ // of relying on custom raw loader, like we do in regular environment.
+ propertiesFile = readFileSync(absoluteUrl, { encoding: "utf8" });
+ } else {
+ propertiesFile = require("raw!" + url);
+ }
+
+ propertiesMap[url] = parsePropertiesFile(propertiesFile);
+ }
+
+ return propertiesMap[url];
+}
+
+/**
+ * Localization convenience methods.
+ *
+ * @param string stringBundleName
+ * The desired string bundle's name.
+ * @param boolean strict
+ * (legacy) pass true to force the helper to throw if the l10n id cannot be found.
+ */
+function LocalizationHelper(stringBundleName, strict = false) {
+ this.stringBundleName = stringBundleName;
+ this.strict = strict;
+}
+
+LocalizationHelper.prototype = {
+ /**
+ * L10N shortcut function.
+ *
+ * @param string name
+ * @return string
+ */
+ getStr(name) {
+ const properties = getProperties(this.stringBundleName);
+ if (name in properties) {
+ return properties[name];
+ }
+
+ if (this.strict) {
+ throw new Error("No localization found for [" + name + "]");
+ }
+
+ console.error("No localization found for [" + name + "]");
+ return name;
+ },
+
+ /**
+ * L10N shortcut function.
+ *
+ * @param string name
+ * @param array args
+ * @return string
+ */
+ getFormatStr(name, ...args) {
+ return sprintf(this.getStr(name), ...args);
+ },
+
+ /**
+ * L10N shortcut function for numeric arguments that need to be formatted.
+ * All numeric arguments will be fixed to 2 decimals and given a localized
+ * decimal separator. Other arguments will be left alone.
+ *
+ * @param string name
+ * @param array args
+ * @return string
+ */
+ getFormatStrWithNumbers(name, ...args) {
+ const newArgs = args.map(x => {
+ return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
+ });
+
+ return this.getFormatStr(name, ...newArgs);
+ },
+
+ /**
+ * Converts a number to a locale-aware string format and keeps a certain
+ * number of decimals.
+ *
+ * @param number number
+ * The number to convert.
+ * @param number decimals [optional]
+ * Total decimals to keep.
+ * @return string
+ * The localized number as a string.
+ */
+ numberWithDecimals(number, decimals = 0) {
+ // Do not show decimals for integers.
+ if (number === (number | 0)) {
+ return getNumberFormatter(0).format(number);
+ }
+
+ // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
+ if (isNaN(number) || number === null) {
+ return getNumberFormatter(0).format(0);
+ }
+
+ // Localize the number using a memoized Intl.NumberFormat formatter.
+ const localized = getNumberFormatter(decimals).format(number);
+
+ // Convert the localized number to a number again.
+ const localizedNumber = localized * 1;
+ // Check if this number is now equal to an integer.
+ if (localizedNumber === (localizedNumber | 0)) {
+ // If it is, remove the fraction part.
+ return getNumberFormatter(0).format(localizedNumber);
+ }
+
+ return localized;
+ },
+};
+
+function getPropertiesForNode(node) {
+ const bundleEl = node.closest("[data-localization-bundle]");
+ if (!bundleEl) {
+ return null;
+ }
+
+ const propertiesUrl = bundleEl.getAttribute("data-localization-bundle");
+ return getProperties(propertiesUrl);
+}
+
+/**
+ * Translate existing markup annotated with data-localization attributes.
+ *
+ * How to use data-localization in markup:
+ *
+ * <div data-localization="content=myContent;title=myTitle"/>
+ *
+ * The data-localization attribute identifies an element as being localizable.
+ * The content of the attribute is semi-colon separated list of descriptors.
+ * - "title=myTitle" means the "title" attribute should be replaced with the localized
+ * string corresponding to the key "myTitle".
+ * - "content=myContent" means the text content of the node should be replaced by the
+ * string corresponding to "myContent"
+ *
+ * How to define the localization bundle in markup:
+ *
+ * <div data-localization-bundle="url/to/my.properties">
+ * [...]
+ * <div data-localization="content=myContent;title=myTitle"/>
+ *
+ * Set the data-localization-bundle on an ancestor of the nodes that should be localized.
+ *
+ * @param {Element} root
+ * The root node to use for the localization
+ */
+function localizeMarkup(root) {
+ const elements = root.querySelectorAll("[data-localization]");
+ for (const element of elements) {
+ const properties = getPropertiesForNode(element);
+ if (!properties) {
+ continue;
+ }
+
+ const attributes = element.getAttribute("data-localization").split(";");
+ for (const attribute of attributes) {
+ const [name, value] = attribute.trim().split("=");
+ if (name === "content") {
+ element.textContent = properties[value];
+ } else {
+ element.setAttribute(name, properties[value]);
+ }
+ }
+
+ element.removeAttribute("data-localization");
+ }
+}
+
+const sharedL10N = new LocalizationHelper(
+ "devtools/shared/locales/shared.properties"
+);
+
+/**
+ * A helper for having the same interface as LocalizationHelper, but for more
+ * than one file. Useful for abstracting l10n string locations.
+ */
+function MultiLocalizationHelper(...stringBundleNames) {
+ const instances = stringBundleNames.map(bundle => {
+ // Use strict = true because the MultiLocalizationHelper logic relies on try/catch
+ // around the underlying LocalizationHelper APIs.
+ return new LocalizationHelper(bundle, true);
+ });
+
+ // Get all function members of the LocalizationHelper class, making sure we're
+ // not executing any potential getters while doing so, and wrap all the
+ // methods we've found to work on all given string bundles.
+ Object.getOwnPropertyNames(LocalizationHelper.prototype)
+ .map(name => ({
+ name,
+ descriptor: Object.getOwnPropertyDescriptor(
+ LocalizationHelper.prototype,
+ name
+ ),
+ }))
+ .filter(({ descriptor }) => descriptor.value instanceof Function)
+ .forEach(method => {
+ this[method.name] = (...args) => {
+ for (const l10n of instances) {
+ try {
+ return method.descriptor.value.apply(l10n, args);
+ } catch (e) {
+ // Do nothing
+ }
+ }
+ return null;
+ };
+ });
+}
+
+exports.LocalizationHelper = LocalizationHelper;
+exports.localizeMarkup = localizeMarkup;
+exports.MultiLocalizationHelper = MultiLocalizationHelper;
+Object.defineProperty(exports, "ELLIPSIS", {
+ get: () => sharedL10N.getStr("ellipsis"),
+});
diff --git a/devtools/shared/layout/dom-matrix-2d.js b/devtools/shared/layout/dom-matrix-2d.js
new file mode 100644
index 0000000000..f6e3e73067
--- /dev/null
+++ b/devtools/shared/layout/dom-matrix-2d.js
@@ -0,0 +1,297 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Returns a matrix for the scaling given.
+ * Calling `scale()` or `scale(1) returns a new identity matrix.
+ *
+ * @param {Number} [sx = 1]
+ * the abscissa of the scaling vector.
+ * If unspecified, it will equal to `1`.
+ * @param {Number} [sy = sx]
+ * The ordinate of the scaling vector.
+ * If not present, its default value is `sx`, leading to a uniform scaling.
+ * @return {Array}
+ * The new matrix.
+ */
+const scale = (sx = 1, sy = sx) => [sx, 0, 0, 0, sy, 0, 0, 0, 1];
+exports.scale = scale;
+
+/**
+ * Returns a matrix for the translation given.
+ * Calling `translate()` or `translate(0) returns a new identity matrix.
+ *
+ * @param {Number} [tx = 0]
+ * The abscissa of the translating vector.
+ * If unspecified, it will equal to `0`.
+ * @param {Number} [ty = tx]
+ * The ordinate of the translating vector.
+ * If unspecified, it will equal to `tx`.
+ * @return {Array}
+ * The new matrix.
+ */
+const translate = (tx = 0, ty = tx) => [1, 0, tx, 0, 1, ty, 0, 0, 1];
+exports.translate = translate;
+
+/**
+ * Returns a matrix that reflects about the Y axis. For example, the point (x1, y1) would
+ * become (-x1, y1).
+ *
+ * @return {Array}
+ * The new matrix.
+ */
+const reflectAboutY = () => [-1, 0, 0, 0, 1, 0, 0, 0, 1];
+exports.reflectAboutY = reflectAboutY;
+
+/**
+ * Returns a matrix for the rotation given.
+ * Calling `rotate()` or `rotate(0)` returns a new identity matrix.
+ *
+ * @param {Number} [angle = 0]
+ * The angle, in radians, for which to return a corresponding rotation matrix.
+ * If unspecified, it will equal `0`.
+ * @return {Array}
+ * The new matrix.
+ */
+const rotate = (angle = 0) => {
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+
+ return [cos, sin, 0, -sin, cos, 0, 0, 0, 1];
+};
+exports.rotate = rotate;
+
+/**
+ * Returns a new identity matrix.
+ *
+ * @return {Array}
+ * The new matrix.
+ */
+const identity = () => [1, 0, 0, 0, 1, 0, 0, 0, 1];
+exports.identity = identity;
+
+/**
+ * Multiplies two matrices and returns a new matrix with the result.
+ *
+ * @param {Array} M1
+ * The first operand.
+ * @param {Array} M2
+ * The second operand.
+ * @return {Array}
+ * The resulting matrix.
+ */
+const multiply = (M1, M2) => {
+ const c11 = M1[0] * M2[0] + M1[1] * M2[3] + M1[2] * M2[6];
+ const c12 = M1[0] * M2[1] + M1[1] * M2[4] + M1[2] * M2[7];
+ const c13 = M1[0] * M2[2] + M1[1] * M2[5] + M1[2] * M2[8];
+
+ const c21 = M1[3] * M2[0] + M1[4] * M2[3] + M1[5] * M2[6];
+ const c22 = M1[3] * M2[1] + M1[4] * M2[4] + M1[5] * M2[7];
+ const c23 = M1[3] * M2[2] + M1[4] * M2[5] + M1[5] * M2[8];
+
+ const c31 = M1[6] * M2[0] + M1[7] * M2[3] + M1[8] * M2[6];
+ const c32 = M1[6] * M2[1] + M1[7] * M2[4] + M1[8] * M2[7];
+ const c33 = M1[6] * M2[2] + M1[7] * M2[5] + M1[8] * M2[8];
+
+ return [c11, c12, c13, c21, c22, c23, c31, c32, c33];
+};
+exports.multiply = multiply;
+
+/**
+ * Applies the given matrix to a point.
+ *
+ * @param {Array} M
+ * The matrix to apply.
+ * @param {Array} P
+ * The point's vector.
+ * @return {Array}
+ * The resulting point's vector.
+ */
+const apply = (M, P) => [
+ M[0] * P[0] + M[1] * P[1] + M[2],
+ M[3] * P[0] + M[4] * P[1] + M[5],
+];
+exports.apply = apply;
+
+/**
+ * Returns `true` if the given matrix is a identity matrix.
+ *
+ * @param {Array} M
+ * The matrix to check
+ * @return {Boolean}
+ * `true` if the matrix passed is a identity matrix, `false` otherwise.
+ */
+const isIdentity = M =>
+ M[0] === 1 &&
+ M[1] === 0 &&
+ M[2] === 0 &&
+ M[3] === 0 &&
+ M[4] === 1 &&
+ M[5] === 0 &&
+ M[6] === 0 &&
+ M[7] === 0 &&
+ M[8] === 1;
+exports.isIdentity = isIdentity;
+
+/**
+ * Get the change of basis matrix and inverted change of basis matrix
+ * for the coordinate system based on the two given vectors, as well as
+ * the lengths of the two given vectors.
+ *
+ * @param {Array} u
+ * The first vector, serving as the "x axis" of the coordinate system.
+ * @param {Array} v
+ * The second vector, serving as the "y axis" of the coordinate system.
+ * @return {Object}
+ * { basis, invertedBasis, uLength, vLength }
+ * basis and invertedBasis are the change of basis matrices. uLength and
+ * vLength are the lengths of u and v.
+ */
+const getBasis = (u, v) => {
+ const uLength = Math.abs(Math.sqrt(u[0] ** 2 + u[1] ** 2));
+ const vLength = Math.abs(Math.sqrt(v[0] ** 2 + v[1] ** 2));
+ const basis = [
+ u[0] / uLength,
+ v[0] / vLength,
+ 0,
+ u[1] / uLength,
+ v[1] / vLength,
+ 0,
+ 0,
+ 0,
+ 1,
+ ];
+ const determinant = 1 / (basis[0] * basis[4] - basis[1] * basis[3]);
+ const invertedBasis = [
+ basis[4] / determinant,
+ -basis[1] / determinant,
+ 0,
+ -basis[3] / determinant,
+ basis[0] / determinant,
+ 0,
+ 0,
+ 0,
+ 1,
+ ];
+ return { basis, invertedBasis, uLength, vLength };
+};
+exports.getBasis = getBasis;
+
+/**
+ * Convert the given matrix to a new coordinate system, based on the change of basis
+ * matrix.
+ *
+ * @param {Array} M
+ * The matrix to convert
+ * @param {Array} basis
+ * The change of basis matrix
+ * @param {Array} invertedBasis
+ * The inverted change of basis matrix
+ * @return {Array}
+ * The converted matrix.
+ */
+const changeMatrixBase = (M, basis, invertedBasis) => {
+ return multiply(invertedBasis, multiply(M, basis));
+};
+exports.changeMatrixBase = changeMatrixBase;
+
+/**
+ * Returns the transformation matrix for the given node, relative to the ancestor passed
+ * as second argument; considering the ancestor transformation too.
+ * If no ancestor is specified, it will returns the transformation matrix relative to the
+ * node's parent element.
+ *
+ * @param {DOMNode} node
+ * The node.
+ * @param {DOMNode} ancestor
+ * The ancestor of the node given.
+ * @return {Array}
+ * The transformation matrix.
+ */
+function getNodeTransformationMatrix(node, ancestor = node.parentElement) {
+ const { a, b, c, d, e, f } = ancestor
+ .getTransformToParent()
+ .multiply(node.getTransformToAncestor(ancestor));
+
+ return [a, c, e, b, d, f, 0, 0, 1];
+}
+exports.getNodeTransformationMatrix = getNodeTransformationMatrix;
+
+/**
+ * Returns the matrix to rotate, translate, and reflect (if needed) from the element's
+ * top-left origin into the actual writing mode and text direction applied to the element.
+ *
+ * @param {Object} size
+ * An element's untransformed content `width` and `height` (excluding any margin,
+ * borders, or padding).
+ * @param {Object} style
+ * The computed `writingMode` and `direction` properties for the element.
+ * @return {Array}
+ * The matrix with adjustments for writing mode and text direction, if any.
+ */
+function getWritingModeMatrix(size, style) {
+ let currentMatrix = identity();
+ const { width, height } = size;
+ const { direction, writingMode } = style;
+
+ switch (writingMode) {
+ case "horizontal-tb":
+ // This is the initial value. No further adjustment needed.
+ break;
+ case "vertical-rl":
+ currentMatrix = multiply(translate(width, 0), rotate(-Math.PI / 2));
+ break;
+ case "vertical-lr":
+ currentMatrix = multiply(reflectAboutY(), rotate(-Math.PI / 2));
+ break;
+ case "sideways-rl":
+ currentMatrix = multiply(translate(width, 0), rotate(-Math.PI / 2));
+ break;
+ case "sideways-lr":
+ currentMatrix = multiply(rotate(Math.PI / 2), translate(-height, 0));
+ break;
+ default:
+ console.error(`Unexpected writing-mode: ${writingMode}`);
+ }
+
+ switch (direction) {
+ case "ltr":
+ // This is the initial value. No further adjustment needed.
+ break;
+ case "rtl":
+ let rowLength = width;
+ if (writingMode != "horizontal-tb") {
+ rowLength = height;
+ }
+ currentMatrix = multiply(currentMatrix, translate(rowLength, 0));
+ currentMatrix = multiply(currentMatrix, reflectAboutY());
+ break;
+ default:
+ console.error(`Unexpected direction: ${direction}`);
+ }
+
+ return currentMatrix;
+}
+exports.getWritingModeMatrix = getWritingModeMatrix;
+
+/**
+ * Convert from the matrix format used in this module:
+ * a, c, e,
+ * b, d, f,
+ * 0, 0, 1
+ * to the format used by the `matrix()` CSS transform function:
+ * a, b, c, d, e, f
+ *
+ * @param {Array} M
+ * The matrix in this module's 9 element format.
+ * @return {String}
+ * The matching 6 element CSS transform function.
+ */
+function getCSSMatrixTransform(M) {
+ const [a, c, e, b, d, f] = M;
+ return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
+}
+exports.getCSSMatrixTransform = getCSSMatrixTransform;
diff --git a/devtools/shared/layout/moz.build b/devtools/shared/layout/moz.build
new file mode 100644
index 0000000000..da30931458
--- /dev/null
+++ b/devtools/shared/layout/moz.build
@@ -0,0 +1,7 @@
+# -*- 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-matrix-2d.js", "utils.js")
diff --git a/devtools/shared/layout/utils.js b/devtools/shared/layout/utils.js
new file mode 100644
index 0000000000..ebd2353414
--- /dev/null
+++ b/devtools/shared/layout/utils.js
@@ -0,0 +1,927 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+loader.lazyRequireGetter(
+ this,
+ "DevToolsUtils",
+ "resource://devtools/shared/DevToolsUtils.js"
+);
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+const SHEET_TYPE = {
+ agent: "AGENT_SHEET",
+ user: "USER_SHEET",
+ author: "AUTHOR_SHEET",
+};
+
+// eslint-disable-next-line no-unused-vars
+loader.lazyRequireGetter(
+ this,
+ "setIgnoreLayoutChanges",
+ "resource://devtools/server/actors/reflow.js",
+ true
+);
+exports.setIgnoreLayoutChanges = (...args) =>
+ this.setIgnoreLayoutChanges(...args);
+
+/**
+ * Returns the `DOMWindowUtils` for the window given.
+ *
+ * @param {DOMWindow} win
+ * @returns {DOMWindowUtils}
+ */
+const utilsCache = new WeakMap();
+function utilsFor(win) {
+ // XXXbz Given that we now have a direct getter for the DOMWindowUtils, is
+ // this weakmap cache path any faster than just calling the getter?
+ if (!utilsCache.has(win)) {
+ utilsCache.set(win, win.windowUtils);
+ }
+ return utilsCache.get(win);
+}
+
+/**
+ * Check a window is part of the boundary window given.
+ *
+ * @param {DOMWindow} boundaryWindow
+ * @param {DOMWindow} win
+ * @return {Boolean}
+ */
+function isWindowIncluded(boundaryWindow, win) {
+ if (win === boundaryWindow) {
+ return true;
+ }
+
+ const parent = win.parent;
+
+ if (!parent || parent === win) {
+ return false;
+ }
+
+ return isWindowIncluded(boundaryWindow, parent);
+}
+exports.isWindowIncluded = isWindowIncluded;
+
+/**
+ * like win.frameElement, but goes through mozbrowsers and mozapps iframes.
+ *
+ * @param {DOMWindow} win
+ * The window to get the frame for
+ * @return {DOMNode}
+ * The element in which the window is embedded.
+ */
+const getFrameElement = win => {
+ const isTopWindow = win && DevToolsUtils.getTopWindow(win) === win;
+ return isTopWindow ? null : win.browsingContext.embedderElement;
+};
+exports.getFrameElement = getFrameElement;
+
+/**
+ * Get the x/y offsets for of all the parent frames of a given node, limited to
+ * the boundary window given.
+ *
+ * @param {DOMWindow} boundaryWindow
+ * The window where to stop to iterate. If `null` is given, the top
+ * window is used.
+ * @param {DOMNode} node
+ * The node for which we are to get the offset
+ * @return {Array}
+ * The frame offset [x, y]
+ */
+function getFrameOffsets(boundaryWindow, node) {
+ let xOffset = 0;
+ let yOffset = 0;
+
+ let frameWin = getWindowFor(node);
+ const scale = getCurrentZoom(node);
+
+ if (boundaryWindow === null) {
+ boundaryWindow = DevToolsUtils.getTopWindow(frameWin);
+ } else if (typeof boundaryWindow === "undefined") {
+ throw new Error("No boundaryWindow given. Use null for the default one.");
+ }
+
+ while (frameWin !== boundaryWindow) {
+ const frameElement = getFrameElement(frameWin);
+ if (!frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ const frameRect = frameElement.getBoundingClientRect();
+
+ const [offsetTop, offsetLeft] = getFrameContentOffset(frameElement);
+
+ xOffset += frameRect.left + offsetLeft;
+ yOffset += frameRect.top + offsetTop;
+
+ frameWin = frameWin.parent;
+ }
+
+ return [xOffset * scale, yOffset * scale];
+}
+exports.getFrameOffsets = getFrameOffsets;
+
+/**
+ * Get box quads adjusted for iframes and zoom level.
+ *
+ * Warning: this function returns things that look like DOMQuad objects but
+ * aren't (they resemble an old version of the spec). Unlike the return value
+ * of node.getBoxQuads, they have a .bounds property and not a .getBounds()
+ * method.
+ *
+ * @param {DOMWindow} boundaryWindow
+ * The window where to stop to iterate. If `null` is given, the top
+ * window is used.
+ * @param {DOMNode} node
+ * The node for which we are to get the box model region
+ * quads.
+ * @param {String} region
+ * The box model region to return: "content", "padding", "border" or
+ * "margin".
+ * @param {Object} [options.ignoreZoom=false]
+ * Ignore zoom used in the context of e.g. canvas.
+ * @return {Array}
+ * An array of objects that have the same structure as quads returned by
+ * getBoxQuads. An empty array if the node has no quads or is invalid.
+ */
+function getAdjustedQuads(
+ boundaryWindow,
+ node,
+ region,
+ { ignoreZoom, ignoreScroll } = {}
+) {
+ if (!node || !node.getBoxQuads) {
+ return [];
+ }
+
+ const quads = node.getBoxQuads({
+ box: region,
+ relativeTo: boundaryWindow.document,
+ createFramesForSuppressedWhitespace: false,
+ });
+
+ if (!quads.length) {
+ return [];
+ }
+
+ const scale = ignoreZoom ? 1 : getCurrentZoom(node);
+ const { scrollX, scrollY } = ignoreScroll
+ ? { scrollX: 0, scrollY: 0 }
+ : boundaryWindow;
+
+ const xOffset = scrollX * scale;
+ const yOffset = scrollY * scale;
+
+ const adjustedQuads = [];
+ for (const quad of quads) {
+ const bounds = quad.getBounds();
+ adjustedQuads.push({
+ p1: {
+ w: quad.p1.w * scale,
+ x: quad.p1.x * scale + xOffset,
+ y: quad.p1.y * scale + yOffset,
+ z: quad.p1.z * scale,
+ },
+ p2: {
+ w: quad.p2.w * scale,
+ x: quad.p2.x * scale + xOffset,
+ y: quad.p2.y * scale + yOffset,
+ z: quad.p2.z * scale,
+ },
+ p3: {
+ w: quad.p3.w * scale,
+ x: quad.p3.x * scale + xOffset,
+ y: quad.p3.y * scale + yOffset,
+ z: quad.p3.z * scale,
+ },
+ p4: {
+ w: quad.p4.w * scale,
+ x: quad.p4.x * scale + xOffset,
+ y: quad.p4.y * scale + yOffset,
+ z: quad.p4.z * scale,
+ },
+ bounds: {
+ bottom: bounds.bottom * scale + yOffset,
+ height: bounds.height * scale,
+ left: bounds.left * scale + xOffset,
+ right: bounds.right * scale + xOffset,
+ top: bounds.top * scale + yOffset,
+ width: bounds.width * scale,
+ x: bounds.x * scale + xOffset,
+ y: bounds.y * scale + yOffset,
+ },
+ });
+ }
+
+ return adjustedQuads;
+}
+exports.getAdjustedQuads = getAdjustedQuads;
+
+/**
+ * Compute the absolute position and the dimensions of a node, relativalely
+ * to the root window.
+
+ * @param {DOMWindow} boundaryWindow
+ * The window where to stop to iterate. If `null` is given, the top
+ * window is used.
+ * @param {DOMNode} node
+ * a DOM element to get the bounds for
+ * @param {DOMWindow} contentWindow
+ * the content window holding the node
+ * @return {Object}
+ * A rect object with the {top, left, width, height} properties
+ */
+function getRect(boundaryWindow, node, contentWindow) {
+ let frameWin = node.ownerDocument.defaultView;
+ const clientRect = node.getBoundingClientRect();
+
+ if (boundaryWindow === null) {
+ boundaryWindow = DevToolsUtils.getTopWindow(frameWin);
+ } else if (typeof boundaryWindow === "undefined") {
+ throw new Error("No boundaryWindow given. Use null for the default one.");
+ }
+
+ // Go up in the tree of frames to determine the correct rectangle.
+ // clientRect is read-only, we need to be able to change properties.
+ const rect = {
+ top: clientRect.top + contentWindow.pageYOffset,
+ left: clientRect.left + contentWindow.pageXOffset,
+ width: clientRect.width,
+ height: clientRect.height,
+ };
+
+ // We iterate through all the parent windows.
+ while (frameWin !== boundaryWindow) {
+ const frameElement = getFrameElement(frameWin);
+ if (!frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ const frameRect = frameElement.getBoundingClientRect();
+
+ const [offsetTop, offsetLeft] = getFrameContentOffset(frameElement);
+
+ rect.top += frameRect.top + offsetTop;
+ rect.left += frameRect.left + offsetLeft;
+
+ frameWin = frameWin.parent;
+ }
+
+ return rect;
+}
+exports.getRect = getRect;
+
+/**
+ * Get the 4 bounding points for a node taking iframes into account.
+ * Note that for transformed nodes, this will return the untransformed bound.
+ *
+ * @param {DOMWindow} boundaryWindow
+ * The window where to stop to iterate. If `null` is given, the top
+ * window is used.
+ * @param {DOMNode} node
+ * @return {Object}
+ * An object with p1,p2,p3,p4 properties being {x,y} objects
+ */
+function getNodeBounds(boundaryWindow, node) {
+ if (!node) {
+ return null;
+ }
+ const { scrollX, scrollY } = boundaryWindow;
+ const scale = getCurrentZoom(node);
+
+ // Find out the offset of the node in its current frame
+ let offsetLeft = 0;
+ let offsetTop = 0;
+ let el = node;
+ while (el?.parentNode) {
+ offsetLeft += el.offsetLeft;
+ offsetTop += el.offsetTop;
+ el = el.offsetParent;
+ }
+
+ // Also take scrolled containers into account
+ el = node;
+ while (el?.parentNode) {
+ if (el.scrollTop) {
+ offsetTop -= el.scrollTop;
+ }
+ if (el.scrollLeft) {
+ offsetLeft -= el.scrollLeft;
+ }
+ el = el.parentNode;
+ }
+
+ // And add the potential frame offset if the node is nested
+ let [xOffset, yOffset] = getFrameOffsets(boundaryWindow, node);
+ xOffset += (offsetLeft + scrollX) * scale;
+ yOffset += (offsetTop + scrollY) * scale;
+
+ // Get the width and height
+ const width = node.offsetWidth * scale;
+ const height = node.offsetHeight * scale;
+
+ return {
+ p1: { x: xOffset, y: yOffset },
+ p2: { x: xOffset + width, y: yOffset },
+ p3: { x: xOffset + width, y: yOffset + height },
+ p4: { x: xOffset, y: yOffset + height },
+ top: yOffset,
+ right: xOffset + width,
+ bottom: yOffset + height,
+ left: xOffset,
+ width,
+ height,
+ };
+}
+exports.getNodeBounds = getNodeBounds;
+
+/**
+ * Same as doing iframe.contentWindow but works with all types of container
+ * elements that act like frames (e.g. <embed>), where 'contentWindow' isn't a
+ * property that can be accessed.
+ * This uses the inIDeepTreeWalker instead.
+ * @param {DOMNode} frame
+ * @return {Window}
+ */
+function safelyGetContentWindow(frame) {
+ if (frame.contentWindow) {
+ return frame.contentWindow;
+ }
+
+ const walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(
+ Ci.inIDeepTreeWalker
+ );
+ walker.showSubDocuments = true;
+ walker.showDocumentsAsNodes = true;
+ walker.init(frame);
+ walker.currentNode = frame;
+
+ const document = walker.nextNode();
+ if (!document || !document.defaultView) {
+ throw new Error("Couldn't get the content window inside frame " + frame);
+ }
+
+ return document.defaultView;
+}
+
+/**
+ * Returns a frame's content offset (frame border + padding).
+ * Note: this function shouldn't need to exist, had the platform provided a
+ * suitable API for determining the offset between the frame's content and
+ * its bounding client rect. Bug 626359 should provide us with such an API.
+ *
+ * @param {DOMNode} frame
+ * The frame.
+ * @return {Array} [offsetTop, offsetLeft]
+ * offsetTop is the distance from the top of the frame and the top of
+ * the content document.
+ * offsetLeft is the distance from the left of the frame and the left
+ * of the content document.
+ */
+function getFrameContentOffset(frame) {
+ const style = safelyGetContentWindow(frame).getComputedStyle(frame);
+
+ // In some cases, the computed style is null
+ if (!style) {
+ return [0, 0];
+ }
+
+ const paddingTop = parseInt(style.getPropertyValue("padding-top"), 10);
+ const paddingLeft = parseInt(style.getPropertyValue("padding-left"), 10);
+
+ const borderTop = parseInt(style.getPropertyValue("border-top-width"), 10);
+ const borderLeft = parseInt(style.getPropertyValue("border-left-width"), 10);
+
+ return [borderTop + paddingTop, borderLeft + paddingLeft];
+}
+
+/**
+ * Check if a node and its document are still alive
+ * and attached to the window.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isNodeConnected(node) {
+ if (!node.ownerDocument || !node.ownerDocument.defaultView) {
+ return false;
+ }
+
+ try {
+ return !(
+ node.compareDocumentPosition(node.ownerDocument.documentElement) &
+ node.DOCUMENT_POSITION_DISCONNECTED
+ );
+ } catch (e) {
+ // "can't access dead object" error
+ return false;
+ }
+}
+exports.isNodeConnected = isNodeConnected;
+
+/**
+ * Determine whether a node is anonymous.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ *
+ * FIXME(bug 1597411): Remove one of these (or both, as
+ * `node.isNativeAnonymous` is quite clear).
+ */
+const isAnonymous = node => node.isNativeAnonymous;
+exports.isAnonymous = isAnonymous;
+exports.isNativeAnonymous = isAnonymous;
+
+/**
+ * Determine whether a node is a template element.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isTemplateElement(node) {
+ return (
+ node.ownerGlobal && node.ownerGlobal.HTMLTemplateElement.isInstance(node)
+ );
+}
+exports.isTemplateElement = isTemplateElement;
+
+/**
+ * Determine whether a node is a shadow root.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+const isShadowRoot = node => node.containingShadowRoot == node;
+exports.isShadowRoot = isShadowRoot;
+
+/*
+ * Gets the shadow root mode (open or closed).
+ *
+ * @param {DOMNode} node
+ * @return {String|null}
+ */
+function getShadowRootMode(node) {
+ return isShadowRoot(node) ? node.mode : null;
+}
+exports.getShadowRootMode = getShadowRootMode;
+
+/**
+ * Determine whether a node is a shadow host, ie. an element that has a shadowRoot
+ * attached to itself.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isShadowHost(node) {
+ const shadowRoot = node.openOrClosedShadowRoot;
+ return shadowRoot && shadowRoot.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
+}
+exports.isShadowHost = isShadowHost;
+
+/**
+ * Determine whether a node is a child of a shadow host. Even if the element has been
+ * assigned to a slot in the attached shadow DOM, the parent node for this element is
+ * still considered to be the "host" element, and we need to walk them differently.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isDirectShadowHostChild(node) {
+ // Pseudo elements and native anonymous elements are always part of the anonymous tree.
+ if (
+ isMarkerPseudoElement(node) ||
+ isBeforePseudoElement(node) ||
+ isAfterPseudoElement(node) ||
+ node.isNativeAnonymous
+ ) {
+ return false;
+ }
+
+ const parentNode = node.parentNode;
+ return parentNode && !!parentNode.openOrClosedShadowRoot;
+}
+exports.isDirectShadowHostChild = isDirectShadowHostChild;
+
+/**
+ * Determine whether a node is a ::marker pseudo.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isMarkerPseudoElement(node) {
+ return node.nodeName === "_moz_generated_content_marker";
+}
+exports.isMarkerPseudoElement = isMarkerPseudoElement;
+
+/**
+ * Determine whether a node is a ::before pseudo.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isBeforePseudoElement(node) {
+ return node.nodeName === "_moz_generated_content_before";
+}
+exports.isBeforePseudoElement = isBeforePseudoElement;
+
+/**
+ * Determine whether a node is a ::after pseudo.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isAfterPseudoElement(node) {
+ return node.nodeName === "_moz_generated_content_after";
+}
+exports.isAfterPseudoElement = isAfterPseudoElement;
+
+/**
+ * Get the current zoom factor applied to the container window of a given node.
+ * @param {DOMNode|DOMWindow}
+ * The node for which the zoom factor should be calculated, or its
+ * owner window.
+ * @return {Number}
+ */
+function getCurrentZoom(node) {
+ const win = getWindowFor(node);
+
+ if (!win) {
+ throw new Error("Unable to get the zoom from the given argument.");
+ }
+
+ return win.browsingContext?.fullZoom || 1.0;
+}
+exports.getCurrentZoom = getCurrentZoom;
+
+/**
+ * Get the display pixel ratio for a given window.
+ * The `devicePixelRatio` property is affected by the zoom (see bug 809788), so we have to
+ * divide by the zoom value in order to get just the display density, expressed as pixel
+ * ratio (the physical display pixel compares to a pixel on a “normal” density screen).
+ *
+ * @param {DOMNode|DOMWindow}
+ * The node for which the zoom factor should be calculated, or its
+ * owner window.
+ * @return {Number}
+ */
+function getDisplayPixelRatio(node) {
+ const win = getWindowFor(node);
+ return win.devicePixelRatio / getCurrentZoom(node);
+}
+exports.getDisplayPixelRatio = getDisplayPixelRatio;
+
+/**
+ * Returns the window's dimensions for the `window` given.
+ *
+ * @return {Object} An object with `width` and `height` properties, representing the
+ * number of pixels for the document's size.
+ */
+function getWindowDimensions(window) {
+ // First we'll try without flushing layout, because it's way faster.
+ const windowUtils = utilsFor(window);
+ let { width, height } = windowUtils.getRootBounds();
+
+ if (!width || !height) {
+ // We need a flush after all :'(
+ width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
+ height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
+
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
+ width -= scrollbarWidth.value;
+ height -= scrollbarHeight.value;
+ }
+
+ return { width, height };
+}
+exports.getWindowDimensions = getWindowDimensions;
+
+/**
+ * Returns the viewport's dimensions for the `window` given.
+ *
+ * @return {Object} An object with `width` and `height` properties, representing the
+ * number of pixels for the viewport's size.
+ */
+function getViewportDimensions(window) {
+ const windowUtils = utilsFor(window);
+
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
+
+ const width = window.innerWidth - scrollbarWidth.value;
+ const height = window.innerHeight - scrollbarHeight.value;
+
+ return { width, height };
+}
+exports.getViewportDimensions = getViewportDimensions;
+
+/**
+ * Return the default view for a given node, where node can be:
+ * - a DOM node
+ * - the document node
+ * - the window itself
+ * @param {DOMNode|DOMWindow|DOMDocument} node The node to get the window for.
+ * @return {DOMWindow}
+ */
+function getWindowFor(node) {
+ if (Node.isInstance(node)) {
+ if (node.nodeType === node.DOCUMENT_NODE) {
+ return node.defaultView;
+ }
+ return node.ownerDocument.defaultView;
+ } else if (node instanceof Ci.nsIDOMWindow) {
+ return node;
+ }
+ return null;
+}
+
+/**
+ * Synchronously loads a style sheet from `uri` and adds it to the list of
+ * additional style sheets of the document.
+ * The sheets added takes effect immediately, and only on the document of the
+ * `window` given.
+ *
+ * @param {DOMWindow} window
+ * @param {String} url
+ * @param {String} [type="agent"]
+ */
+function loadSheet(window, url, type = "agent") {
+ if (!(type in SHEET_TYPE)) {
+ type = "agent";
+ }
+
+ const windowUtils = utilsFor(window);
+ try {
+ windowUtils.loadSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]);
+ } catch (e) {
+ // The method fails if the url is already loaded.
+ }
+}
+exports.loadSheet = loadSheet;
+
+/**
+ * Remove the document style sheet at `sheetURI` from the list of additional
+ * style sheets of the document. The removal takes effect immediately.
+ *
+ * @param {DOMWindow} window
+ * @param {String} url
+ * @param {String} [type="agent"]
+ */
+function removeSheet(window, url, type = "agent") {
+ if (!(type in SHEET_TYPE)) {
+ type = "agent";
+ }
+
+ const windowUtils = utilsFor(window);
+ try {
+ windowUtils.removeSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]);
+ } catch (e) {
+ // The method fails if the url is already removed.
+ }
+}
+exports.removeSheet = removeSheet;
+
+/**
+ * Get the untransformed coordinates for a node.
+ *
+ * @param {DOMNode} node
+ * The node for which the DOMQuad is to be returned.
+ * @param {String} region
+ * The box model region to return: "content", "padding", "border" or
+ * "margin".
+ * @return {DOMQuad}
+ * A DOMQuad representation of the node.
+ */
+function getUntransformedQuad(node, region = "border") {
+ // Get the inverse transformation matrix for the node.
+ const matrix = node.getTransformToViewport();
+ const inverse = matrix.inverse();
+ const win = node.ownerGlobal;
+
+ // Get the adjusted quads for the node (including scroll offsets).
+ const quads = getAdjustedQuads(win, node, region, {
+ ignoreZoom: true,
+ });
+
+ // Create DOMPoints from the transformed node position.
+ const p1 = new DOMPoint(quads[0].p1.x, quads[0].p1.y);
+ const p2 = new DOMPoint(quads[0].p2.x, quads[0].p2.y);
+ const p3 = new DOMPoint(quads[0].p3.x, quads[0].p3.y);
+ const p4 = new DOMPoint(quads[0].p4.x, quads[0].p4.y);
+
+ // Apply the inverse transformation matrix to the points to get the
+ // untransformed points.
+ const ip1 = inverse.transformPoint(p1);
+ const ip2 = inverse.transformPoint(p2);
+ const ip3 = inverse.transformPoint(p3);
+ const ip4 = inverse.transformPoint(p4);
+
+ // Save the results in a DOMQuad.
+ const quad = new DOMQuad(
+ { x: ip1.x, y: ip1.y },
+ { x: ip2.x, y: ip2.y },
+ { x: ip3.x, y: ip3.y },
+ { x: ip4.x, y: ip4.y }
+ );
+
+ // Remove the border offsets because we include them when calculating
+ // offsets in the while loop.
+ const style = win.getComputedStyle(node);
+ const leftAdjustment = parseInt(style.borderLeftWidth, 10) || 0;
+ const topAdjustment = parseInt(style.borderTopWidth, 10) || 0;
+
+ quad.p1.x -= leftAdjustment;
+ quad.p2.x -= leftAdjustment;
+ quad.p3.x -= leftAdjustment;
+ quad.p4.x -= leftAdjustment;
+ quad.p1.y -= topAdjustment;
+ quad.p2.y -= topAdjustment;
+ quad.p3.y -= topAdjustment;
+ quad.p4.y -= topAdjustment;
+
+ // Calculate offsets.
+ while (node) {
+ const nodeStyle = win.getComputedStyle(node);
+ const borderLeftWidth = parseInt(nodeStyle.borderLeftWidth, 10) || 0;
+ const borderTopWidth = parseInt(nodeStyle.borderTopWidth, 10) || 0;
+ const leftOffset = node.offsetLeft - node.scrollLeft + borderLeftWidth;
+ const topOffset = node.offsetTop - node.scrollTop + borderTopWidth;
+
+ quad.p1.x += leftOffset;
+ quad.p2.x += leftOffset;
+ quad.p3.x += leftOffset;
+ quad.p4.x += leftOffset;
+ quad.p1.y += topOffset;
+ quad.p2.y += topOffset;
+ quad.p3.y += topOffset;
+ quad.p4.y += topOffset;
+
+ node = node.offsetParent;
+ }
+
+ return quad;
+}
+exports.getUntransformedQuad = getUntransformedQuad;
+
+/**
+ * Calculate the total of the node and all of its ancestor's scrollTop and
+ * scrollLeft values.
+ *
+ * @param {DOMNode} node
+ * The node for which the absolute scroll offsets should be calculated.
+ * @return {Object} object
+ * An object containing scrollTop and scrollLeft values.
+ * @return {Number} object.scrollLeft
+ * The total scrollLeft values of the node and all of its ancestors.
+ * @return {Number} object.scrollTop
+ * The total scrollTop values of the node and all of its ancestors.
+ */
+function getAbsoluteScrollOffsetsForNode(node) {
+ const doc = node.ownerDocument;
+
+ // Our walker will only iterate up to document.body so we start by saving the
+ // scroll values for `document.documentElement`.
+ let scrollTop = doc.documentElement.scrollTop;
+ let scrollLeft = doc.documentElement.scrollLeft;
+ const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT);
+ walker.currentNode = node;
+ let currentNode = walker.currentNode;
+
+ // Iterate from `node` up the tree to `document.body` adding scroll offsets
+ // as we go.
+ while (currentNode) {
+ const nodeScrollTop = currentNode.scrollTop;
+ const nodeScrollLeft = currentNode.scrollLeft;
+
+ if (nodeScrollTop || nodeScrollLeft) {
+ scrollTop += nodeScrollTop;
+ scrollLeft += nodeScrollLeft;
+ }
+
+ currentNode = walker.parentNode();
+ }
+
+ return {
+ scrollLeft,
+ scrollTop,
+ };
+}
+exports.getAbsoluteScrollOffsetsForNode = getAbsoluteScrollOffsetsForNode;
+
+/**
+ * Check if the provided node is a <frame> or <iframe> element.
+ *
+ * @param {DOMNode} node
+ * @returns {Boolean}
+ */
+function isFrame(node) {
+ const className = ChromeUtils.getClassName(node);
+ return className == "HTMLIFrameElement" || className == "HTMLFrameElement";
+}
+
+/**
+ * Check if the provided node is representing a remote <browser> element.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isRemoteBrowserElement(node) {
+ return (
+ ChromeUtils.getClassName(node) == "XULFrameElement" &&
+ !node.childNodes.length &&
+ node.getAttribute("remote") == "true"
+ );
+}
+exports.isRemoteBrowserElement = isRemoteBrowserElement;
+
+/**
+ * Check if the provided node is representing a remote frame.
+ *
+ * - In the context of the browser toolbox, a remote frame can be the <browser remote>
+ * element found inside each tab.
+ * - In the context of the content toolbox, a remote frame can be a <iframe> that contains
+ * a different origin document.
+ *
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+function isRemoteFrame(node) {
+ if (isFrame(node)) {
+ return node.frameLoader?.isRemoteFrame;
+ }
+
+ if (isRemoteBrowserElement(node)) {
+ return true;
+ }
+
+ return false;
+}
+exports.isRemoteFrame = isRemoteFrame;
+
+/**
+ * Check if the provided node is representing a frame that has its own dedicated child target.
+ *
+ * @param {BrowsingContextTargetActor} targetActor
+ * @param {DOMNode} node
+ * @returns {Boolean}
+ */
+function isFrameWithChildTarget(targetActor, node) {
+ // If the iframe is blocked because of CSP, it won't have a document (and no associated targets)
+ if (isFrameBlockedByCSP(node)) {
+ return false;
+ }
+
+ return isRemoteFrame(node) || (isFrame(node) && targetActor.ignoreSubFrames);
+}
+
+exports.isFrameWithChildTarget = isFrameWithChildTarget;
+
+/**
+ * Check if the provided node is representing a frame that is blocked by CSP.
+ *
+ * @param {DOMNode} node
+ * @returns {Boolean}
+ */
+function isFrameBlockedByCSP(node) {
+ if (!isFrame(node)) {
+ return false;
+ }
+
+ if (!node.src) {
+ return false;
+ }
+
+ let uri;
+ try {
+ uri = lazy.NetUtil.newURI(node.src);
+ } catch (e) {
+ return false;
+ }
+
+ const res = node.ownerDocument.csp.shouldLoad(
+ Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+ null, // nsICSPEventListener
+ null, // nsILoadInfo
+ uri,
+ null, // aOriginalURIIfRedirect
+ false // aSendViolationReports
+ );
+
+ return res !== Ci.nsIContentPolicy.ACCEPT;
+}
+
+exports.isFrameBlockedByCSP = isFrameBlockedByCSP;
diff --git a/devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs b/devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs
new file mode 100644
index 0000000000..06c33b8891
--- /dev/null
+++ b/devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs
@@ -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/. */
+
+const { DevToolsLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs",
+ {
+ // `loadInDevToolsLoader` will import the loader in a special priviledged
+ // global created for DevTools, which will be reused as the shared global
+ // to load additional modules for the "DistinctSystemPrincipalLoader".
+ loadInDevToolsLoader: true,
+ }
+);
+
+// When debugging system principal resources (JSMs, chrome documents, ...)
+// We have to load DevTools actors in another system principal global.
+// That's mostly because of spidermonkey's Debugger API which requires
+// debuggee and debugger to be in distinct principals.
+//
+// We try to hold a single instance of this special loader via this API.
+//
+// @param requester object
+// Object/instance which is using the loader.
+// The same requester object should be passed to release method.
+let systemLoader = null;
+const systemLoaderRequesters = new Set();
+
+export function useDistinctSystemPrincipalLoader(requester) {
+ if (!systemLoader) {
+ systemLoader = new DevToolsLoader({
+ useDevToolsLoaderGlobal: true,
+ });
+ systemLoaderRequesters.clear();
+ }
+ systemLoaderRequesters.add(requester);
+ return systemLoader;
+}
+
+export function releaseDistinctSystemPrincipalLoader(requester) {
+ systemLoaderRequesters.delete(requester);
+ if (systemLoaderRequesters.size == 0) {
+ systemLoader.destroy();
+ systemLoader = null;
+ }
+}
diff --git a/devtools/shared/loader/Loader.sys.mjs b/devtools/shared/loader/Loader.sys.mjs
new file mode 100644
index 0000000000..e34810dde8
--- /dev/null
+++ b/devtools/shared/loader/Loader.sys.mjs
@@ -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/. */
+
+/**
+ * Manages the base loader (base-loader.sys.mjs) instance used to load the developer tools.
+ */
+
+import {
+ Loader,
+ Require,
+ resolveURI,
+ unload,
+} from "resource://devtools/shared/loader/base-loader.sys.mjs";
+import { requireRawId } from "resource://devtools/shared/loader/loader-plugin-raw.sys.mjs";
+
+export const DEFAULT_SANDBOX_NAME = "DevTools (Module loader)";
+
+var gNextLoaderID = 0;
+
+/**
+ * The main devtools API. The standard instance of this loader is exported as
+ * |loader| below, but if a fresh copy of the loader is needed, then a new
+ * one can also be created.
+ *
+ * The two following boolean flags are used to control the sandboxes into
+ * which the modules are loaded.
+ * @param invisibleToDebugger boolean
+ * If true, the modules won't be visible by the Debugger API.
+ * This typically allows to hide server modules from the debugger panel.
+ * @param freshCompartment boolean
+ * If true, the modules will be forced to be loaded in a distinct
+ * compartment. It is typically used to load the modules in a distinct
+ * system compartment, different from the main one, which is shared by
+ * all JSMs, XPCOMs and modules loaded with this flag set to true.
+ * We use this in order to debug modules loaded in this shared system
+ * compartment. The debugger actor has to be running in a distinct
+ * compartment than the context it is debugging.
+ * @param useDevToolsLoaderGlobal boolean
+ * If true, the loader will reuse the current global to load other
+ * modules instead of creating a sandbox with custom options. Cannot be
+ * used with invisibleToDebugger and/or freshCompartment.
+ * TODO: This should ultimately replace invisibleToDebugger.
+ */
+export function DevToolsLoader({
+ invisibleToDebugger = false,
+ freshCompartment = false,
+ useDevToolsLoaderGlobal = false,
+} = {}) {
+ if (useDevToolsLoaderGlobal && (invisibleToDebugger || freshCompartment)) {
+ throw new Error(
+ "Loader cannot use invisibleToDebugger or freshCompartment if useDevToolsLoaderGlobal is true"
+ );
+ }
+
+ const paths = {
+ // This resource:// URI is only registered when running DAMP tests.
+ // This is done by: testing/talos/talos/tests/devtools/addon/api.js
+ "damp-test": "resource://damp-test/content",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ devtools: "resource://devtools",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Allow access to xpcshell test items from the loader.
+ "xpcshell-test": "resource://test",
+
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Allow access to locale data using paths closer to what is
+ // used in the source tree.
+ "devtools/client/locales": "chrome://devtools/locale",
+ "devtools/shared/locales": "chrome://devtools-shared/locale",
+ "devtools/startup/locales": "chrome://devtools-startup/locale",
+ "toolkit/locales": "chrome://global/locale",
+ };
+
+ const sharedGlobal = useDevToolsLoaderGlobal
+ ? Cu.getGlobalForObject({})
+ : undefined;
+ this.loader = new Loader({
+ paths,
+ sharedGlobal,
+ invisibleToDebugger,
+ freshCompartment,
+ sandboxName: useDevToolsLoaderGlobal
+ ? "DevTools (Server Module Loader)"
+ : DEFAULT_SANDBOX_NAME,
+ // Make sure `define` function exists. JSON Viewer needs modules in AMD
+ // format, as it currently uses RequireJS from a content document and
+ // can't access our usual loaders. So, any modules shared with the JSON
+ // Viewer should include a define wrapper:
+ //
+ // // Make this available to both AMD and CJS environments
+ // define(function(require, exports, module) {
+ // ... code ...
+ // });
+ //
+ // Bug 1248830 will work out a better plan here for our content module
+ // loading needs, especially as we head towards devtools.html.
+ supportAMDModules: true,
+ requireHook: (id, require) => {
+ if (id.startsWith("raw!") || id.startsWith("theme-loader!")) {
+ return requireRawId(id, require);
+ }
+ return require(id);
+ },
+ });
+
+ this.require = Require(this.loader, { id: "devtools" });
+
+ // Various globals are available from ESM, but not from sandboxes,
+ // inject them into the globals list.
+ // Changes here should be mirrored to devtools/.eslintrc.
+ const injectedGlobals = {
+ CanonicalBrowsingContext,
+ console,
+ BrowsingContext,
+ ChromeWorker,
+ DebuggerNotificationObserver,
+ DOMPoint,
+ DOMQuad,
+ DOMRect,
+ fetch,
+ HeapSnapshot,
+ IOUtils,
+ L10nRegistry,
+ Localization,
+ NamedNodeMap,
+ NodeFilter,
+ PathUtils,
+ Services,
+ StructuredCloneHolder,
+ TelemetryStopwatch,
+ WebExtensionPolicy,
+ WindowGlobalParent,
+ WindowGlobalChild,
+ };
+ for (const name in injectedGlobals) {
+ this.loader.globals[name] = injectedGlobals[name];
+ }
+
+ // Fetch custom pseudo modules and globals
+ const { modules, globals } = this.require(
+ "resource://devtools/shared/loader/builtin-modules.js"
+ );
+
+ // Register custom pseudo modules to the current loader instance
+ for (const id in modules) {
+ const uri = resolveURI(id, this.loader.mapping);
+ this.loader.modules[uri] = {
+ get exports() {
+ return modules[id];
+ },
+ };
+ }
+
+ // Register custom globals to the current loader instance
+ Object.defineProperties(
+ this.loader.sharedGlobal,
+ Object.getOwnPropertyDescriptors(globals)
+ );
+
+ // Define the loader id for these two usecases:
+ // * access via the JSM (this.id)
+ // let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ // loader.id
+ this.id = gNextLoaderID++;
+ // * access via module's `loader` global
+ // loader.id
+ globals.loader.id = this.id;
+ globals.loader.invisibleToDebugger = invisibleToDebugger;
+
+ // Expose lazy helpers on `loader`
+ // ie. when you use it like that from a JSM:
+ // let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ // loader.lazyGetter(...);
+ this.lazyGetter = globals.loader.lazyGetter;
+ this.lazyServiceGetter = globals.loader.lazyServiceGetter;
+ this.lazyRequireGetter = globals.loader.lazyRequireGetter;
+}
+
+DevToolsLoader.prototype = {
+ destroy(reason = "shutdown") {
+ unload(this.loader, reason);
+ delete this.loader;
+ },
+
+ /**
+ * Return true if |id| refers to something requiring help from a
+ * loader plugin.
+ */
+ isLoaderPluginId(id) {
+ return id.startsWith("raw!");
+ },
+};
+
+// Export the standard instance of DevToolsLoader used by the tools.
+export var loader = new DevToolsLoader({
+ /**
+ * Sets whether the compartments loaded by this instance should be invisible
+ * to the debugger. Invisibility is needed for loaders that support debugging
+ * of chrome code. This is true of remote target environments, like Fennec or
+ * B2G. It is not the default case for desktop Firefox because we offer the
+ * Browser Toolbox for chrome debugging there, which uses its own, separate
+ * loader instance.
+ * @see devtools/client/framework/browser-toolbox/Launcher.sys.mjs
+ */
+ invisibleToDebugger: Services.appinfo.name !== "Firefox",
+});
+
+export var require = loader.require;
diff --git a/devtools/shared/loader/base-loader.sys.mjs b/devtools/shared/loader/base-loader.sys.mjs
new file mode 100644
index 0000000000..b9d625f3e3
--- /dev/null
+++ b/devtools/shared/loader/base-loader.sys.mjs
@@ -0,0 +1,638 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported Loader, resolveURI, Module, Require, unload */
+
+const systemPrincipal = Components.Constructor(
+ "@mozilla.org/systemprincipal;1",
+ "nsIPrincipal"
+)();
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsIResProtocolHandler"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+// Define some shortcuts.
+function* getOwnIdentifiers(x) {
+ yield* Object.getOwnPropertyNames(x);
+ yield* Object.getOwnPropertySymbols(x);
+}
+
+function isJSONURI(uri) {
+ return uri.endsWith(".json");
+}
+function isJSMURI(uri) {
+ return uri.endsWith(".jsm");
+}
+function isSYSMJSURI(uri) {
+ return uri.endsWith(".sys.mjs");
+}
+function isJSURI(uri) {
+ return uri.endsWith(".js");
+}
+const AbsoluteRegExp = /^(resource|chrome|file|jar):/;
+function isAbsoluteURI(uri) {
+ return AbsoluteRegExp.test(uri);
+}
+function isRelative(id) {
+ return id.startsWith(".");
+}
+
+function readURI(uri) {
+ const nsURI = lazy.NetUtil.newURI(uri);
+ if (nsURI.scheme == "resource") {
+ // Resolve to a real URI, this will catch any obvious bad paths without
+ // logging assertions in debug builds, see bug 1135219
+ uri = lazy.resProto.resolveURI(nsURI);
+ }
+
+ const stream = lazy.NetUtil.newChannel({
+ uri: lazy.NetUtil.newURI(uri, "UTF-8"),
+ loadUsingSystemPrincipal: true,
+ }).open();
+ const count = stream.available();
+ const data = lazy.NetUtil.readInputStreamToString(stream, count, {
+ charset: "UTF-8",
+ });
+
+ stream.close();
+
+ return data;
+}
+
+// Combines all arguments into a resolved, normalized path
+function join(base, ...paths) {
+ // If this is an absolute URL, we need to normalize only the path portion,
+ // or we wind up stripping too many slashes and producing invalid URLs.
+ const match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec(
+ base
+ );
+ if (match) {
+ return match[1] + normalize([match[2], ...paths].join("/"));
+ }
+
+ return normalize([base, ...paths].join("/"));
+}
+
+// Function takes set of options and returns a JS sandbox. Function may be
+// passed set of options:
+// - `name`: A string value which identifies the sandbox in about:memory. Will
+// throw exception if omitted.
+// - `prototype`: Ancestor for the sandbox that will be created. Defaults to
+// `{}`.
+// - `invisibleToDebugger`: True, if the sandbox is part of the debugger
+// implementation and should not be tracked by debugger API.
+// For more details see:
+// @see https://searchfox.org/mozilla-central/rev/0948667bc62415d48abff27e1405fb4ab4d65d75/js/xpconnect/idl/xpccomponents.idl#127-245
+function Sandbox(options) {
+ // Normalize options and rename to match `Cu.Sandbox` expectations.
+ const sandboxOptions = {
+ // This will allow exposing Components as well as Cu, Ci and Cr.
+ wantComponents: true,
+
+ // By default, Sandbox come with a very limited set of global.
+ // The list of all available symbol names is available over there:
+ // https://searchfox.org/mozilla-central/rev/31368c7795f44b7a15531d6c5e52dc97f82cf2d5/js/xpconnect/src/Sandbox.cpp#905-997
+ // Request to expose all meaningful global here:
+ wantGlobalProperties: [
+ "AbortController",
+ "atob",
+ "btoa",
+ "Blob",
+ "crypto",
+ "ChromeUtils",
+ "CSS",
+ "CSSRule",
+ "DOMParser",
+ "Element",
+ "Event",
+ "FileReader",
+ "FormData",
+ "Headers",
+ "InspectorUtils",
+ "MIDIInputMap",
+ "MIDIOutputMap",
+ "Node",
+ "TextDecoder",
+ "TextEncoder",
+ "URL",
+ "URLSearchParams",
+ "Window",
+ "XMLHttpRequest",
+ ],
+
+ sandboxName: options.name,
+ sandboxPrototype: "prototype" in options ? options.prototype : {},
+ invisibleToDebugger:
+ "invisibleToDebugger" in options ? options.invisibleToDebugger : false,
+ freshCompartment: options.freshCompartment || false,
+ };
+
+ return Cu.Sandbox(systemPrincipal, sandboxOptions);
+}
+
+// This allows defining some modules in AMD format while retaining CommonJS
+// compatibility with this loader by allowing the factory function to have
+// access to general CommonJS functions, e.g.
+//
+// define(function(require, exports, module) {
+// ... code ...
+// });
+function define(factory) {
+ factory(this.require, this.exports, this.module);
+}
+
+// Populates `exports` of the given CommonJS `module` object, in the context
+// of the given `loader` by evaluating code associated with it.
+function load(loader, module) {
+ const require = Require(loader, module);
+
+ // We expose set of properties defined by `CommonJS` specification via
+ // prototype of the sandbox. Also globals are deeper in the prototype
+ // chain so that each module has access to them as well.
+ const properties = {
+ require,
+ module,
+ exports: module.exports,
+ };
+ if (loader.supportAMDModules) {
+ properties.define = define;
+ }
+
+ // Create a new object in the shared global of the loader, that will be used
+ // as the scope object for this particular module.
+ const scopeFromSharedGlobal = new loader.sharedGlobal.Object();
+ Object.assign(scopeFromSharedGlobal, properties);
+
+ const originalExports = module.exports;
+ try {
+ Services.scriptloader.loadSubScript(module.uri, scopeFromSharedGlobal);
+ } catch (error) {
+ // loadSubScript sometime throws string errors, which includes no stack.
+ // At least provide the current stack by re-throwing a real Error object.
+ if (typeof error == "string") {
+ if (
+ error.startsWith("Error creating URI") ||
+ error.startsWith("Error opening input stream (invalid filename?)")
+ ) {
+ throw new Error(
+ `Module \`${module.id}\` is not found at ${module.uri}`
+ );
+ }
+ throw new Error(
+ `Error while loading module \`${module.id}\` at ${module.uri}:` +
+ "\n" +
+ error
+ );
+ }
+ // Otherwise just re-throw everything else which should have a stack
+ throw error;
+ }
+
+ // Only freeze the exports object if we created it ourselves. Modules
+ // which completely replace the exports object and still want it
+ // frozen need to freeze it themselves.
+ if (module.exports === originalExports) {
+ Object.freeze(module.exports);
+ }
+
+ return module;
+}
+
+// Utility function to normalize module `uri`s so they have `.js` extension.
+function normalizeExt(uri) {
+ if (isJSURI(uri) || isJSONURI(uri) || isJSMURI(uri) || isSYSMJSURI(uri)) {
+ return uri;
+ }
+ return uri + ".js";
+}
+
+// Utility function to join paths. In common case `base` is a
+// `requirer.uri` but in some cases it may be `baseURI`. In order to
+// avoid complexity we require `baseURI` with a trailing `/`.
+function resolve(id, base) {
+ if (!isRelative(id)) {
+ return id;
+ }
+
+ const baseDir = dirname(base);
+
+ let resolved;
+ if (baseDir.includes(":")) {
+ resolved = join(baseDir, id);
+ } else {
+ resolved = normalize(`${baseDir}/${id}`);
+ }
+
+ // Joining and normalizing removes the "./" from relative files.
+ // We need to ensure the resolution still has the root
+ if (base.startsWith("./")) {
+ resolved = "./" + resolved;
+ }
+
+ return resolved;
+}
+
+function compileMapping(paths) {
+ // Make mapping array that is sorted from longest path to shortest path.
+ const mapping = Object.keys(paths)
+ .sort((a, b) => b.length - a.length)
+ .map(path => [path, paths[path]]);
+
+ const PATTERN = /([.\\?+*(){}[\]^$])/g;
+ const escapeMeta = str => str.replace(PATTERN, "\\$1");
+
+ const patterns = [];
+ paths = {};
+
+ for (let [path, uri] of mapping) {
+ // Strip off any trailing slashes to make comparisons simpler
+ if (path.endsWith("/")) {
+ path = path.slice(0, -1);
+ uri = uri.replace(/\/+$/, "");
+ }
+
+ paths[path] = uri;
+
+ // We only want to match path segments explicitly. Examples:
+ // * "foo/bar" matches for "foo/bar"
+ // * "foo/bar" matches for "foo/bar/baz"
+ // * "foo/bar" does not match for "foo/bar-1"
+ // * "foo/bar/" does not match for "foo/bar"
+ // * "foo/bar/" matches for "foo/bar/baz"
+ //
+ // Check for an empty path, an exact match, or a substring match
+ // with the next character being a forward slash.
+ if (path == "") {
+ patterns.push("");
+ } else {
+ patterns.push(`${escapeMeta(path)}(?=$|/)`);
+ }
+ }
+
+ const pattern = new RegExp(`^(${patterns.join("|")})`);
+
+ // This will replace the longest matching path mapping at the start of
+ // the ID string with its mapped value.
+ return id => {
+ return id.replace(pattern, (m0, m1) => paths[m1]);
+ };
+}
+
+export function resolveURI(id, mapping) {
+ // Do not resolve if already a resource URI
+ if (isAbsoluteURI(id)) {
+ return normalizeExt(id);
+ }
+
+ return normalizeExt(mapping(id));
+}
+
+// Creates version of `require` that will be exposed to the given `module`
+// in the context of the given `loader`. Each module gets own limited copy
+// of `require` that is allowed to load only a modules that are associated
+// with it during link time.
+export function Require(loader, requirer) {
+ const { modules, mapping, mappingCache, requireHook } = loader;
+
+ function require(id) {
+ if (!id) {
+ // Throw if `id` is not passed.
+ throw Error(
+ "You must provide a module name when calling require() from " +
+ requirer.id,
+ requirer.uri
+ );
+ }
+
+ if (requireHook) {
+ return requireHook(id, _require);
+ }
+
+ return _require(id);
+ }
+
+ function _require(id) {
+ let { uri, requirement } = getRequirements(id);
+
+ let module = null;
+ // If module is already cached by loader then just use it.
+ if (uri in modules) {
+ module = modules[uri];
+ } else if (isJSMURI(uri)) {
+ module = modules[uri] = Module(requirement, uri);
+ module.exports = ChromeUtils.import(uri);
+ } else if (isSYSMJSURI(uri)) {
+ module = modules[uri] = Module(requirement, uri);
+ module.exports = ChromeUtils.importESModule(uri);
+ } else if (isJSONURI(uri)) {
+ let data;
+
+ // First attempt to load and parse json uri
+ // ex: `test.json`
+ // If that doesn"t exist, check for `test.json.js`
+ // for node parity
+ try {
+ data = JSON.parse(readURI(uri));
+ module = modules[uri] = Module(requirement, uri);
+ module.exports = data;
+ } catch (err) {
+ // If error thrown from JSON parsing, throw that, do not
+ // attempt to find .json.js file
+ if (err && /JSON\.parse/.test(err.message)) {
+ throw err;
+ }
+ uri = uri + ".js";
+ }
+ }
+
+ // If not yet cached, load and cache it.
+ // We also freeze module to prevent it from further changes
+ // at runtime.
+ if (!(uri in modules)) {
+ // Many of the loader's functionalities are dependent
+ // on modules[uri] being set before loading, so we set it and
+ // remove it if we have any errors.
+ module = modules[uri] = Module(requirement, uri);
+ try {
+ Object.freeze(load(loader, module));
+ } catch (e) {
+ // Clear out modules cache so we can throw on a second invalid require
+ delete modules[uri];
+ throw e;
+ }
+ }
+
+ return module.exports;
+ }
+
+ // Resolution function taking a module name/path and
+ // returning a resourceURI and a `requirement` used by the loader.
+ // Used by both `require` and `require.resolve`.
+ function getRequirements(id) {
+ if (!id) {
+ // Throw if `id` is not passed.
+ throw Error(
+ "you must provide a module name when calling require() from " +
+ requirer.id,
+ requirer.uri
+ );
+ }
+
+ let requirement, uri;
+
+ if (modules[id]) {
+ uri = requirement = id;
+ } else if (requirer) {
+ // Resolve `id` to its requirer if it's relative.
+ requirement = resolve(id, requirer.id);
+ } else {
+ requirement = id;
+ }
+
+ // Resolves `uri` of module using loaders resolve function.
+ if (!uri) {
+ if (mappingCache.has(requirement)) {
+ uri = mappingCache.get(requirement);
+ } else {
+ uri = resolveURI(requirement, mapping);
+ mappingCache.set(requirement, uri);
+ }
+ }
+
+ // Throw if `uri` can not be resolved.
+ if (!uri) {
+ throw Error(
+ "Module: Can not resolve '" +
+ id +
+ "' module required by " +
+ requirer.id +
+ " located at " +
+ requirer.uri,
+ requirer.uri
+ );
+ }
+
+ return { uri, requirement };
+ }
+
+ // Expose the `resolve` function for this `Require` instance
+ require.resolve = _require.resolve = function (id) {
+ const { uri } = getRequirements(id);
+ return uri;
+ };
+
+ // This is like webpack's require.context. It returns a new require
+ // function that prepends the prefix to any requests.
+ require.context = prefix => {
+ return id => {
+ return require(prefix + id);
+ };
+ };
+
+ return require;
+}
+
+// Makes module object that is made available to CommonJS modules when they
+// are evaluated, along with `exports` and `require`.
+export function Module(id, uri) {
+ return Object.create(null, {
+ id: { enumerable: true, value: id },
+ exports: {
+ enumerable: true,
+ writable: true,
+ value: Object.create(null),
+ configurable: true,
+ },
+ uri: { value: uri },
+ });
+}
+
+// Takes `loader`, and unload `reason` string and notifies all observers that
+// they should cleanup after them-self.
+export function unload(loader, reason) {
+ // subject is a unique object created per loader instance.
+ // This allows any code to cleanup on loader unload regardless of how
+ // it was loaded. To handle unload for specific loader subject may be
+ // asserted against loader.destructor or require("@loader/unload")
+ // Note: We don not destroy loader's module cache or sandboxes map as
+ // some modules may do cleanup in subsequent turns of event loop. Destroying
+ // cache may cause module identity problems in such cases.
+ const subject = { wrappedJSObject: loader.destructor };
+ Services.obs.notifyObservers(subject, "devtools:loader:destroy", reason);
+}
+
+// Function makes new loader that can be used to load CommonJS modules.
+// Loader takes following options:
+// - `paths`: Mandatory dictionary of require path mapped to absolute URIs.
+// Object keys are path prefix used in require(), values are URIs where each
+// prefix should be mapped to.
+// - `globals`: Optional map of globals, that all module scopes will inherit
+// from. Map is also exposed under `globals` property of the returned loader
+// so it can be extended further later. Defaults to `{}`.
+// - `sandboxName`: String, name of the sandbox displayed in about:memory.
+// - `invisibleToDebugger`: Boolean. Should be true when loading debugger
+// modules, in order to ignore them from the Debugger API.
+// - `sandboxPrototype`: Object used to define globals on all module's
+// sandboxes.
+// - `requireHook`: Optional function used to replace native require function
+// from loader. This function receive the module path as first argument,
+// and native require method as second argument.
+export function Loader(options) {
+ let { paths, globals } = options;
+ if (!globals) {
+ globals = {};
+ }
+
+ // We create an identity object that will be dispatched on an unload
+ // event as subject. This way unload listeners will be able to assert
+ // which loader is unloaded. Please note that we intentionally don"t
+ // use `loader` as subject to prevent a loader access leakage through
+ // observer notifications.
+ const destructor = Object.create(null);
+
+ const mapping = compileMapping(paths);
+
+ // Define pseudo modules.
+ const builtinModuleExports = {
+ "@loader/unload": destructor,
+ "@loader/options": options,
+ };
+
+ const modules = {};
+ for (const id of Object.keys(builtinModuleExports)) {
+ // We resolve `uri` from `id` since modules are cached by `uri`.
+ const uri = resolveURI(id, mapping);
+ const module = Module(id, uri);
+
+ // Lazily expose built-in modules in order to
+ // allow them to be loaded lazily.
+ Object.defineProperty(module, "exports", {
+ enumerable: true,
+ get() {
+ return builtinModuleExports[id];
+ },
+ });
+
+ modules[uri] = module;
+ }
+
+ let sharedGlobal;
+ if (options.sharedGlobal) {
+ sharedGlobal = options.sharedGlobal;
+ } else {
+ // Create the unique sandbox we will be using for all modules,
+ // so that we prevent creating a new compartment per module.
+ // The side effect is that all modules will share the same
+ // global objects.
+ sharedGlobal = Sandbox({
+ name: options.sandboxName || "DevTools",
+ invisibleToDebugger: options.invisibleToDebugger || false,
+ prototype: options.sandboxPrototype || globals,
+ freshCompartment: options.freshCompartment,
+ });
+ }
+
+ if (options.sharedGlobal || options.sandboxPrototype) {
+ // If we were given a sharedGlobal or a sandboxPrototype, we have to define
+ // the globals on the shared global directly. Note that this will not work
+ // for callers who depend on being able to add globals after the loader was
+ // created.
+ for (const name of getOwnIdentifiers(globals)) {
+ Object.defineProperty(
+ sharedGlobal,
+ name,
+ Object.getOwnPropertyDescriptor(globals, name)
+ );
+ }
+ }
+
+ // Loader object is just a representation of a environment
+ // state. We mark its properties non-enumerable
+ // as they are pure implementation detail that no one should rely upon.
+ const returnObj = {
+ destructor: { enumerable: false, value: destructor },
+ globals: { enumerable: false, value: globals },
+ mapping: { enumerable: false, value: mapping },
+ mappingCache: { enumerable: false, value: new Map() },
+ // Map of module objects indexed by module URIs.
+ modules: { enumerable: false, value: modules },
+ sharedGlobal: { enumerable: false, value: sharedGlobal },
+ supportAMDModules: {
+ enumerable: false,
+ value: options.supportAMDModules || false,
+ },
+ // Whether the modules loaded should be ignored by the debugger
+ invisibleToDebugger: {
+ enumerable: false,
+ value: options.invisibleToDebugger || false,
+ },
+ requireHook: {
+ enumerable: false,
+ writable: true,
+ value: options.requireHook,
+ },
+ };
+
+ return Object.create(null, returnObj);
+}
+
+// NB: These methods are from the UNIX implementation of OS.Path. Refactoring
+// this module to not use path methods on stringly-typed URIs is
+// non-trivial.
+function dirname(path) {
+ let index = path.lastIndexOf("/");
+ if (index == -1) {
+ return ".";
+ }
+ while (index >= 0 && path[index] == "/") {
+ --index;
+ }
+ return path.slice(0, index + 1);
+}
+
+function normalize(path) {
+ const stack = [];
+ let absolute;
+ if (path.length >= 0 && path[0] == "/") {
+ absolute = true;
+ } else {
+ absolute = false;
+ }
+ path.split("/").forEach(function (v) {
+ switch (v) {
+ case "":
+ case ".": // fallthrough
+ break;
+ case "..":
+ if (!stack.length) {
+ if (absolute) {
+ throw new Error("Path is ill-formed: attempting to go past root");
+ } else {
+ stack.push("..");
+ }
+ } else if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ break;
+ default:
+ stack.push(v);
+ }
+ });
+ const string = stack.join("/");
+ return absolute ? "/" + string : string;
+}
diff --git a/devtools/shared/loader/browser-loader-mocks.js b/devtools/shared/loader/browser-loader-mocks.js
new file mode 100644
index 0000000000..c8fe528ee5
--- /dev/null
+++ b/devtools/shared/loader/browser-loader-mocks.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/. */
+
+"use strict";
+
+// Map of mocked modules, keys are absolute URIs for devtools modules such as
+// "resource://devtools/path/to/mod.js, values are objects (anything passed to
+// setMockedModule technically).
+const _mocks = {};
+
+/**
+ * Retrieve a mocked module matching the provided uri, eg "resource://path/to/file.js".
+ */
+function getMockedModule(uri) {
+ return _mocks[uri];
+}
+exports.getMockedModule = getMockedModule;
+
+/**
+ * Module paths are transparently provided with or without ".js" when using the loader,
+ * normalize the user-provided module paths to always have modules ending with ".js".
+ */
+function _getUriForModulePath(modulePath) {
+ // Assume js modules and add the .js extension if missing.
+ if (!modulePath.endsWith(".js")) {
+ modulePath = modulePath + ".js";
+ }
+
+ // Add resource:// scheme if no scheme is specified.
+ if (!modulePath.includes("://")) {
+ modulePath = "resource://" + modulePath;
+ }
+
+ return modulePath;
+}
+
+/**
+ * Assign a mock object to the provided module path.
+ * @param mock
+ * Plain JavaScript object that will implement the expected API for the mocked
+ * module.
+ * @param modulePath
+ * The module path should be the absolute module path, starting with `devtools`:
+ * "devtools/client/some-panel/some-module"
+ */
+function setMockedModule(mock, modulePath) {
+ const uri = _getUriForModulePath(modulePath);
+ _mocks[uri] = new Proxy(mock, {
+ get(target, key) {
+ if (typeof target[key] === "function") {
+ // Functions are wrapped to be able to update the methods during the test, even if
+ // the methods were imported with destructuring. For instance:
+ // `const { someMethod } = require("devtools/client/shared/my-module");`
+ return function () {
+ return target[key].apply(target, arguments);
+ };
+ }
+ return target[key];
+ },
+ });
+}
+exports.setMockedModule = setMockedModule;
+
+/**
+ * Remove any mock object defined for the provided absolute module path.
+ */
+function removeMockedModule(modulePath) {
+ const uri = _getUriForModulePath(modulePath);
+ delete _mocks[uri];
+}
+exports.removeMockedModule = removeMockedModule;
diff --git a/devtools/shared/loader/browser-loader.js b/devtools/shared/loader/browser-loader.js
new file mode 100644
index 0000000000..f42b009e17
--- /dev/null
+++ b/devtools/shared/loader/browser-loader.js
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 BaseLoader = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/base-loader.sys.mjs"
+);
+const { require: devtoolsRequire, loader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const flags = devtoolsRequire("devtools/shared/flags");
+const { joinURI } = devtoolsRequire("devtools/shared/path");
+const { assert } = devtoolsRequire("devtools/shared/DevToolsUtils");
+
+const lazy = {};
+
+loader.lazyRequireGetter(
+ lazy,
+ "getMockedModule",
+ "resource://devtools/shared/loader/browser-loader-mocks.js",
+ {}
+);
+
+const BROWSER_BASED_DIRS = [
+ "resource://devtools/client/inspector/boxmodel",
+ "resource://devtools/client/inspector/changes",
+ "resource://devtools/client/inspector/computed",
+ "resource://devtools/client/inspector/events",
+ "resource://devtools/client/inspector/flexbox",
+ "resource://devtools/client/inspector/fonts",
+ "resource://devtools/client/inspector/grids",
+ "resource://devtools/client/inspector/layout",
+ "resource://devtools/client/inspector/markup",
+ "resource://devtools/client/jsonview",
+ "resource://devtools/client/netmonitor/src/utils",
+ "resource://devtools/client/shared/fluent-l10n",
+ "resource://devtools/client/shared/redux",
+ "resource://devtools/client/shared/vendor",
+];
+
+const COMMON_LIBRARY_DIRS = ["resource://devtools/client/shared/vendor"];
+
+// Any directory that matches the following regular expression
+// is also considered as browser based module directory.
+// ('resource://devtools/client/.*/components/')
+//
+// An example:
+// * `resource://devtools/client/inspector/components`
+// * `resource://devtools/client/inspector/shared/components`
+const browserBasedDirsRegExp =
+ /^resource\:\/\/devtools\/client\/\S*\/components\//;
+
+/*
+ * Create a loader to be used in a browser environment. This evaluates
+ * modules in their own environment, but sets window (the normal
+ * global object) as the sandbox prototype, so when a variable is not
+ * defined it checks `window` before throwing an error. This makes all
+ * browser APIs available to modules by default, like a normal browser
+ * environment, but modules are still evaluated in their own scope.
+ *
+ * Another very important feature of this loader is that it *only*
+ * deals with modules loaded from under `baseURI`. Anything loaded
+ * outside of that path will still be loaded from the devtools loader,
+ * so all system modules are still shared and cached across instances.
+ * An exception to this is anything under
+ * `devtools/client/shared/{vendor/components}`, which is where shared libraries
+ * and React components live that should be evaluated in a browser environment.
+ *
+ * @param string baseURI
+ * Base path to load modules from. If null or undefined, only
+ * the shared vendor/components modules are loaded with the browser
+ * loader.
+ * @param Object window
+ * The window instance to evaluate modules within
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ * @return Object
+ * An object with two properties:
+ * - loader: the Loader instance
+ * - require: a function to require modules with
+ */
+function BrowserLoader(options) {
+ const browserLoaderBuilder = new BrowserLoaderBuilder(options);
+ return {
+ loader: browserLoaderBuilder.loader,
+ require: browserLoaderBuilder.require,
+ };
+}
+
+/**
+ * Private class used to build the Loader instance and require method returned
+ * by BrowserLoader(baseURI, window).
+ *
+ * @param string baseURI
+ * Base path to load modules from.
+ * @param Function commonLibRequire
+ * Require function that should be used to load common libraries, like React.
+ * Allows for sharing common modules between tools, instead of loading a new
+ * instance into each tool. For example, pass "toolbox.browserRequire" here.
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ * @param Object window
+ * The window instance to evaluate modules within
+ */
+function BrowserLoaderBuilder({
+ baseURI,
+ commonLibRequire,
+ useOnlyShared,
+ window,
+}) {
+ assert(
+ !!baseURI !== !!useOnlyShared,
+ "Cannot use both `baseURI` and `useOnlyShared`."
+ );
+
+ const loaderOptions = devtoolsRequire("@loader/options");
+
+ const opts = {
+ sandboxPrototype: window,
+ sandboxName: "DevTools (UI loader)",
+ paths: loaderOptions.paths,
+ invisibleToDebugger: loaderOptions.invisibleToDebugger,
+ // Make sure `define` function exists. This allows defining some modules
+ // in AMD format while retaining CommonJS compatibility through this hook.
+ // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
+ // from a content document and can't access our usual loaders. So, any
+ // modules shared with the JSON Viewer should include a define wrapper:
+ //
+ // // Make this available to both AMD and CJS environments
+ // define(function(require, exports, module) {
+ // ... code ...
+ // });
+ //
+ // Bug 1248830 will work out a better plan here for our content module
+ // loading needs, especially as we head towards devtools.html.
+ supportAMDModules: true,
+ requireHook: (id, require) => {
+ // If |id| requires special handling, simply defer to devtools
+ // immediately.
+ if (loader.isLoaderPluginId(id)) {
+ return devtoolsRequire(id);
+ }
+
+ const uri = require.resolve(id);
+
+ // The mocks can be set from tests using browser-loader-mocks.js setMockedModule().
+ // If there is an entry for a given uri in the `mocks` object, return it instead of
+ // requiring the module.
+ if (flags.testing && lazy.getMockedModule(uri)) {
+ return lazy.getMockedModule(uri);
+ }
+
+ if (
+ commonLibRequire &&
+ COMMON_LIBRARY_DIRS.some(dir => uri.startsWith(dir))
+ ) {
+ return commonLibRequire(uri);
+ }
+
+ // Check if the URI matches one of hardcoded paths or a regexp.
+ const isBrowserDir =
+ BROWSER_BASED_DIRS.some(dir => uri.startsWith(dir)) ||
+ uri.match(browserBasedDirsRegExp) != null;
+
+ if ((useOnlyShared || !uri.startsWith(baseURI)) && !isBrowserDir) {
+ return devtoolsRequire(uri);
+ }
+
+ return require(uri);
+ },
+ globals: {
+ // Allow modules to use the window's console to ensure logs appear in a
+ // tab toolbox, if one exists, instead of just the browser console.
+ console: window.console,
+ // Allow modules to use the DevToolsLoader lazy loading helpers.
+ loader: {
+ lazyGetter: loader.lazyGetter,
+ lazyServiceGetter: loader.lazyServiceGetter,
+ lazyRequireGetter: this.lazyRequireGetter.bind(this),
+ },
+ },
+ };
+
+ const mainModule = BaseLoader.Module(baseURI, joinURI(baseURI, "main.js"));
+ this.loader = BaseLoader.Loader(opts);
+ // When running tests, expose the BrowserLoader instance for metrics tests.
+ if (flags.testing) {
+ window.getBrowserLoaderForWindow = () => this;
+ }
+ this.require = BaseLoader.Require(this.loader, mainModule);
+}
+
+BrowserLoaderBuilder.prototype = {
+ /**
+ * Define a getter property on the given object that requires the given
+ * module. This enables delaying importing modules until the module is
+ * actually used.
+ *
+ * Several getters can be defined at once by providing an array of
+ * properties and enabling destructuring.
+ *
+ * @param { Object } obj
+ * The object to define the property on.
+ * @param { String | Array<String> } properties
+ * String: Name of the property for the getter.
+ * Array<String>: When destructure is true, properties can be an array of
+ * strings to create several getters at once.
+ * @param { String } module
+ * The module path.
+ * @param { Boolean } destructure
+ * Pass true if the property name is a member of the module's exports.
+ */
+ lazyRequireGetter(obj, properties, module, destructure) {
+ if (Array.isArray(properties) && !destructure) {
+ throw new Error(
+ "Pass destructure=true to call lazyRequireGetter with an array of properties"
+ );
+ }
+
+ if (!Array.isArray(properties)) {
+ properties = [properties];
+ }
+
+ for (const property of properties) {
+ loader.lazyGetter(obj, property, () => {
+ return destructure
+ ? this.require(module)[property]
+ : this.require(module || property);
+ });
+ }
+ },
+};
+
+this.BrowserLoader = BrowserLoader;
+
+this.EXPORTED_SYMBOLS = ["BrowserLoader"];
diff --git a/devtools/shared/loader/builtin-modules.js b/devtools/shared/loader/builtin-modules.js
new file mode 100644
index 0000000000..7dc04e5e98
--- /dev/null
+++ b/devtools/shared/loader/builtin-modules.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 module defines custom globals injected in all our modules and also
+ * pseudo modules that aren't separate files but just dynamically set values.
+ *
+ * Note that some globals are being defined by base-loader.sys.mjs via wantGlobalProperties property.
+ *
+ * As it does so, the module itself doesn't have access to these globals,
+ * nor the pseudo modules. Be careful to avoid loading any other js module as
+ * they would also miss them.
+ */
+
+const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+/**
+ * Defines a getter on a specified object that will be created upon first use.
+ *
+ * @param object
+ * The object to define the lazy getter on.
+ * @param name
+ * The name of the getter to define on object.
+ * @param lambda
+ * A function that returns what the getter should return. This will
+ * only ever be called once.
+ */
+function defineLazyGetter(object, name, lambda) {
+ Object.defineProperty(object, name, {
+ get() {
+ // Redefine this accessor property as a data property.
+ // Delete it first, to rule out "too much recursion" in case object is
+ // a proxy whose defineProperty handler might unwittingly trigger this
+ // getter again.
+ delete object[name];
+ const value = lambda.apply(object);
+ Object.defineProperty(object, name, {
+ value,
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ });
+ return value;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+}
+
+/**
+ * Defines a getter on a specified object for a service. The service will not
+ * be obtained until first use.
+ *
+ * @param object
+ * The object to define the lazy getter on.
+ * @param name
+ * The name of the getter to define on object for the service.
+ * @param contract
+ * The contract used to obtain the service.
+ * @param interfaceName
+ * The name of the interface to query the service to.
+ */
+function defineLazyServiceGetter(object, name, contract, interfaceName) {
+ defineLazyGetter(object, name, function () {
+ return Cc[contract].getService(Ci[interfaceName]);
+ });
+}
+
+/**
+ * Define a getter property on the given object that requires the given
+ * module. This enables delaying importing modules until the module is
+ * actually used.
+ *
+ * Several getters can be defined at once by providing an array of
+ * properties and enabling destructuring.
+ *
+ * @param { Object } obj
+ * The object to define the property on.
+ * @param { String | Array<String> } properties
+ * String: Name of the property for the getter.
+ * Array<String>: When destructure is true, properties can be an array of
+ * strings to create several getters at once.
+ * @param { String } module
+ * The module path.
+ * @param { Boolean } destructure
+ * Pass true if the property name is a member of the module's exports.
+ */
+function lazyRequireGetter(obj, properties, module, destructure) {
+ if (Array.isArray(properties) && !destructure) {
+ throw new Error(
+ "Pass destructure=true to call lazyRequireGetter with an array of properties"
+ );
+ }
+
+ if (!Array.isArray(properties)) {
+ properties = [properties];
+ }
+
+ for (const property of properties) {
+ defineLazyGetter(obj, property, () => {
+ return destructure
+ ? require(module)[property]
+ : require(module || property);
+ });
+ }
+}
+
+// List of pseudo modules exposed to all devtools modules.
+exports.modules = {
+ HeapSnapshot,
+ // Expose "chrome" Promise, which aren't related to any document
+ // and so are never frozen, even if the browser loader module which
+ // pull it is destroyed. See bug 1402779.
+ Promise,
+ TelemetryStopwatch,
+};
+
+defineLazyGetter(exports.modules, "Debugger", () => {
+ const global = Cu.getGlobalForObject(this);
+ // Debugger may already have been added.
+ if (global.Debugger) {
+ return global.Debugger;
+ }
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(global);
+ return global.Debugger;
+});
+
+defineLazyGetter(exports.modules, "ChromeDebugger", () => {
+ // Sandbox are memory expensive, so we should create as little as possible.
+ const debuggerSandbox = Cu.Sandbox(systemPrincipal, {
+ // This sandbox is used for the ChromeDebugger implementation.
+ // As we want to load the `Debugger` API for debugging chrome contexts,
+ // we have to ensure loading it in a distinct compartment from its debuggee.
+ freshCompartment: true,
+ });
+
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(debuggerSandbox);
+ return debuggerSandbox.Debugger;
+});
+
+defineLazyGetter(exports.modules, "xpcInspector", () => {
+ return Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
+});
+
+// List of all custom globals exposed to devtools modules.
+// Changes here should be mirrored to devtools/.eslintrc.
+exports.globals = {
+ isWorker: false,
+ loader: {
+ lazyGetter: defineLazyGetter,
+ lazyServiceGetter: defineLazyServiceGetter,
+ lazyRequireGetter,
+ // Defined by Loader.sys.mjs
+ id: null,
+ },
+};
+// DevTools loader copy globals property descriptors on each module global
+// object so that we have to memoize them from here in order to instantiate each
+// global only once.
+// `globals` is a cache object on which we put all global values
+// and we set getters on `exports.globals` returning `globals` values.
+const globals = {};
+function lazyGlobal(name, getter) {
+ defineLazyGetter(globals, name, getter);
+ Object.defineProperty(exports.globals, name, {
+ get() {
+ return globals[name];
+ },
+ configurable: true,
+ enumerable: true,
+ });
+}
+
+// Lazily define a few things so that the corresponding modules are only loaded
+// when used.
+lazyGlobal("clearTimeout", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .clearTimeout;
+});
+lazyGlobal("setTimeout", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .setTimeout;
+});
+lazyGlobal("clearInterval", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .clearInterval;
+});
+lazyGlobal("setInterval", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .setInterval;
+});
+lazyGlobal("WebSocket", () => {
+ return Services.appShell.hiddenDOMWindow.WebSocket;
+});
diff --git a/devtools/shared/loader/loader-plugin-raw.sys.mjs b/devtools/shared/loader/loader-plugin-raw.sys.mjs
new file mode 100644
index 0000000000..1e645ad3be
--- /dev/null
+++ b/devtools/shared/loader/loader-plugin-raw.sys.mjs
@@ -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/. */
+
+import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
+
+/**
+ * A function that can be used as part of a require hook for a
+ * loader.js Loader.
+ * This function handles "raw!" and "theme-loader!" requires.
+ * See also: https://github.com/webpack/raw-loader.
+ */
+export const requireRawId = function (id, require) {
+ const index = id.indexOf("!");
+ const rawId = id.slice(index + 1);
+ let uri = require.resolve(rawId);
+ // If the original string did not end with ".js", then
+ // require.resolve might have added the suffix. We don't want to
+ // add a suffix for a raw load (if needed the caller can specify it
+ // manually), so remove it here.
+ if (!id.endsWith(".js") && uri.endsWith(".js")) {
+ uri = uri.slice(0, -3);
+ }
+
+ const stream = NetUtil.newChannel({
+ uri: NetUtil.newURI(uri, "UTF-8"),
+ loadUsingSystemPrincipal: true,
+ }).open();
+
+ const count = stream.available();
+ const data = NetUtil.readInputStreamToString(stream, count, {
+ charset: "UTF-8",
+ });
+ stream.close();
+
+ // For the time being it doesn't seem worthwhile to cache the
+ // result here.
+ return data;
+};
diff --git a/devtools/shared/loader/moz.build b/devtools/shared/loader/moz.build
new file mode 100644
index 0000000000..cda2083625
--- /dev/null
+++ b/devtools/shared/loader/moz.build
@@ -0,0 +1,21 @@
+# -*- 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/.
+
+# The browser-loader modules should only be shipped together with the client.
+if CONFIG["MOZ_DEVTOOLS"] == "all":
+ DevToolsModules(
+ "browser-loader-mocks.js",
+ "browser-loader.js",
+ )
+
+DevToolsModules(
+ "base-loader.sys.mjs",
+ "builtin-modules.js",
+ "DistinctSystemPrincipalLoader.sys.mjs",
+ "loader-plugin-raw.sys.mjs",
+ "Loader.sys.mjs",
+ "worker-loader.js",
+)
diff --git a/devtools/shared/loader/worker-loader.js b/devtools/shared/loader/worker-loader.js
new file mode 100644
index 0000000000..4d8ff61bc7
--- /dev/null
+++ b/devtools/shared/loader/worker-loader.js
@@ -0,0 +1,536 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 worker, DebuggerNotificationObserver */
+
+// A CommonJS module loader that is designed to run inside a worker debugger.
+// We can't simply use the SDK module loader, because it relies heavily on
+// Components, which isn't available in workers.
+//
+// In principle, the standard instance of the worker loader should provide the
+// same built-in modules as its devtools counterpart, so that both loaders are
+// interchangable on the main thread, making them easier to test.
+//
+// On the worker thread, some of these modules, in particular those that rely on
+// the use of Components, and for which the worker debugger doesn't provide an
+// alternative API, will be replaced by vacuous objects. Consequently, they can
+// still be required, but any attempts to use them will lead to an exception.
+//
+// Note: to see dump output when running inside the worker thread, you might
+// need to enable the browser.dom.window.dump.enabled pref.
+
+this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];
+
+// Some notes on module ids and URLs:
+//
+// An id is either a relative id or an absolute id. An id is relative if and
+// only if it starts with a dot. An absolute id is a normalized id if and only
+// if it contains no redundant components.
+//
+// Every normalized id is a URL. A URL is either an absolute URL or a relative
+// URL. A URL is absolute if and only if it starts with a scheme name followed
+// by a colon and 2 or 3 slashes.
+
+/**
+ * Convert the given relative id to an absolute id.
+ *
+ * @param String id
+ * The relative id to be resolved.
+ * @param String baseId
+ * The absolute base id to resolve the relative id against.
+ *
+ * @return String
+ * An absolute id
+ */
+function resolveId(id, baseId) {
+ return baseId + "/../" + id;
+}
+
+/**
+ * Convert the given absolute id to a normalized id.
+ *
+ * @param String id
+ * The absolute id to be normalized.
+ *
+ * @return String
+ * A normalized id.
+ */
+function normalizeId(id) {
+ // An id consists of an optional root and a path. A root consists of either
+ // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
+ // root are not used as separators, so only normalize the path.
+ const [, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
+
+ const stack = [];
+ path.split("/").forEach(function (component) {
+ switch (component) {
+ case "":
+ case ".":
+ break;
+ case "..":
+ if (stack.length === 0) {
+ if (root !== undefined) {
+ throw new Error("Can't normalize absolute id '" + id + "'!");
+ } else {
+ stack.push("..");
+ }
+ } else if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ break;
+ default:
+ stack.push(component);
+ break;
+ }
+ });
+
+ return (root ? root : "") + stack.join("/");
+}
+
+/**
+ * Create a module object with the given normalized id.
+ *
+ * @param String
+ * The normalized id of the module to be created.
+ *
+ * @return Object
+ * A module with the given id.
+ */
+function createModule(id) {
+ return Object.create(null, {
+ // CommonJS specifies the id property to be non-configurable and
+ // non-writable.
+ id: {
+ configurable: false,
+ enumerable: true,
+ value: id,
+ writable: false,
+ },
+
+ // CommonJS does not specify an exports property, so follow the NodeJS
+ // convention, which is to make it non-configurable and writable.
+ exports: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true,
+ },
+ });
+}
+
+/**
+ * Create a CommonJS loader with the following options:
+ * - createSandbox:
+ * A function that will be used to create sandboxes. It should take the name
+ * and prototype of the sandbox to be created, and return the newly created
+ * sandbox as result. This option is required.
+ * - globals:
+ * A map of names to built-in globals that will be exposed to every module.
+ * Defaults to the empty map.
+ * - loadSubScript:
+ * A function that will be used to load scripts in sandboxes. It should take
+ * the URL from and the sandbox in which the script is to be loaded, and not
+ * return a result. This option is required.
+ * - modules:
+ * A map from normalized ids to built-in modules that will be added to the
+ * module cache. Defaults to the empty map.
+ * - paths:
+ * A map of paths to base URLs that will be used to resolve relative URLs to
+ * absolute URLS. Defaults to the empty map.
+ * - resolve:
+ * A function that will be used to resolve relative ids to absolute ids. It
+ * should take the relative id of a module to be required and the absolute
+ * id of the requiring module as arguments, and return the absolute id of
+ * the module to be required as result. Defaults to resolveId above.
+ */
+function WorkerDebuggerLoader(options) {
+ /**
+ * Convert the given relative URL to an absolute URL, using the map of paths
+ * given below.
+ *
+ * @param String url
+ * The relative URL to be resolved.
+ *
+ * @return String
+ * An absolute URL.
+ */
+ function resolveURL(url) {
+ let found = false;
+ for (const [path, baseURL] of paths) {
+ if (url.startsWith(path)) {
+ found = true;
+ url = url.replace(path, baseURL);
+ break;
+ }
+ }
+ if (!found) {
+ throw new Error("Can't resolve relative URL '" + url + "'!");
+ }
+
+ // If the url has no extension, use ".js" by default.
+ // Also allow loading JSMs, but they would need a shim in order to
+ // be loaded as a CommonJS module. (See SessionDataHelpers.jsm)
+ return url.endsWith(".js") || url.endsWith(".jsm") ? url : url + ".js";
+ }
+
+ /**
+ * Load the given module with the given url.
+ *
+ * @param Object module
+ * The module object to be loaded.
+ * @param String url
+ * The URL to load the module from.
+ */
+ function loadModule(module, url) {
+ // CommonJS specifies 3 free variables: require, exports, and module. These
+ // must be exposed to every module, so define these as properties on the
+ // sandbox prototype. Additional built-in globals are exposed by making
+ // the map of built-in globals the prototype of the sandbox prototype.
+ const prototype = Object.create(globals);
+ prototype.Components = {};
+ prototype.require = createRequire(module);
+ prototype.exports = module.exports;
+ prototype.module = module;
+
+ const sandbox = createSandbox(url, prototype);
+ try {
+ loadSubScript(url, sandbox);
+ } catch (error) {
+ if (/^Error opening input stream/.test(String(error))) {
+ throw new Error(
+ "Can't load module '" + module.id + "' with url '" + url + "'!"
+ );
+ }
+ throw error;
+ }
+
+ // The value of exports may have been changed by the module script, so
+ // freeze it if and only if it is still an object.
+ if (typeof module.exports === "object" && module.exports !== null) {
+ Object.freeze(module.exports);
+ }
+ }
+
+ /**
+ * Create a require function for the given module. If no module is given,
+ * create a require function for the top-level module instead.
+ *
+ * @param Object requirer
+ * The module for which the require function is to be created.
+ *
+ * @return Function
+ * A require function for the given module.
+ */
+ function createRequire(requirer) {
+ return function require(id) {
+ // Make sure an id was passed.
+ if (id === undefined) {
+ throw new Error("Can't require module without id!");
+ }
+
+ // Built-in modules are cached by id rather than URL, so try to find the
+ // module to be required by id first.
+ let module = modules[id];
+ if (module === undefined) {
+ // Failed to find the module to be required by id, so convert the id to
+ // a URL and try again.
+
+ // If the id is relative, convert it to an absolute id.
+ if (id.startsWith(".")) {
+ if (requirer === undefined) {
+ throw new Error(
+ "Can't require top-level module with relative id " +
+ "'" +
+ id +
+ "'!"
+ );
+ }
+ id = resolve(id, requirer.id);
+ }
+
+ // Convert the absolute id to a normalized id.
+ id = normalizeId(id);
+
+ // Convert the normalized id to a URL.
+ let url = id;
+
+ // If the URL is relative, resolve it to an absolute URL.
+ if (url.match(/^\w+:\/\//) === null) {
+ url = resolveURL(id);
+ }
+
+ // Try to find the module to be required by URL.
+ module = modules[url];
+ if (module === undefined) {
+ // Failed to find the module to be required in the cache, so create
+ // a new module, load it from the given URL, and add it to the cache.
+
+ // Add modules to the cache early so that any recursive calls to
+ // require for the same module will return the partially-loaded module
+ // from the cache instead of triggering a new load.
+ module = modules[url] = createModule(id);
+
+ try {
+ loadModule(module, url);
+ } catch (error) {
+ // If the module failed to load, remove it from the cache so that
+ // subsequent calls to require for the same module will trigger a
+ // new load, instead of returning a partially-loaded module from
+ // the cache.
+ delete modules[url];
+ throw error;
+ }
+
+ Object.freeze(module);
+ }
+ }
+
+ return module.exports;
+ };
+ }
+
+ const createSandbox = options.createSandbox;
+ const globals = options.globals || Object.create(null);
+ const loadSubScript = options.loadSubScript;
+
+ // Create the module cache, by converting each entry in the map from
+ // normalized ids to built-in modules to a module object, with the exports
+ // property of each module set to a frozen version of the original entry.
+ const modules = options.modules || {};
+ for (const id in modules) {
+ const module = createModule(id);
+ module.exports = Object.freeze(modules[id]);
+ modules[id] = module;
+ }
+
+ // Convert the map of paths to base URLs into an array for use by resolveURL.
+ // The array is sorted from longest to shortest path to ensure that the
+ // longest path is always the first to be found.
+ let paths = options.paths || Object.create(null);
+ paths = Object.keys(paths)
+ .sort((a, b) => b.length - a.length)
+ .map(path => [path, paths[path]]);
+
+ const resolve = options.resolve || resolveId;
+
+ this.require = createRequire();
+}
+
+this.WorkerDebuggerLoader = WorkerDebuggerLoader;
+
+var loader = {
+ lazyGetter(object, name, lambda) {
+ Object.defineProperty(object, name, {
+ get() {
+ delete object[name];
+ object[name] = lambda.apply(object);
+ return object[name];
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ },
+ lazyServiceGetter() {
+ throw new Error("Can't import XPCOM service from worker thread!");
+ },
+ lazyRequireGetter(obj, properties, module, destructure) {
+ if (Array.isArray(properties) && !destructure) {
+ throw new Error(
+ "Pass destructure=true to call lazyRequireGetter with an array of properties"
+ );
+ }
+
+ if (!Array.isArray(properties)) {
+ properties = [properties];
+ }
+
+ for (const property of properties) {
+ Object.defineProperty(obj, property, {
+ get: () =>
+ destructure
+ ? worker.require(module)[property]
+ : worker.require(module || property),
+ });
+ }
+ },
+};
+
+// The following APIs are defined differently depending on whether we are on the
+// main thread or a worker thread. On the main thread, we use the Components
+// object to implement them. On worker threads, we use the APIs provided by
+// the worker debugger.
+
+/* eslint-disable no-shadow */
+var {
+ Debugger,
+ URL,
+ createSandbox,
+ dump,
+ rpc,
+ loadSubScript,
+ setImmediate,
+ xpcInspector,
+} = function () {
+ // Main thread
+ if (typeof Components === "object") {
+ const principal = Components.Constructor(
+ "@mozilla.org/systemprincipal;1",
+ "nsIPrincipal"
+ )();
+
+ // To ensure that the this passed to addDebuggerToGlobal is a global, the
+ // Debugger object needs to be defined in a sandbox.
+ const sandbox = Cu.Sandbox(principal, {
+ wantGlobalProperties: ["ChromeUtils"],
+ });
+ Cu.evalInSandbox(
+ `
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ 'resource://gre/modules/jsdebugger.sys.mjs'
+);
+addDebuggerToGlobal(globalThis);
+`,
+ sandbox
+ );
+ const Debugger = sandbox.Debugger;
+
+ const createSandbox = function (name, prototype) {
+ return Cu.Sandbox(principal, {
+ invisibleToDebugger: true,
+ sandboxName: name,
+ sandboxPrototype: prototype,
+ wantComponents: false,
+ wantXrays: false,
+ });
+ };
+
+ const rpc = undefined;
+
+ // eslint-disable-next-line mozilla/use-services
+ const subScriptLoader = Cc[
+ "@mozilla.org/moz/jssubscript-loader;1"
+ ].getService(Ci.mozIJSSubScriptLoader);
+
+ const loadSubScript = function (url, sandbox) {
+ subScriptLoader.loadSubScript(url, sandbox);
+ };
+
+ const Timer = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+
+ const setImmediate = function (callback) {
+ Timer.setTimeout(callback, 0);
+ };
+
+ const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(
+ Ci.nsIJSInspector
+ );
+
+ const { URL } = Cu.Sandbox(principal, {
+ wantGlobalProperties: ["URL"],
+ });
+
+ return {
+ Debugger,
+ URL,
+ createSandbox,
+ dump: this.dump,
+ rpc,
+ loadSubScript,
+ setImmediate,
+ xpcInspector,
+ };
+ }
+ // Worker thread
+ const requestors = [];
+
+ const scope = this;
+
+ const xpcInspector = {
+ get eventLoopNestLevel() {
+ return requestors.length;
+ },
+
+ get lastNestRequestor() {
+ return requestors.length === 0 ? null : requestors[requestors.length - 1];
+ },
+
+ enterNestedEventLoop(requestor) {
+ requestors.push(requestor);
+ scope.enterEventLoop();
+ return requestors.length;
+ },
+
+ exitNestedEventLoop() {
+ requestors.pop();
+ scope.leaveEventLoop();
+ return requestors.length;
+ },
+ };
+
+ return {
+ Debugger: this.Debugger,
+ URL: this.URL,
+ createSandbox: this.createSandbox,
+ dump: this.dump,
+ rpc: this.rpc,
+ loadSubScript: this.loadSubScript,
+ setImmediate: this.setImmediate,
+ xpcInspector,
+ };
+}.call(this);
+/* eslint-enable no-shadow */
+
+// Create the default instance of the worker loader, using the APIs we defined
+// above.
+
+this.worker = new WorkerDebuggerLoader({
+ createSandbox,
+ globals: {
+ isWorker: true,
+ dump,
+ loader,
+ rpc,
+ URL,
+ setImmediate,
+ retrieveConsoleEvents: this.retrieveConsoleEvents,
+ setConsoleEventHandler: this.setConsoleEventHandler,
+ clearConsoleEvents: this.clearConsoleEvents,
+ console,
+ btoa: this.btoa,
+ atob: this.atob,
+ Services: Object.create(null),
+ ChromeUtils,
+ DebuggerNotificationObserver,
+
+ // The following APIs rely on the use of Components, and the worker debugger
+ // does not provide alternative definitions for them. Consequently, they are
+ // stubbed out both on the main thread and worker threads.
+ Cc: undefined,
+ ChromeWorker: undefined,
+ Ci: undefined,
+ Cu: undefined,
+ Cr: undefined,
+ Components: undefined,
+ },
+ loadSubScript,
+ modules: {
+ Debugger,
+ xpcInspector,
+ },
+ paths: {
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ devtools: "resource://devtools",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ promise: "resource://gre/modules/Promise-backend.js",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "xpcshell-test": "resource://test",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ },
+});
diff --git a/devtools/shared/locales/en-US/accessibility.properties b/devtools/shared/locales/en-US/accessibility.properties
new file mode 100644
index 0000000000..73958f93d1
--- /dev/null
+++ b/devtools/shared/locales/en-US/accessibility.properties
@@ -0,0 +1,142 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio): A title text for the color contrast
+# ratio description, used by the accessibility highlighter to display the value. %S in the
+# content will be replaced by the contrast ratio numerical value.
+accessibility.contrast.ratio=Contrast: %S
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio.error): A title text for the color
+# contrast ratio, used when the tool is unable to calculate the contrast ratio value.
+accessibility.contrast.ratio.error=Unable to calculate
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio.label): A title text for the color
+# contrast ratio description, used together with the actual values.
+accessibility.contrast.ratio.label=Contrast:
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio.label.large): A title text for the color
+# contrast ratio description that also specifies that the color contrast criteria used is
+# if for large text.
+accessibility.contrast.ratio.label.large=Contrast (large text):
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.area): A title text that
+# describes that currently selected accessible object for an <area> element must have
+# its name provided via the alt attribute.
+accessibility.text.label.issue.area = Use “alt” attribute to label “area” elements that have the “href” attribute.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.dialog): A title text that
+# describes that currently selected accessible object for a dialog should have a name
+# provided.
+accessibility.text.label.issue.dialog = Dialogs should be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.document.title): A title text that
+# describes that currently selected accessible object for a document must have a name
+# provided via title.
+accessibility.text.label.issue.document.title = Documents must have a title.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.embed): A title text that
+# describes that currently selected accessible object for an <embed> must have a name
+# provided.
+accessibility.text.label.issue.embed = Embedded content must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.figure): A title text that
+# describes that currently selected accessible object for a figure should have a name
+# provided.
+accessibility.text.label.issue.figure = Figures with optional captions should be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset): A title text that
+# describes that currently selected accessible object for a <fieldset> must have a name
+# provided.
+accessibility.text.label.issue.fieldset = “fieldset” elements must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset.legend2): A title text that
+# describes that currently selected accessible object for a <fieldset> must have a name
+# provided via <legend> element.
+accessibility.text.label.issue.fieldset.legend2 = Use a “legend” element to label a “fieldset”.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.form): A title text that
+# describes that currently selected accessible object for a form element must have a name
+# provided.
+accessibility.text.label.issue.form = Form elements must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.form.visible): A title text that
+# describes that currently selected accessible object for a form element should have a name
+# provided via a visible label/element.
+accessibility.text.label.issue.form.visible = Form elements should have a visible text label.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.frame): A title text that
+# describes that currently selected accessible object for a <frame> must have a name
+# provided.
+accessibility.text.label.issue.frame = “frame” elements must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.glyph): A title text that
+# describes that currently selected accessible object for a <mglyph> must have a name
+# provided via alt attribute.
+accessibility.text.label.issue.glyph = Use “alt” attribute to label “mglyph” elements.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.heading): A title text that
+# describes that currently selected accessible object for a heading must have a name
+# provided.
+accessibility.text.label.issue.heading = Headings must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.heading.content): A title text that
+# describes that currently selected accessible object for a heading must have visible
+# content.
+accessibility.text.label.issue.heading.content = Headings should have visible text content.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.iframe): A title text that
+# describes that currently selected accessible object for an <iframe> have a name
+# provided via title attribute.
+accessibility.text.label.issue.iframe = Use “title” attribute to describe “iframe” content.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.image): A title text that
+# describes that currently selected accessible object for graphical content must have a
+# name provided.
+accessibility.text.label.issue.image = Content with images must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.interactive): A title text that
+# describes that currently selected accessible object for interactive element must have a
+# name provided.
+accessibility.text.label.issue.interactive = Interactive elements must be labeled.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.optgroup.label2): A title text that
+# describes that currently selected accessible object for an <optgroup> must have a
+# name provided via label attribute.
+accessibility.text.label.issue.optgroup.label2 = Use a “label” attribute to label an “optgroup”.
+
+# LOCALIZATION NOTE (accessibility.text.label.issue.toolbar): A title text that
+# describes that currently selected accessible object for a toolbar must have a
+# name provided when there is more than one toolbar in the document.
+accessibility.text.label.issue.toolbar = Toolbars must be labeled when there is more than one toolbar.
+
+# LOCALIZATION NOTE (accessibility.keyboard.issue.semantics): A title text that
+# describes that currently selected accessible object is focusable and should
+# indicate that it could be interacted with.
+accessibility.keyboard.issue.semantics=Focusable elements should have interactive semantics.
+
+# LOCALIZATION NOTE (accessibility.keyboard.issue.tabindex): A title text that
+# describes that currently selected accessible object has a corresponding
+# DOMNode that defines a tabindex attribute greater that 0 which can result in
+# unexpected behaviour when navigating with keyboard.
+accessibility.keyboard.issue.tabindex=Avoid using “tabindex” attribute greater than zero.
+
+# LOCALIZATION NOTE (accessibility.keyboard.issue.action): A title text that
+# describes that currently selected accessible object is interactive but can not
+# be activated using keyboard or accessibility API.
+accessibility.keyboard.issue.action=Interactive elements must be able to be activated using a keyboard.
+
+# LOCALIZATION NOTE (accessibility.keyboard.issue.focusable): A title text that
+# describes that currently selected accessible object is interactive but is not
+# focusable with a keyboard.
+accessibility.keyboard.issue.focusable=Interactive elements must be focusable.
+
+# LOCALIZATION NOTE (accessibility.keyboard.issue.focus.visible): A title text
+# that describes that currently selected accessible object is focusable but
+# might not have appropriate focus styling.
+accessibility.keyboard.issue.focus.visible=Focusable element may be missing focus styling.
+
+# LOCALIZATION NOTE (accessibility.keyboard.issue.mouse.only): A title text that
+# describes that currently selected accessible object is not focusable and not
+# semantic but can be activated via mouse (e.g. has click handler).
+accessibility.keyboard.issue.mouse.only=Clickable elements must be focusable and should have interactive semantics.
diff --git a/devtools/shared/locales/en-US/debugger-paused-reasons.ftl b/devtools/shared/locales/en-US/debugger-paused-reasons.ftl
new file mode 100644
index 0000000000..3188cb8ed3
--- /dev/null
+++ b/devtools/shared/locales/en-US/debugger-paused-reasons.ftl
@@ -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/.
+
+### These strings are used inside the Debugger which is available from the Web
+### Developer sub-menu -> 'Debugger', as well as in the "Paused Debugger
+### Overlay" that is displayed in the content page when it pauses.
+
+### The correct localization of this file might be to keep it in
+### English, or another language commonly spoken among web developers.
+### You want to make that choice consistent across the developer tools.
+### A good criteria is the language in which you'd find the best
+### documentation on web development on the web.
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused due to a `debugger` statement in the code
+whypaused-debugger-statement = Paused on debugger statement
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on a breakpoint
+whypaused-breakpoint = Paused on breakpoint
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on an event breakpoint.
+whypaused-event-breakpoint = Paused on event breakpoint
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on an exception
+whypaused-exception = Paused on exception
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on a DOM mutation breakpoint
+whypaused-mutation-breakpoint = Paused on DOM mutation
+
+# The text that is displayed to describe an added node which triggers a subtree
+# modification
+whypaused-mutation-breakpoint-added = Added:
+
+# The text that is displayed to describe a removed node which triggers a subtree
+# modification
+whypaused-mutation-breakpoint-removed = Removed:
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused at a JS execution
+whypaused-interrupted = Paused at Execution
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused while stepping in or out of the stack
+whypaused-resume-limit = Paused while stepping
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on a dom event
+whypaused-pause-on-dom-events = Paused on event listener
+
+# The text that is displayed in an info block when evaluating a conditional
+# breakpoint throws an error
+whypaused-breakpoint-condition-thrown = Error with conditional breakpoint
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on an xml http request
+whypaused-xhr = Paused on XMLHttpRequest
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on a promise rejection
+whypaused-promise-rejection = Paused on promise rejection
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused at a watchpoint on an object property
+whypaused-get-watchpoint = Paused on property get
+
+# The text that is displayed in an info block explaining how the debugger is
+# currently paused at a watchpoint on an object property
+whypaused-set-watchpoint = Paused on property set
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on an assert
+whypaused-assert = Paused on assertion
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on a debugger statement
+whypaused-debug-command = Paused on debugged function
+
+# The text that is displayed in a info block explaining how the debugger is
+# currently paused on an event listener breakpoint set
+whypaused-other = Debugger paused
diff --git a/devtools/shared/locales/en-US/debugger.properties b/devtools/shared/locales/en-US/debugger.properties
new file mode 100644
index 0000000000..65e2beb79d
--- /dev/null
+++ b/devtools/shared/locales/en-US/debugger.properties
@@ -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/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Browser Tools sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (remoteIncomingPromptTitle): The title displayed on the
+# dialog that prompts the user to allow the incoming connection.
+remoteIncomingPromptTitle=Incoming Connection
+
+# LOCALIZATION NOTE (remoteIncomingPromptHeader): Header displayed on the
+# dialog that prompts the user to allow the incoming connection.
+remoteIncomingPromptHeader=An incoming request to permit remote debugging connection was detected. A remote client can take complete control over your browser!
+# LOCALIZATION NOTE (remoteIncomingPromptClientEndpoint): Part of the prompt
+# dialog for the user to choose whether an incoming connection should be
+# allowed.
+# %1$S: The host and port of the client such as "127.0.0.1:6000"
+remoteIncomingPromptClientEndpoint=Client Endpoint: %1$S
+# LOCALIZATION NOTE (remoteIncomingPromptServerEndpoint): Part of the prompt
+# dialog for the user to choose whether an incoming connection should be
+# allowed.
+# %1$S: The host and port of the server such as "127.0.0.1:6000"
+remoteIncomingPromptServerEndpoint=Server Endpoint: %1$S
+# LOCALIZATION NOTE (remoteIncomingPromptFooter): Footer displayed on the
+# dialog that prompts the user to allow the incoming connection.
+remoteIncomingPromptFooter=Allow connection?
+
+# LOCALIZATION NOTE (remoteIncomingPromptDisable): The label displayed on the
+# third button in the incoming connection dialog that lets the user disable the
+# remote devtools server.
+remoteIncomingPromptDisable=Disable
+
+# LOCALIZATION NOTE (clientSendOOBTitle): The title displayed on the dialog that
+# instructs the user to transfer an authentication token to the server.
+clientSendOOBTitle=Client Identification
+# LOCALIZATION NOTE (clientSendOOBHeader): Header displayed on the dialog that
+# instructs the user to transfer an authentication token to the server.
+clientSendOOBHeader=The endpoint you are connecting to needs more information to authenticate this connection. Please provide the token below in the prompt that appears on the other end.
+# LOCALIZATION NOTE (clientSendOOBHash): Part of the dialog that instructs the
+# user to transfer an authentication token to the server.
+# %1$S: The client's cert fingerprint
+clientSendOOBHash=My Cert: %1$S
+# LOCALIZATION NOTE (clientSendOOBToken): Part of the dialog that instructs the
+# user to transfer an authentication token to the server.
+# %1$S: The authentication token that the user will transfer.
+clientSendOOBToken=Token: %1$S
+
+# LOCALIZATION NOTE (serverReceiveOOBTitle): The title displayed on the dialog
+# that instructs the user to provide an authentication token from the client.
+serverReceiveOOBTitle=Provide Client Token
+# LOCALIZATION NOTE (serverReceiveOOBBody): Main text displayed on the dialog
+# that instructs the user to provide an authentication token from the client.
+serverReceiveOOBBody=The client should be displaying a token value. Enter that token value here to complete authentication with this client.
diff --git a/devtools/shared/locales/en-US/eyedropper.properties b/devtools/shared/locales/en-US/eyedropper.properties
new file mode 100644
index 0000000000..0f320ab37c
--- /dev/null
+++ b/devtools/shared/locales/en-US/eyedropper.properties
@@ -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/.
+
+# LOCALIZATION NOTE These strings are used in the Eyedropper color tool.
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (colorValue.copied): This text is displayed when the user selects a
+# color with the eyedropper and it's copied to the clipboard.
+colorValue.copied=copied
diff --git a/devtools/shared/locales/en-US/highlighters.ftl b/devtools/shared/locales/en-US/highlighters.ftl
new file mode 100644
index 0000000000..2eb3196cf4
--- /dev/null
+++ b/devtools/shared/locales/en-US/highlighters.ftl
@@ -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/.
+
+### This file contains strings used in highlighters.
+### Highlighters are visualizations that DevTools draws on top of content to aid
+### in understanding content sizing, etc.
+
+# The row and column position of a grid cell shown in the grid cell infobar when hovering
+# over the CSS grid outline.
+# Variables
+# $row (integer) - The row index
+# $column (integer) - The column index
+grid-row-column-positions = Row { $row } / Column { $column }
+
+# The layout type of an element shown in the infobar when hovering over a DOM element and
+# it is a grid container.
+gridtype-container = Grid Container
+
+# The layout type of an element shown in the infobar when hovering over a DOM element and
+# it is a grid item.
+gridtype-item = Grid Item
+
+# The layout type of an element shown in the infobar when hovering over a DOM element and
+# it is both a grid container and a grid item.
+gridtype-dual = Grid Container/Item
+
+# The layout type of an element shown in the infobar when hovering over a DOM element and
+# it is a flex container.
+flextype-container = Flex Container
+
+# The layout type of an element shown in the infobar when hovering over a DOM element and
+# it is a flex item.
+flextype-item = Flex Item
+
+# The layout type of an element shown in the infobar when hovering over a DOM element and
+# it is both a flex container and a flex item.
+flextype-dual = Flex Container/Item
+
+# The message displayed in the content page when the user clicks on the
+# "Pick an element from the page" in about:devtools-toolbox inspector panel, when
+# debugging a remote page.
+# Variables
+# $action (string) - Will either be remote-node-picker-notice-action-desktop or
+# remote-node-picker-notice-action-touch
+remote-node-picker-notice = DevTools Node Picker enabled. { $action }
+
+# Text displayed in `remote-node-picker-notice`, when the remote page is on desktop
+remote-node-picker-notice-action-desktop = Click an element to select it in the Inspector
+
+# Text displayed in `remote-node-picker-notice`, when the remote page is on Android
+remote-node-picker-notice-action-touch = Tap an element to select it in the Inspector
+
+# The text displayed in the button that is in the notice in the content page when the user
+# clicks on the "Pick an element from the page" in about:devtools-toolbox inspector panel,
+# when debugging a remote page.
+remote-node-picker-notice-hide-button = Hide
+
+# The text displayed in a toolbox notification message which is only displayed
+# if prefers-reduced-motion is enabled (via OS-level settings or by using the
+# ui.prefersReducedMotion=1 preference).
+simple-highlighters-message = When prefers-reduced-motion is enabled, a simpler highlighter can be enabled in the settings panel, to avoid flashing colors.
+
+# Text displayed in a button inside the "simple-highlighters-message" toolbox
+# notification. "Settings" here refers to the DevTools settings panel.
+simple-highlighters-settings-button = Open Settings
diff --git a/devtools/shared/locales/en-US/screenshot.properties b/devtools/shared/locales/en-US/screenshot.properties
new file mode 100644
index 0000000000..fd1296e14d
--- /dev/null
+++ b/devtools/shared/locales/en-US/screenshot.properties
@@ -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/.
+
+# LOCALIZATION NOTE These strings are used inside the Web Console commands
+# which can be executed in the Developer Tools, available in the
+# Browser Tools sub-menu -> 'Web Developer Tools'
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (screenshotDesc) A very short description of the
+# 'screenshot' command. Displayed when the --help flag is passed to
+# the screenshot command.
+screenshotDesc=Save an image of the page
+
+# LOCALIZATION NOTE (screenshotFilenameDesc) A very short string to describe
+# the 'filename' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotFilenameDesc=Destination filename
+
+# LOCALIZATION NOTE (screenshotFilenameManual) A fuller description of the
+# 'filename' parameter to the 'screenshot' command.
+screenshotFilenameManual=The name of the file (should have a ‘.png’ extension) to which we write the screenshot.
+
+# LOCALIZATION NOTE (screenshotClipboardDesc) A very short string to describe
+# the 'clipboard' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotClipboardDesc=Copy screenshot to clipboard? (true/false)
+
+# LOCALIZATION NOTE (screenshotClipboardManual) A fuller description of the
+# 'clipboard' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotClipboardManual=True if you want to copy the screenshot instead of saving it to a file.
+
+# LOCALIZATION NOTE (screenshotGroupOptions) A label for the optional options of
+# the screenshot command. Displayed when the --help flag is passed to the
+# screenshot command.
+screenshotGroupOptions=Options
+
+# LOCALIZATION NOTE (screenshotDelayDesc) A very short string to describe
+# the 'delay' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotDelayDesc=Delay (seconds)
+
+# LOCALIZATION NOTE (screenshotDelayManual) A fuller description of the
+# 'delay' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotDelayManual=The time to wait (in seconds) before the screenshot is taken
+
+# LOCALIZATION NOTE (screenshotDPRDesc) A very short string to describe
+# the 'dpr' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotDPRDesc=Device pixel ratio
+
+# LOCALIZATION NOTE (screenshotDPRManual) A fuller description of the
+# 'dpr' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotDPRManual=The device pixel ratio to use when taking the screenshot
+
+# LOCALIZATION NOTE (screenshotFullPageDesc) A very short string to describe
+# the 'fullpage' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFullPageDesc=Entire webpage? (true/false)
+
+# LOCALIZATION NOTE (screenshotFullPageManual) A fuller description of the
+# 'fullpage' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFullPageManual=True if the screenshot should also include parts of the webpage which are outside the current scrolled bounds.
+
+# LOCALIZATION NOTE (screenshotFileDesc) A very short string to describe
+# the 'file' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFileDesc=Save to file? (true/false)
+
+# LOCALIZATION NOTE (screenshotFileManual) A fuller description of the
+# 'file' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFileManual=True if the screenshot should save the file even when other options are enabled (eg. clipboard).
+
+# LOCALIZATION NOTE (screenshotGeneratedFilename) The auto generated filename
+# when no file name is provided. The first argument (%1$S) is the date string
+# in yyyy-mm-dd format and the second argument (%2$S) is the time string
+# in HH.MM.SS format. Please don't add the extension here.
+screenshotGeneratedFilename=Screen Shot %1$S at %2$S
+
+# LOCALIZATION NOTE (screenshotErrorSavingToFile) Text displayed to user upon
+# encountering error while saving the screenshot to the file specified.
+# The argument (%1$S) is the filename.
+screenshotErrorSavingToFile=Error saving to %1$S
+
+# LOCALIZATION NOTE (screenshotSavedToFile) Text displayed to user when the
+# screenshot is successfully saved to the file specified.
+# The argument (%1$S) is the filename.
+screenshotSavedToFile=Saved to %1$S
+
+# LOCALIZATION NOTE (screenshotErrorCopying) Text displayed to user upon
+# encountering error while copying the screenshot to clipboard.
+screenshotErrorCopying=Error occurred while copying screenshot to clipboard.
+
+# LOCALIZATION NOTE (screenshotCopied) Text displayed to user when the
+# screenshot is successfully copied to the clipboard.
+screenshotCopied=Screenshot copied to clipboard.
+
+# LOCALIZATION NOTE (inspectNodeDesc) A very short string to describe the
+# 'node' parameter to the 'inspect' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+inspectNodeDesc=CSS selector
+
+# LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node'
+# parameter to the 'inspect' command. Displayed when the --help flag is
+# passed to the `screenshot command.
+inspectNodeManual=A CSS selector for use with document.querySelector which identifies a single element
+
+# LOCALIZATION NOTE (screenshotTruncationWarning) Text displayed to user when the image
+# that would be created by the screenshot is too big and needs to be truncated to avoid
+# errors.
+# The first parameter is the width of the final image and the second parameter is the
+# height of the image.
+screenshotTruncationWarning=The image was cut off to %1$S×%2$S as the resulting image was too large
+
+# LOCALIZATION NOTE (screenshotDPRDecreasedWarning2) Text displayed to user when
+# taking the screenshot initially failed. When the Device Pixel Ratio is larger
+# than 1.0 a second try immediately after displaying this message is attempted.
+screenshotDPRDecreasedWarning=The device pixel ratio was reduced to 1 as the resulting image was too large
+
+# LOCALIZATION NOTE (screenshotRenderingError) Text displayed to user upon
+# encountering an error while rendering the screenshot. This most often happens when the
+# resulting image is too large to be rendered.
+screenshotRenderingError=Error creating the image. The resulting image was probably too large.
+
+# LOCALIZATION NOTE (screenshotNoSelectorMatchWarning) Text displayed to user when the
+# provided selector for the screenshot does not match any element on the page.
+# The argument (%1$S) is selector.
+screenshotNoSelectorMatchWarning=The ‘%S’ selector does not match any element on the page.
diff --git a/devtools/shared/locales/en-US/shared.properties b/devtools/shared/locales/en-US/shared.properties
new file mode 100644
index 0000000000..0978450dee
--- /dev/null
+++ b/devtools/shared/locales/en-US/shared.properties
@@ -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/.
+
+# LOCALIZATION NOTE (ellipsis): The ellipsis (three dots) character
+ellipsis=…
diff --git a/devtools/shared/locales/en-US/styleinspector.properties b/devtools/shared/locales/en-US/styleinspector.properties
new file mode 100644
index 0000000000..4d74bac132
--- /dev/null
+++ b/devtools/shared/locales/en-US/styleinspector.properties
@@ -0,0 +1,267 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE These strings are used inside the Style Inspector.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+
+# LOCALIZATION NOTE (rule.status): For each style property the panel shows
+# the rules which hold that specific property. For every rule, the rule status
+# is also displayed: a rule can be the best match, a match, a parent match, or a
+# rule did not match the element the user has highlighted.
+rule.status.BEST=Best Match
+rule.status.MATCHED=Matched
+rule.status.PARENT_MATCH=Parent Match
+
+# LOCALIZATION NOTE (rule.sourceElement, rule.sourceInline,
+# rule.sourceConstructed): For each style property the panel shows the rules
+# which hold that specific property.
+# For every rule, the rule source is also displayed: a rule can come from a
+# file, from the same page (inline), from a constructed style sheet
+# (constructed), or from the element itself (element).
+rule.sourceInline=inline
+rule.sourceConstructed=constructed
+rule.sourceElement=element
+
+# LOCALIZATION NOTE (rule.inheritedFrom): Shown for CSS rules
+# that were inherited from a parent node. Will be passed a node
+# identifier of the parent node.
+# e.g "Inherited from body#bodyID"
+rule.inheritedFrom=Inherited from %S
+
+# LOCALIZATION NOTE (rule.keyframe): Shown for CSS Rules keyframe header.
+# Will be passed an identifier of the keyframe animation name.
+rule.keyframe=Keyframes %S
+
+# LOCALIZATION NOTE (rule.userAgentStyles): Shown next to the style sheet
+# link for CSS rules that were loaded from a user agent style sheet.
+# These styles will not be editable, and will only be visible if the
+# devtools.inspector.showUserAgentStyles pref is true.
+rule.userAgentStyles=(user agent)
+
+# LOCALIZATION NOTE (rule.pseudoElement): Shown for CSS rules
+# pseudo element header
+rule.pseudoElement=Pseudo-elements
+
+# LOCALIZATION NOTE (rule.selectedElement): Shown for CSS rules element header if
+# pseudo elements are present in the rule view.
+rule.selectedElement=This Element
+
+# LOCALIZATION NOTE (rule.warning.title): When an invalid property value is
+# entered into the rule view a warning icon is displayed. This text is used for
+# the title attribute of the warning icon.
+rule.warning.title=Invalid property value
+
+# LOCALIZATION NOTE (rule.warningName.title): When an invalid property name is
+# entered into the rule view a warning icon is displayed. This text is used for
+# the title attribute of the warning icon.
+rule.warningName.title=Invalid property name
+
+# LOCALIZATION NOTE (rule.filterProperty.title): Text displayed in the tooltip
+# of the search button that is shown next to a property that has been overridden
+# in the rule view.
+rule.filterProperty.title=Filter rules containing this property
+
+# LOCALIZATION NOTE (rule.empty): Text displayed when the highlighter is
+# first opened and there's no node selected in the rule view.
+rule.empty=No element selected.
+
+# LOCALIZATION NOTE (rule.variableValue): Text displayed in a tooltip
+# when the mouse is over a variable use (like "var(--something)") in
+# the rule view. The first argument is the variable name and the
+# second argument is the value.
+rule.variableValue=%S = %S
+
+# LOCALIZATION NOTE (rule.variableUnset): Text displayed in a tooltip
+# when the mouse is over a variable use (like "var(--something)"),
+# where the variable is not set. the rule view. The argument is the
+# variable name.
+rule.variableUnset=%S is not set
+
+# LOCALIZATION NOTE (rule.selectorHighlighter.tooltip): Text displayed in a
+# tooltip when the mouse is over a selector highlighter icon in the rule view.
+rule.selectorHighlighter.tooltip=Highlight all elements matching this selector
+
+# LOCALIZATION NOTE (rule.colorSwatch.tooltip): Text displayed in a tooltip
+# when the mouse is over a color swatch in the rule view.
+rule.colorSwatch.tooltip=Click to open the color picker, Shift+click to change the color format
+
+# LOCALIZATION NOTE (rule.bezierSwatch.tooltip): Text displayed in a tooltip
+# when the mouse is over a cubic-bezier swatch in the rule view.
+rule.bezierSwatch.tooltip=Click to open the timing-function editor
+
+# LOCALIZATION NOTE (rule.filterSwatch.tooltip): Text displayed in a tooltip
+# when the mouse is over a filter swatch in the rule view.
+rule.filterSwatch.tooltip=Click to open the filter editor
+
+# LOCALIZATION NOTE (rule.angleSwatch.tooltip): Text displayed in a tooltip
+# when the mouse is over a angle swatch in the rule view.
+rule.angleSwatch.tooltip=Shift+click to change the angle format
+
+# LOCALIZATION NOTE (rule.flexToggle.tooltip): Text displayed in a tooltip
+# when the mouse is over a Flexbox toggle icon in the rule view.
+rule.flexToggle.tooltip=Click to toggle the Flexbox highlighter
+
+# LOCALIZATION NOTE (rule.gridToggle.tooltip): Text displayed in a tooltip
+# when the mouse is over a CSS Grid toggle icon in the rule view.
+rule.gridToggle.tooltip=Click to toggle the CSS Grid highlighter
+
+# LOCALIZATION NOTE (rule.filterStyles.placeholder): This is the placeholder that
+# goes in the search box when no search term has been entered.
+rule.filterStyles.placeholder=Filter Styles
+
+# LOCALIZATION NOTE (rule.addRule.tooltip): This is the tooltip shown when
+# hovering the `Add new rule` button in the rules view toolbar.
+rule.addRule.tooltip=Add new rule
+
+# LOCALIZATION NOTE (rule.togglePseudo.tooltip): This is the tooltip
+# shown when hovering over the `Toggle Pseudo Class Panel` button in the
+# rule view toolbar.
+rule.togglePseudo.tooltip=Toggle pseudo-classes
+
+# LOCALIZATION NOTE (rule.classPanel.toggleClass.tooltip): This is the tooltip
+# shown when hovering over the `Toggle Class Panel` button in the
+# rule view toolbar.
+rule.classPanel.toggleClass.tooltip=Toggle classes
+
+# LOCALIZATION NOTE (rule.classPanel.newClass.placeholder): This is the placeholder
+# shown inside the text field used to add a new class in the rule-view.
+rule.classPanel.newClass.placeholder=Add new class
+
+# LOCALIZATION NOTE (rule.classPanel.noClasses): This is the text displayed in the
+# class panel when the current element has no classes applied.
+rule.classPanel.noClasses=No classes on this element
+
+# LOCALIZATION NOTE (rule.printSimulation.tooltip):
+# This is the tooltip of the print simulation button in the Rule View toolbar
+# that toggles print simulation.
+rule.printSimulation.tooltip=Toggle print media simulation for the page
+
+# LOCALIZATION NOTE (rule.colorSchemeSimulation.tooltip):
+# This is the tooltip of the color scheme simulation button in the Rule View
+# toolbar that toggles color-scheme simulation.
+rule.colorSchemeSimulation.tooltip=Toggle color-scheme simulation for the page
+
+# LOCALIZATION NOTE (rule.twistyCollapse.label): The text a screen reader
+# speaks when the header of a rule is expanded.
+rule.twistyCollapse.label=Collapse
+
+# LOCALIZATION NOTE (rule.twistyExpand.label): The text a screen reader
+# speaks when the header of a rule is collapsed.
+rule.twistyExpand.label=Expand
+
+# LOCALIZATION NOTE (rule.expandableContainerToggleButton.title):
+# This is the tooltip for expandable container toggle button in the Rule View (Pseudo-elements, keyframes, …)
+rule.expandableContainerToggleButton.title=Toggle panel
+
+# LOCALIZATION NOTE (rule.containerQuery.selectContainerButton.tooltip): Text displayed in a
+# tooltip when the mouse is over the icon to select a container in a container query in the rule view.
+rule.containerQuery.selectContainerButton.tooltip=Click to select the container node
+
+# LOCALIZATION NOTE (rule.propertyToggle.label):
+# This is the label for the checkbox input in the rule view that allow to disable/re-enable
+# a specific property in a rule.
+# The argument is the property name.
+rule.propertyToggle.label=Enable %S property
+
+# LOCALIZATION NOTE (rule.newPropertyName.label):
+# This is the label for the new property input in the rule view.
+rule.newPropertyName.label=New property name
+
+# LOCALIZATION NOTE (rule.propertyName.label):
+# This is the label for the property name input in the rule view.
+rule.propertyName.label=Property name
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyColor): Text displayed in the rule
+# and computed view context menu when a color value was clicked.
+styleinspector.contextmenu.copyColor=Copy Color
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyColor.accessKey): Access key for
+# the rule and computed view context menu "Copy Color" entry.
+styleinspector.contextmenu.copyColor.accessKey=L
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyUrl): In rule and computed view :
+# text displayed in the context menu for an image URL.
+# Clicking it copies the URL to the clipboard of the user.
+styleinspector.contextmenu.copyUrl=Copy URL
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyUrl.accessKey): Access key for
+# the rule and computed view context menu "Copy URL" entry.
+styleinspector.contextmenu.copyUrl.accessKey=U
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyImageDataUrl): In rule and computed view :
+# text displayed in the context menu for an image URL.
+# Clicking it copies the image as Data-URL to the clipboard of the user.
+styleinspector.contextmenu.copyImageDataUrl=Copy Image Data-URL
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyImageDataUrl.accessKey): Access key for
+# the rule and computed view context menu "Copy Image Data-URL" entry.
+styleinspector.contextmenu.copyImageDataUrl.accessKey=I
+
+# LOCALIZATION NOTE (styleinspector.copyImageDataUrlError): Text set in the clipboard
+# if an error occurs when using the copyImageDataUrl context menu action
+# (invalid image link, timeout, etc...)
+styleinspector.copyImageDataUrlError=Failed to copy image Data-URL
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.toggleOrigSources): Text displayed in the rule view
+# context menu.
+styleinspector.contextmenu.toggleOrigSources=Show Original Sources
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.toggleOrigSources.accessKey): Access key for
+# the rule view context menu "Show original sources" entry.
+styleinspector.contextmenu.toggleOrigSources.accessKey=O
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.addNewRule): Text displayed in the
+# rule view context menu for adding a new rule to the element.
+# This should match inspector.addRule.tooltip in inspector.properties
+styleinspector.contextmenu.addNewRule=Add New Rule
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.addNewRule.accessKey): Access key for
+# the rule view context menu "Add rule" entry.
+styleinspector.contextmenu.addNewRule.accessKey=R
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.selectAll): Text displayed in the
+# computed view context menu.
+styleinspector.contextmenu.selectAll=Select All
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.selectAll.accessKey): Access key for
+# the computed view context menu "Select all" entry.
+styleinspector.contextmenu.selectAll.accessKey=A
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copy): Text displayed in the
+# computed view context menu.
+styleinspector.contextmenu.copy=Copy
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copy.accessKey): Access key for
+# the computed view context menu "Copy" entry.
+styleinspector.contextmenu.copy.accessKey=C
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyLocation): Text displayed in the
+# rule view context menu for copying the source location.
+styleinspector.contextmenu.copyLocation=Copy Location
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyDeclaration): Text
+# displayed in the rule view context menu for copying the CSS declaration.
+styleinspector.contextmenu.copyDeclaration=Copy Declaration
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyPropertyName): Text displayed in
+# the rule view context menu for copying the property name.
+styleinspector.contextmenu.copyPropertyName=Copy Property Name
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyPropertyValue): Text displayed in
+# the rule view context menu for copying the property value.
+styleinspector.contextmenu.copyPropertyValue=Copy Property Value
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copyRule): Text displayed in the
+# rule view context menu for copying the rule.
+styleinspector.contextmenu.copyRule=Copy Rule
+
+# LOCALIZATION NOTE (styleinspector.contextmenu.copySelector): Text displayed in the
+# rule view context menu for copying the selector.
+styleinspector.contextmenu.copySelector=Copy Selector
diff --git a/devtools/shared/locales/en-US/webconsole-commands.ftl b/devtools/shared/locales/en-US/webconsole-commands.ftl
new file mode 100644
index 0000000000..14517f2321
--- /dev/null
+++ b/devtools/shared/locales/en-US/webconsole-commands.ftl
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# These strings are used inside the Web Console commands
+# which can be executed in the Developer Tools, available in the
+# Browser Tools sub-menu -> 'Web Developer Tools'
+
+# Usage string for :block command
+webconsole-commands-usage-block =
+ :block URL_STRING
+
+ Start blocking network requests
+
+ It accepts only one URL_STRING argument, an unquoted string which will be used to block all requests whose URL includes this string.
+ Use :unblock or the Network Monitor request blocking sidebar to undo this.
+
+# Usage string for :unblock command
+webconsole-commands-usage-unblock =
+ :unblock URL_STRING
+
+ Stop blocking network requests
+
+ It accepts only one argument, the exact same string previously passed to :block.
diff --git a/devtools/shared/locales/jar.mn b/devtools/shared/locales/jar.mn
new file mode 100644
index 0000000000..05820242b3
--- /dev/null
+++ b/devtools/shared/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[localization] @AB_CD@.jar:
+ devtools/shared (%*.ftl)
+
+@AB_CD@.jar:
+% locale devtools-shared @AB_CD@ %locale/@AB_CD@/devtools/shared/
+ locale/@AB_CD@/devtools/shared/ (%*.properties)
diff --git a/devtools/shared/locales/l10n.toml b/devtools/shared/locales/l10n.toml
new file mode 100644
index 0000000000..88e817e349
--- /dev/null
+++ b/devtools/shared/locales/l10n.toml
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+basepath = "../../.."
+
+[env]
+ l = "{l10n_base}/{locale}/"
+
+[[paths]]
+ reference = "devtools/shared/locales/en-US/**"
+ l10n = "{l}devtools/shared/**"
diff --git a/devtools/shared/locales/moz.build b/devtools/shared/locales/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/devtools/shared/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/devtools/shared/moz.build b/devtools/shared/moz.build
new file mode 100644
index 0000000000..302ab58fa4
--- /dev/null
+++ b/devtools/shared/moz.build
@@ -0,0 +1,79 @@
+# -*- 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/.
+
+include("../templates.mozbuild")
+
+DIRS += [
+ "css",
+ "commands",
+ "compatibility",
+ "discovery",
+ "heapsnapshot",
+ "images",
+ "inspector",
+ "jsbeautify",
+ "layout",
+ "loader",
+ "locales",
+ "network-observer",
+ "node-properties",
+ "performance-new",
+ "platform",
+ "protocol",
+ "qrcode",
+ "security",
+ "sprintfjs",
+ "specs",
+ "storage",
+ "test-helpers",
+ "transport",
+ "webconsole",
+ "worker",
+]
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+BROWSER_CHROME_MANIFESTS += ["test-helpers/browser.toml"]
+XPCSHELL_TESTS_MANIFESTS += ["test-helpers/xpcshell.toml"]
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+DevToolsModules(
+ "accessibility.js",
+ "async-storage.js",
+ "async-utils.js",
+ "constants.js",
+ "content-observer.js",
+ "debounce.js",
+ "DevToolsInfaillibleUtils.sys.mjs",
+ "DevToolsUtils.js",
+ "dom-helpers.js",
+ "dom-node-constants.js",
+ "dom-node-filter-constants.js",
+ "event-emitter.js",
+ "extend.js",
+ "flags.js",
+ "generate-uuid.js",
+ "indentation.js",
+ "indexed-db.js",
+ "l10n.js",
+ "natural-sort.js",
+ "path.js",
+ "picker-constants.js",
+ "plural-form.js",
+ "protocol.js",
+ "system.js",
+ "ThreadSafeDevToolsUtils.js",
+ "throttle.js",
+ "validate-breakpoint.jsm",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "General")
diff --git a/devtools/shared/natural-sort.js b/devtools/shared/natural-sort.js
new file mode 100644
index 0000000000..0b6a30db5f
--- /dev/null
+++ b/devtools/shared/natural-sort.js
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Based on the Natural Sort algorithm for Javascript - Version 0.8.1 - adapted
+ * for Firefox DevTools and released under the MIT license.
+ *
+ * Author: Jim Palmer (based on chunking idea from Dave Koelle)
+ *
+ * Repository:
+ * https://github.com/overset/javascript-natural-sort/
+ */
+
+"use strict";
+
+const tokenizeNumbersRx =
+ /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g;
+const hexRx = /^0x[0-9a-f]+$/i;
+const startsWithNullRx = /^\0/;
+const endsWithNullRx = /\0$/;
+const whitespaceRx = /\s+/g;
+const startsWithZeroRx = /^0/;
+const versionRx = /^([\w-]+-)?\d+\.\d+\.\d+$/;
+const numericDateRx = /^\d+[- /]\d+[- /]\d+$/;
+
+// If a string contains any of these, we'll try to parse it as a Date
+const dateKeywords = [
+ "mon",
+ "tues",
+ "wed",
+ "thur",
+ "fri",
+ "sat",
+ "sun",
+
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec",
+];
+
+/**
+ * Figures whether a given string should be considered by naturalSort to be a
+ * Date, and returns the Date's timestamp if so. Some Date formats, like
+ * single numbers and MM.DD.YYYY, are not supported due to conflicts with things
+ * like version numbers.
+ */
+function tryParseDate(str) {
+ const lowerCaseStr = str.toLowerCase();
+ return (
+ !versionRx.test(str) &&
+ (numericDateRx.test(str) ||
+ dateKeywords.some(s => lowerCaseStr.includes(s))) &&
+ Date.parse(str)
+ );
+}
+
+/**
+ * Sort numbers, strings, IP Addresses, Dates, Filenames, version numbers etc.
+ * "the way humans do."
+ *
+ * @param {Object} a
+ * Passed in by Array.sort(a, b)
+ * @param {Object} b
+ * Passed in by Array.sort(a, b)
+ * @param {String} sessionString
+ * Client-side value of storage-expires-session l10n string.
+ * Since this function can be called from both the client and the server,
+ * and given that client and server might have different locale, we can't compute
+ * the localized string directly from here.
+ * @param {Boolean} insensitive
+ * Should the search be case insensitive?
+ */
+// eslint-disable-next-line complexity
+function naturalSort(a = "", b = "", sessionString, insensitive = false) {
+ // Ensure we are working with trimmed strings
+ a = (a + "").trim();
+ b = (b + "").trim();
+
+ if (insensitive) {
+ a = a.toLowerCase();
+ b = b.toLowerCase();
+ sessionString = sessionString.toLowerCase();
+ }
+
+ // Chunk/tokenize - Here we split the strings into arrays or strings and
+ // numbers.
+ const aChunks = a
+ .replace(tokenizeNumbersRx, "\0$1\0")
+ .replace(startsWithNullRx, "")
+ .replace(endsWithNullRx, "")
+ .split("\0");
+ const bChunks = b
+ .replace(tokenizeNumbersRx, "\0$1\0")
+ .replace(startsWithNullRx, "")
+ .replace(endsWithNullRx, "")
+ .split("\0");
+
+ // Hex or date detection.
+ const aHexOrDate = parseInt(a.match(hexRx), 16) || tryParseDate(a);
+ const bHexOrDate = parseInt(b.match(hexRx), 16) || tryParseDate(b);
+
+ if (
+ (aHexOrDate || bHexOrDate) &&
+ (a === sessionString || b === sessionString)
+ ) {
+ // We have a date and a session string. Move "Session" above the date
+ // (for session cookies)
+ if (a === sessionString) {
+ return -1;
+ } else if (b === sessionString) {
+ return 1;
+ }
+ }
+
+ // Try and sort Hex codes or Dates.
+ if (aHexOrDate && bHexOrDate) {
+ if (aHexOrDate < bHexOrDate) {
+ return -1;
+ } else if (aHexOrDate > bHexOrDate) {
+ return 1;
+ }
+ return 0;
+ }
+
+ // Natural sorting through split numeric strings and default strings
+ const aChunksLength = aChunks.length;
+ const bChunksLength = bChunks.length;
+ const maxLen = Math.max(aChunksLength, bChunksLength);
+
+ for (let i = 0; i < maxLen; i++) {
+ const aChunk = normalizeChunk(aChunks[i] || "", aChunksLength);
+ const bChunk = normalizeChunk(bChunks[i] || "", bChunksLength);
+
+ // Handle numeric vs string comparison - number < string
+ if (isNaN(aChunk) !== isNaN(bChunk)) {
+ return isNaN(aChunk) ? 1 : -1;
+ }
+
+ // If unicode use locale comparison
+ // eslint-disable-next-line no-control-regex
+ if (/[^\x00-\x80]/.test(aChunk + bChunk) && aChunk.localeCompare) {
+ const comp = aChunk.localeCompare(bChunk);
+ return comp / Math.abs(comp);
+ }
+ if (aChunk < bChunk) {
+ return -1;
+ } else if (aChunk > bChunk) {
+ return 1;
+ }
+ }
+ return null;
+}
+
+// Normalize spaces; find floats not starting with '0', string or 0 if not
+// defined
+const normalizeChunk = function (str, length) {
+ return (
+ ((!str.match(startsWithZeroRx) || length == 1) && parseFloat(str)) ||
+ str.replace(whitespaceRx, " ").trim() ||
+ 0
+ );
+};
+
+exports.naturalSortCaseSensitive = function naturalSortCaseSensitive(
+ a,
+ b,
+ sessionString
+) {
+ return naturalSort(a, b, sessionString, false);
+};
+
+exports.naturalSortCaseInsensitive = function naturalSortCaseInsensitive(
+ a,
+ b,
+ sessionString
+) {
+ return naturalSort(a, b, sessionString, true);
+};
diff --git a/devtools/shared/network-observer/ChannelMap.sys.mjs b/devtools/shared/network-observer/ChannelMap.sys.mjs
new file mode 100644
index 0000000000..3c54b1171a
--- /dev/null
+++ b/devtools/shared/network-observer/ChannelMap.sys.mjs
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * FinalizationRegistry callback, see
+ * https://searchfox.org/mozilla-central/source/js/src/builtin/FinalizationRegistryObject.h
+ *
+ * Will be invoked when the channel corresponding to the weak reference is
+ * "destroyed", at which point we can cleanup the corresponding entry in our
+ * regular map.
+ */
+function deleteIdFromRefMap({ refMap, id }) {
+ refMap.delete(id);
+}
+
+/**
+ * This object implements iterable weak map for HTTP channels tracked by
+ * the network observer.
+ *
+ * We can't use Map() for storing HTTP channel references since we don't
+ * know when we should remove the entry in it (it's wrong to do it in
+ * 'onTransactionClose' since it doesn't have to be the last platform
+ * notification for a given channel). We want the map to auto update
+ * when the channel is garbage collected.
+ *
+ * We can't use WeakMap() since searching for a value by the channel object
+ * isn't reliable (there might be different objects representing the same
+ * channel). We need to search by channel ID, but ID can't be used as key
+ * in WeakMap().
+ *
+ * So, this custom map solves aforementioned issues.
+ */
+export class ChannelMap {
+ #finalizationRegistry;
+ #refMap;
+ #weakMap;
+
+ constructor() {
+ // See https://searchfox.org/mozilla-central/source/js/src/builtin/FinalizationRegistryObject.h
+ this.#finalizationRegistry = new FinalizationRegistry(deleteIdFromRefMap);
+
+ // Map of channel id to a channel weak reference.
+ this.#refMap = new Map();
+
+ /**
+ * WeakMap from nsIChannel instances to objects which encapsulate ChannelMap
+ * values with the following structure:
+ * @property {Object} value
+ * The actual value stored in this ChannelMap entry, which should relate
+ * to this channel.
+ * @property {WeakRef} ref
+ * Weak reference for the channel object which is the key of the entry.
+ */
+ this.#weakMap = new WeakMap();
+ }
+
+ /**
+ * Remove all entries from the ChannelMap.
+ */
+ clear() {
+ this.#refMap.clear();
+ }
+
+ /**
+ * Delete the entry for the provided channel from the underlying maps, if any.
+ * Note that this will only delete entries which were set for the exact same
+ * nsIChannel object, and will not attempt to look up entries by channel id.
+ *
+ * @param {nsIChannel} channel
+ * The key to delete from the ChannelMap.
+ *
+ * @return {boolean}
+ * True if an entry was deleted, false otherwise.
+ */
+ delete(channel) {
+ const entry = this.#weakMap.get(channel);
+ if (!entry) {
+ return false;
+ }
+
+ this.#weakMap.delete(channel);
+ this.#refMap.delete(channel.channelId);
+ this.#finalizationRegistry.unregister(entry.ref);
+ return true;
+ }
+
+ /**
+ * Retrieve a value stored in the ChannelMap by the provided channel.
+ *
+ * @param {nsIChannel} channel
+ * The key to delete from the ChannelMap.
+ *
+ * @return {Object|null}
+ * The value held for the provided channel.
+ * Null if the channel did not match any known key.
+ */
+ get(channel) {
+ const ref = this.#refMap.get(channel.channelId);
+ const key = ref ? ref.deref() : null;
+ if (!key) {
+ return null;
+ }
+ const channelInfo = this.#weakMap.get(key);
+ return channelInfo ? channelInfo.value : null;
+ }
+
+ /**
+ * Adds or updates an entry in the ChannelMap for the provided channel.
+ *
+ * @param {nsIChannel} channel
+ * The key of the entry to add or update.
+ * @param {Object} value
+ * The value to add or update.
+ */
+ set(channel, value) {
+ const ref = new WeakRef(channel);
+ this.#weakMap.set(channel, { value, ref });
+ this.#refMap.set(channel.channelId, ref);
+ this.#finalizationRegistry.register(
+ channel,
+ {
+ refMap: this.#refMap,
+ id: channel.channelId,
+ },
+ ref
+ );
+ }
+}
diff --git a/devtools/shared/network-observer/NetworkAuthListener.sys.mjs b/devtools/shared/network-observer/NetworkAuthListener.sys.mjs
new file mode 100644
index 0000000000..2ab5517aa1
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkAuthListener.sys.mjs
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+/**
+ * This class is a simplified version from the AuthRequestor used by the
+ * WebExtensions codebase at:
+ * https://searchfox.org/mozilla-central/rev/fd2325f5b2a5be8f8f2acf9307285f2b7de06582/toolkit/components/extensions/webrequest/WebRequest.sys.mjs#434-579
+ *
+ * The NetworkAuthListener will monitor the provided channel and will invoke the
+ * owner's `onAuthPrompt` end point whenever an auth challenge is requested.
+ *
+ * The owner will receive several callbacks to proceed with the prompt:
+ * - cancelAuthPrompt(): cancel the authentication attempt
+ * - forwardAuthPrompt(): forward the auth prompt request to the next
+ * notification callback. If no other custom callback is set, this will
+ * typically lead to show the auth prompt dialog in the browser UI.
+ * - provideAuthCredentials(username, password): attempt to authenticate with
+ * the provided username and password.
+ *
+ * Please note that the request will be blocked until the consumer calls one of
+ * the callbacks listed above. Make sure to eventually unblock the request if
+ * you implement `onAuthPrompt`.
+ *
+ * @param {nsIChannel} channel
+ * The channel to monitor.
+ * @param {object} owner
+ * The owner object, expected to implement `onAuthPrompt`.
+ */
+export class NetworkAuthListener {
+ constructor(channel, owner) {
+ this.notificationCallbacks = channel.notificationCallbacks;
+ this.loadGroupCallbacks =
+ channel.loadGroup && channel.loadGroup.notificationCallbacks;
+ this.owner = owner;
+
+ // Setup the channel's notificationCallbacks to be handled by this instance.
+ channel.notificationCallbacks = this;
+ }
+
+ // See https://searchfox.org/mozilla-central/source/netwerk/base/nsIAuthPrompt2.idl
+ asyncPromptAuth(channel, callback, context, level, authInfo) {
+ const isProxy = !!(authInfo.flags & authInfo.AUTH_PROXY);
+ const cancelAuthPrompt = () => {
+ if (channel.canceled) {
+ return;
+ }
+
+ try {
+ callback.onAuthCancelled(context, false);
+ } catch (e) {
+ console.error(`NetworkAuthListener failed to cancel auth prompt ${e}`);
+ }
+ };
+
+ const forwardAuthPrompt = () => {
+ if (channel.canceled) {
+ return;
+ }
+
+ const prompt = this.#getForwardPrompt(isProxy);
+ prompt.asyncPromptAuth(channel, callback, context, level, authInfo);
+ };
+
+ const provideAuthCredentials = (username, password) => {
+ if (channel.canceled) {
+ return;
+ }
+
+ authInfo.username = username;
+ authInfo.password = password;
+ try {
+ callback.onAuthAvailable(context, authInfo);
+ } catch (e) {
+ console.error(
+ `NetworkAuthListener failed to provide auth credentials ${e}`
+ );
+ }
+ };
+
+ const authDetails = {
+ isProxy,
+ realm: authInfo.realm,
+ scheme: authInfo.authenticationScheme,
+ };
+ const authCallbacks = {
+ cancelAuthPrompt,
+ forwardAuthPrompt,
+ provideAuthCredentials,
+ };
+
+ // The auth callbacks may only be called asynchronously after this method
+ // successfully returned.
+ lazy.setTimeout(() => this.#notifyOwner(authDetails, authCallbacks), 1);
+
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: cancelAuthPrompt,
+ };
+ }
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsIAuthPrompt2)) {
+ return this;
+ }
+ try {
+ return this.notificationCallbacks.getInterface(iid);
+ } catch (e) {}
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+
+ // See https://searchfox.org/mozilla-central/source/netwerk/base/nsIAuthPromptProvider.idl
+ getAuthPrompt(reason, iid) {
+ // This should never get called without getInterface having been called first.
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this;
+ }
+ return this.#getForwardedInterface(Ci.nsIAuthPromptProvider).getAuthPrompt(
+ reason,
+ iid
+ );
+ }
+
+ // See https://searchfox.org/mozilla-central/source/netwerk/base/nsIAuthPrompt2.idl
+ promptAuth(channel, level, authInfo) {
+ this.#getForwardedInterface(Ci.nsIAuthPrompt2).promptAuth(
+ channel,
+ level,
+ authInfo
+ );
+ }
+
+ #getForwardedInterface(iid) {
+ try {
+ return this.notificationCallbacks.getInterface(iid);
+ } catch (e) {
+ return this.loadGroupCallbacks.getInterface(iid);
+ }
+ }
+
+ #getForwardPrompt(isProxy) {
+ const reason = isProxy
+ ? Ci.nsIAuthPromptProvider.PROMPT_PROXY
+ : Ci.nsIAuthPromptProvider.PROMPT_NORMAL;
+ for (const callbacks of [
+ this.notificationCallbacks,
+ this.loadGroupCallbacks,
+ ]) {
+ try {
+ return callbacks
+ .getInterface(Ci.nsIAuthPromptProvider)
+ .getAuthPrompt(reason, Ci.nsIAuthPrompt2);
+ } catch (e) {}
+ try {
+ return callbacks.getInterface(Ci.nsIAuthPrompt2);
+ } catch (e) {}
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+
+ #notifyOwner(authDetails, authCallbacks) {
+ if (typeof this.owner.onAuthPrompt == "function") {
+ this.owner.onAuthPrompt(authDetails, authCallbacks);
+ } else {
+ console.error(
+ "NetworkObserver owner enabled the auth prompt listener " +
+ "but does not implement 'onAuthPrompt'. " +
+ "Forwarding the auth prompt to the next notification callback."
+ );
+ authCallbacks.forwardAuthPrompt();
+ }
+ }
+}
+
+NetworkAuthListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIAuthPromptProvider",
+ "nsIAuthPrompt2",
+]);
diff --git a/devtools/shared/network-observer/NetworkHelper.sys.mjs b/devtools/shared/network-observer/NetworkHelper.sys.mjs
new file mode 100644
index 0000000000..f225e51e08
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkHelper.sys.mjs
@@ -0,0 +1,913 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2007, Parakey Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the
+ * following conditions are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer in the documentation and/or other
+ * materials provided with the distribution.
+ *
+ * * Neither the name of Parakey Inc. nor the names of its
+ * contributors may be used to endorse or promote products
+ * derived from this software without specific prior
+ * written permission of Parakey Inc.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Creator:
+ * Joe Hewitt
+ * Contributors
+ * John J. Barton (IBM Almaden)
+ * Jan Odvarko (Mozilla Corp.)
+ * Max Stepanov (Aptana Inc.)
+ * Rob Campbell (Mozilla Corp.)
+ * Hans Hillen (Paciello Group, Mozilla)
+ * Curtis Bartley (Mozilla Corp.)
+ * Mike Collins (IBM Almaden)
+ * Kevin Decker
+ * Mike Ratcliffe (Comartis AG)
+ * Hernan Rodríguez Colmeiro
+ * Austin Andrews
+ * Christoph Dorn
+ * Steven Roussey (AppCenter Inc, Network54)
+ * Mihai Sucan (Mozilla Corp.)
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DevToolsInfaillibleUtils:
+ "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs",
+
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+// It would make sense to put this in the above
+// ChromeUtils.defineESModuleGetters, but that doesn't seem to work.
+ChromeUtils.defineLazyGetter(lazy, "certDecoder", () => {
+ const { parse, pemToDER } = ChromeUtils.importESModule(
+ "chrome://global/content/certviewer/certDecoder.mjs"
+ );
+ return { parse, pemToDER };
+});
+
+// "Lax", "Strict" and "None" are special values of the SameSite cookie
+// attribute that should not be translated.
+const COOKIE_SAMESITE = {
+ LAX: "Lax",
+ STRICT: "Strict",
+ NONE: "None",
+};
+
+/**
+ * Helper object for networking stuff.
+ *
+ * Most of the following functions have been taken from the Firebug source. They
+ * have been modified to match the Firefox coding rules.
+ */
+export var NetworkHelper = {
+ /**
+ * Converts text with a given charset to unicode.
+ *
+ * @param string text
+ * Text to convert.
+ * @param string charset
+ * Charset to convert the text to.
+ * @returns string
+ * Converted text.
+ */
+ convertToUnicode(text, charset) {
+ // FIXME: We need to throw when text can't be converted e.g. the contents of
+ // an image. Until we have a way to do so with TextEncoder and TextDecoder
+ // we need to use nsIScriptableUnicodeConverter instead.
+ const conv = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ try {
+ conv.charset = charset || "UTF-8";
+ return conv.ConvertToUnicode(text);
+ } catch (ex) {
+ return text;
+ }
+ },
+
+ /**
+ * Reads all available bytes from stream and converts them to charset.
+ *
+ * @param nsIInputStream stream
+ * @param string charset
+ * @returns string
+ * UTF-16 encoded string based on the content of stream and charset.
+ */
+ readAndConvertFromStream(stream, charset) {
+ let text = null;
+ try {
+ text = lazy.NetUtil.readInputStreamToString(stream, stream.available());
+ return this.convertToUnicode(text, charset);
+ } catch (err) {
+ return text;
+ }
+ },
+
+ /**
+ * Reads the posted text from request.
+ *
+ * @param nsIHttpChannel request
+ * @param string charset
+ * The content document charset, used when reading the POSTed data.
+ * @returns string or null
+ * Returns the posted string if it was possible to read from request
+ * otherwise null.
+ */
+ readPostTextFromRequest(request, charset) {
+ if (request instanceof Ci.nsIUploadChannel) {
+ const iStream = request.uploadStream;
+
+ let isSeekableStream = false;
+ if (iStream instanceof Ci.nsISeekableStream) {
+ isSeekableStream = true;
+ }
+
+ let prevOffset;
+ if (isSeekableStream) {
+ prevOffset = iStream.tell();
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ }
+
+ // Read data from the stream.
+ const text = this.readAndConvertFromStream(iStream, charset);
+
+ // Seek locks the file, so seek to the beginning only if necko hasn't
+ // read it yet, since necko doesn't seek to 0 before reading (at lest
+ // not till 459384 is fixed).
+ if (isSeekableStream && prevOffset == 0) {
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ }
+ return text;
+ }
+ return null;
+ },
+
+ /**
+ * Reads the posted text from the page's cache.
+ *
+ * @param nsIDocShell docShell
+ * @param string charset
+ * @returns string or null
+ * Returns the posted string if it was possible to read from
+ * docShell otherwise null.
+ */
+ readPostTextFromPage(docShell, charset) {
+ const webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ return this.readPostTextFromPageViaWebNav(webNav, charset);
+ },
+
+ /**
+ * Reads the posted text from the page's cache, given an nsIWebNavigation
+ * object.
+ *
+ * @param nsIWebNavigation webNav
+ * @param string charset
+ * @returns string or null
+ * Returns the posted string if it was possible to read from
+ * webNav, otherwise null.
+ */
+ readPostTextFromPageViaWebNav(webNav, charset) {
+ if (webNav instanceof Ci.nsIWebPageDescriptor) {
+ const descriptor = webNav.currentDescriptor;
+
+ if (
+ descriptor instanceof Ci.nsISHEntry &&
+ descriptor.postData &&
+ descriptor instanceof Ci.nsISeekableStream
+ ) {
+ descriptor.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+
+ return this.readAndConvertFromStream(descriptor, charset);
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Gets the topFrameElement that is associated with request. This
+ * works in single-process and multiprocess contexts. It may cross
+ * the content/chrome boundary.
+ *
+ * @param nsIHttpChannel request
+ * @returns Element|null
+ * The top frame element for the given request.
+ */
+ getTopFrameForRequest(request) {
+ try {
+ return this.getRequestLoadContext(request).topFrameElement;
+ } catch (ex) {
+ // request loadContext is not always available.
+ }
+ return null;
+ },
+
+ /**
+ * Gets the nsIDOMWindow that is associated with request.
+ *
+ * @param nsIHttpChannel request
+ * @returns nsIDOMWindow or null
+ */
+ getWindowForRequest(request) {
+ try {
+ return this.getRequestLoadContext(request).associatedWindow;
+ } catch (ex) {
+ // On some request notificationCallbacks and loadGroup are both null,
+ // so that we can't retrieve any nsILoadContext interface.
+ // Fallback on nsILoadInfo to try to retrieve the request's window.
+ // (this is covered by test_network_get.html and its CSS request)
+ return request.loadInfo.loadingDocument?.defaultView;
+ }
+ },
+
+ /**
+ * Gets the nsILoadContext that is associated with request.
+ *
+ * @param nsIHttpChannel request
+ * @returns nsILoadContext or null
+ */
+ getRequestLoadContext(request) {
+ try {
+ if (request.loadInfo.workerAssociatedBrowsingContext) {
+ return request.loadInfo.workerAssociatedBrowsingContext;
+ }
+ } catch (ex) {
+ // Ignore.
+ }
+ try {
+ return request.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) {
+ // Ignore.
+ }
+
+ try {
+ return request.loadGroup.notificationCallbacks.getInterface(
+ Ci.nsILoadContext
+ );
+ } catch (ex) {
+ // Ignore.
+ }
+
+ return null;
+ },
+
+ /**
+ * Determines whether the request has been made for the top level document.
+ *
+ * @param nsIHttpChannel request
+ * @returns Boolean True if the request represents the top level document.
+ */
+ isTopLevelLoad(request) {
+ if (request instanceof Ci.nsIChannel) {
+ const loadInfo = request.loadInfo;
+ if (loadInfo?.isTopLevelLoad) {
+ return request.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Loads the content of url from the cache.
+ *
+ * @param string url
+ * URL to load the cached content for.
+ * @param string charset
+ * Assumed charset of the cached content. Used if there is no charset
+ * on the channel directly.
+ * @param function callback
+ * Callback that is called with the loaded cached content if available
+ * or null if something failed while getting the cached content.
+ */
+ loadFromCache(url, charset, callback) {
+ const channel = lazy.NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Ensure that we only read from the cache and not the server.
+ channel.loadFlags =
+ Ci.nsIRequest.LOAD_FROM_CACHE |
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
+ Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
+
+ lazy.NetUtil.asyncFetch(channel, (inputStream, statusCode, request) => {
+ if (!Components.isSuccessCode(statusCode)) {
+ callback(null);
+ return;
+ }
+
+ // Try to get the encoding from the channel. If there is none, then use
+ // the passed assumed charset.
+ const requestChannel = request.QueryInterface(Ci.nsIChannel);
+ const contentCharset = requestChannel.contentCharset || charset;
+
+ // Read the content of the stream using contentCharset as encoding.
+ callback(this.readAndConvertFromStream(inputStream, contentCharset));
+ });
+ },
+
+ /**
+ * Parse a raw Cookie header value.
+ *
+ * @param string header
+ * The raw Cookie header value.
+ * @return array
+ * Array holding an object for each cookie. Each object holds the
+ * following properties: name and value.
+ */
+ parseCookieHeader(header) {
+ const cookies = header.split(";");
+ const result = [];
+
+ cookies.forEach(function (cookie) {
+ const equal = cookie.indexOf("=");
+ const name = cookie.substr(0, equal);
+ const value = cookie.substr(equal + 1);
+ result.push({
+ name: unescape(name.trim()),
+ value: unescape(value.trim()),
+ });
+ });
+
+ return result;
+ },
+
+ /**
+ * Parse a raw Set-Cookie header value.
+ *
+ * @param array headers
+ * Array of raw Set-Cookie header values.
+ * @return array
+ * Array holding an object for each cookie. Each object holds the
+ * following properties: name, value, secure (boolean), httpOnly
+ * (boolean), path, domain, samesite and expires (ISO date string).
+ */
+ parseSetCookieHeaders(headers) {
+ function parseSameSiteAttribute(attribute) {
+ attribute = attribute.toLowerCase();
+ switch (attribute) {
+ case COOKIE_SAMESITE.LAX.toLowerCase():
+ return COOKIE_SAMESITE.LAX;
+ case COOKIE_SAMESITE.STRICT.toLowerCase():
+ return COOKIE_SAMESITE.STRICT;
+ default:
+ return COOKIE_SAMESITE.NONE;
+ }
+ }
+
+ const cookies = [];
+
+ for (const header of headers) {
+ const rawCookies = header.split(/\r\n|\n|\r/);
+
+ rawCookies.forEach(function (cookie) {
+ const equal = cookie.indexOf("=");
+ const name = unescape(cookie.substr(0, equal).trim());
+ const parts = cookie.substr(equal + 1).split(";");
+ const value = unescape(parts.shift().trim());
+
+ cookie = { name, value };
+
+ parts.forEach(function (part) {
+ part = part.trim();
+ if (part.toLowerCase() == "secure") {
+ cookie.secure = true;
+ } else if (part.toLowerCase() == "httponly") {
+ cookie.httpOnly = true;
+ } else if (part.indexOf("=") > -1) {
+ const pair = part.split("=");
+ pair[0] = pair[0].toLowerCase();
+ if (pair[0] == "path" || pair[0] == "domain") {
+ cookie[pair[0]] = pair[1];
+ } else if (pair[0] == "samesite") {
+ cookie[pair[0]] = parseSameSiteAttribute(pair[1]);
+ } else if (pair[0] == "expires") {
+ try {
+ pair[1] = pair[1].replace(/-/g, " ");
+ cookie.expires = new Date(pair[1]).toISOString();
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ }
+ });
+
+ cookies.push(cookie);
+ });
+ }
+
+ return cookies;
+ },
+
+ // This is a list of all the mime category maps jviereck could find in the
+ // firebug code base.
+ mimeCategoryMap: {
+ "text/plain": "txt",
+ "text/html": "html",
+ "text/xml": "xml",
+ "text/xsl": "txt",
+ "text/xul": "txt",
+ "text/css": "css",
+ "text/sgml": "txt",
+ "text/rtf": "txt",
+ "text/x-setext": "txt",
+ "text/richtext": "txt",
+ "text/javascript": "js",
+ "text/jscript": "txt",
+ "text/tab-separated-values": "txt",
+ "text/rdf": "txt",
+ "text/xif": "txt",
+ "text/ecmascript": "js",
+ "text/vnd.curl": "txt",
+ "text/x-json": "json",
+ "text/x-js": "txt",
+ "text/js": "txt",
+ "text/vbscript": "txt",
+ "view-source": "txt",
+ "view-fragment": "txt",
+ "application/xml": "xml",
+ "application/xhtml+xml": "xml",
+ "application/atom+xml": "xml",
+ "application/rss+xml": "xml",
+ "application/vnd.mozilla.maybe.feed": "xml",
+ "application/javascript": "js",
+ "application/x-javascript": "js",
+ "application/x-httpd-php": "txt",
+ "application/rdf+xml": "xml",
+ "application/ecmascript": "js",
+ "application/http-index-format": "txt",
+ "application/json": "json",
+ "application/x-js": "txt",
+ "application/x-mpegurl": "txt",
+ "application/vnd.apple.mpegurl": "txt",
+ "multipart/mixed": "txt",
+ "multipart/x-mixed-replace": "txt",
+ "image/svg+xml": "svg",
+ "application/octet-stream": "bin",
+ "image/jpeg": "image",
+ "image/jpg": "image",
+ "image/gif": "image",
+ "image/png": "image",
+ "image/bmp": "image",
+ "application/x-shockwave-flash": "flash",
+ "video/x-flv": "flash",
+ "audio/mpeg3": "media",
+ "audio/x-mpeg-3": "media",
+ "video/mpeg": "media",
+ "video/x-mpeg": "media",
+ "video/vnd.mpeg.dash.mpd": "xml",
+ "audio/ogg": "media",
+ "application/ogg": "media",
+ "application/x-ogg": "media",
+ "application/x-midi": "media",
+ "audio/midi": "media",
+ "audio/x-mid": "media",
+ "audio/x-midi": "media",
+ "music/crescendo": "media",
+ "audio/wav": "media",
+ "audio/x-wav": "media",
+ "text/json": "json",
+ "application/x-json": "json",
+ "application/json-rpc": "json",
+ "application/x-web-app-manifest+json": "json",
+ "application/manifest+json": "json",
+ },
+
+ /**
+ * Check if the given MIME type is a text-only MIME type.
+ *
+ * @param string mimeType
+ * @return boolean
+ */
+ isTextMimeType(mimeType) {
+ if (mimeType.indexOf("text/") == 0) {
+ return true;
+ }
+
+ // XML and JSON often come with custom MIME types, so in addition to the
+ // standard "application/xml" and "application/json", we also look for
+ // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and
+ // "-json" as suffixes.
+ if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(mimeType)) {
+ return true;
+ }
+
+ const category = this.mimeCategoryMap[mimeType] || null;
+ switch (category) {
+ case "txt":
+ case "js":
+ case "json":
+ case "css":
+ case "html":
+ case "svg":
+ case "xml":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+
+ /**
+ * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
+ * extracts security information from them.
+ *
+ * @param object securityInfo
+ * The securityInfo object of a request. If null channel is assumed
+ * to be insecure.
+ * @param object originAttributes
+ * The OriginAttributes of the request.
+ * @param object httpActivity
+ * The httpActivity object for the request with at least members
+ * { private, hostname }.
+ * @param Map decodedCertificateCache
+ * A Map of certificate fingerprints to decoded certificates, to avoid
+ * repeatedly decoding previously-seen certificates.
+ *
+ * @return object
+ * Returns an object containing following members:
+ * - state: The security of the connection used to fetch this
+ * request. Has one of following string values:
+ * * "insecure": the connection was not secure (only http)
+ * * "weak": the connection has minor security issues
+ * * "broken": secure connection failed (e.g. expired cert)
+ * * "secure": the connection was properly secured.
+ * If state == broken:
+ * - errorMessage: error code string.
+ * If state == secure:
+ * - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3.
+ * - cipherSuite: the cipher suite used in this connection.
+ * - cert: information about certificate used in this connection.
+ * See parseCertificateInfo for the contents.
+ * - hsts: true if host uses Strict Transport Security,
+ * false otherwise
+ * - hpkp: true if host uses Public Key Pinning, false otherwise
+ * If state == weak: Same as state == secure and
+ * - weaknessReasons: list of reasons that cause the request to be
+ * considered weak. See getReasonsForWeakness.
+ */
+ async parseSecurityInfo(
+ securityInfo,
+ originAttributes,
+ httpActivity,
+ decodedCertificateCache
+ ) {
+ const info = {
+ state: "insecure",
+ };
+
+ // The request did not contain any security info.
+ if (!securityInfo) {
+ return info;
+ }
+
+ /**
+ * Different scenarios to consider here and how they are handled:
+ * - request is HTTP, the connection is not secure
+ * => securityInfo is null
+ * => state === "insecure"
+ *
+ * - request is HTTPS, the connection is secure
+ * => .securityState has STATE_IS_SECURE flag
+ * => state === "secure"
+ *
+ * - request is HTTPS, the connection has security issues
+ * => .securityState has STATE_IS_INSECURE flag
+ * => .errorCode is an NSS error code.
+ * => state === "broken"
+ *
+ * - request is HTTPS, the connection was terminated before the security
+ * could be validated
+ * => .securityState has STATE_IS_INSECURE flag
+ * => .errorCode is NOT an NSS error code.
+ * => .errorMessage is not available.
+ * => state === "insecure"
+ *
+ * - request is HTTPS but it uses a weak cipher or old protocol, see
+ * https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
+ * security/manager/ssl/nsNSSCallbacks.cpp#l1233
+ * - request is mixed content (which makes no sense whatsoever)
+ * => .securityState has STATE_IS_BROKEN flag
+ * => .errorCode is NOT an NSS error code
+ * => .errorMessage is not available
+ * => state === "weak"
+ */
+
+ const wpl = Ci.nsIWebProgressListener;
+ const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+ if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+ const state = securityInfo.securityState;
+
+ let uri = null;
+ if (httpActivity.channel?.URI) {
+ uri = httpActivity.channel.URI;
+ }
+ if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
+ // it is not enough to look at the transport security info -
+ // schemes other than https and wss are subject to
+ // downgrade/etc at the scheme level and should always be
+ // considered insecure
+ info.state = "insecure";
+ } else if (state & wpl.STATE_IS_SECURE) {
+ // The connection is secure if the scheme is sufficient
+ info.state = "secure";
+ } else if (state & wpl.STATE_IS_BROKEN) {
+ // The connection is not secure, there was no error but there's some
+ // minor security issues.
+ info.state = "weak";
+ info.weaknessReasons = this.getReasonsForWeakness(state);
+ } else if (state & wpl.STATE_IS_INSECURE) {
+ // This was most likely an https request that was aborted before
+ // validation. Return info as info.state = insecure.
+ return info;
+ } else {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.parseSecurityInfo",
+ "Security state " + state + " has no known STATE_IS_* flags."
+ );
+ return info;
+ }
+
+ // Cipher suite.
+ info.cipherSuite = securityInfo.cipherName;
+
+ // Key exchange group name.
+ info.keaGroupName = securityInfo.keaGroupName;
+
+ // Certificate signature scheme.
+ info.signatureSchemeName = securityInfo.signatureSchemeName;
+
+ // Protocol version.
+ info.protocolVersion = this.formatSecurityProtocol(
+ securityInfo.protocolVersion
+ );
+
+ // Certificate.
+ info.cert = await this.parseCertificateInfo(
+ securityInfo.serverCert,
+ decodedCertificateCache
+ );
+
+ // Certificate transparency status.
+ info.certificateTransparency = securityInfo.certificateTransparencyStatus;
+
+ // HSTS and HPKP if available.
+ if (httpActivity.hostname) {
+ const sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ const pkps = Cc[
+ "@mozilla.org/security/publickeypinningservice;1"
+ ].getService(Ci.nsIPublicKeyPinningService);
+
+ if (!uri) {
+ // isSecureURI only cares about the host, not the scheme.
+ const host = httpActivity.hostname;
+ uri = Services.io.newURI("https://" + host);
+ }
+
+ info.hsts = sss.isSecureURI(uri, originAttributes);
+ info.hpkp = pkps.hostHasPins(uri);
+ } else {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.parseSecurityInfo",
+ "Could not get HSTS/HPKP status as hostname is not available."
+ );
+ info.hsts = false;
+ info.hpkp = false;
+ }
+ } else {
+ // The connection failed.
+ info.state = "broken";
+ info.errorMessage = securityInfo.errorCodeString;
+ }
+
+ // These values can be unset in rare cases, e.g. when stashed connection
+ // data is deseralized from an older version of Firefox.
+ try {
+ info.usedEch = securityInfo.isAcceptedEch;
+ } catch {
+ info.usedEch = false;
+ }
+ try {
+ info.usedDelegatedCredentials = securityInfo.isDelegatedCredential;
+ } catch {
+ info.usedDelegatedCredentials = false;
+ }
+ info.usedOcsp = securityInfo.madeOCSPRequests;
+ info.usedPrivateDns = securityInfo.usedPrivateDNS;
+
+ return info;
+ },
+
+ /**
+ * Takes an nsIX509Cert and returns an object with certificate information.
+ *
+ * @param nsIX509Cert cert
+ * The certificate to extract the information from.
+ * @param Map decodedCertificateCache
+ * A Map of certificate fingerprints to decoded certificates, to avoid
+ * repeatedly decoding previously-seen certificates.
+ * @return object
+ * An object with following format:
+ * {
+ * subject: { commonName, organization, organizationalUnit },
+ * issuer: { commonName, organization, organizationUnit },
+ * validity: { start, end },
+ * fingerprint: { sha1, sha256 }
+ * }
+ */
+ async parseCertificateInfo(cert, decodedCertificateCache) {
+ function getDNComponent(dn, componentType) {
+ for (const [type, value] of dn.entries) {
+ if (type == componentType) {
+ return value;
+ }
+ }
+ return undefined;
+ }
+
+ const info = {};
+ if (cert) {
+ const certHash = cert.sha256Fingerprint;
+ let parsedCert = decodedCertificateCache.get(certHash);
+ if (!parsedCert) {
+ parsedCert = await lazy.certDecoder.parse(
+ lazy.certDecoder.pemToDER(cert.getBase64DERString())
+ );
+ decodedCertificateCache.set(certHash, parsedCert);
+ }
+ info.subject = {
+ commonName: getDNComponent(parsedCert.subject, "Common Name"),
+ organization: getDNComponent(parsedCert.subject, "Organization"),
+ organizationalUnit: getDNComponent(
+ parsedCert.subject,
+ "Organizational Unit"
+ ),
+ };
+
+ info.issuer = {
+ commonName: getDNComponent(parsedCert.issuer, "Common Name"),
+ organization: getDNComponent(parsedCert.issuer, "Organization"),
+ organizationUnit: getDNComponent(
+ parsedCert.issuer,
+ "Organizational Unit"
+ ),
+ };
+
+ info.validity = {
+ start: parsedCert.notBeforeUTC,
+ end: parsedCert.notAfterUTC,
+ };
+
+ info.fingerprint = {
+ sha1: parsedCert.fingerprint.sha1,
+ sha256: parsedCert.fingerprint.sha256,
+ };
+ } else {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.parseCertificateInfo",
+ "Secure connection established without certificate."
+ );
+ }
+
+ return info;
+ },
+
+ /**
+ * Takes protocolVersion of TransportSecurityInfo object and returns
+ * human readable description.
+ *
+ * @param Number version
+ * One of nsITransportSecurityInfo version constants.
+ * @return string
+ * One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if @param version
+ * is valid, Unknown otherwise.
+ */
+ formatSecurityProtocol(version) {
+ switch (version) {
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
+ return "TLSv1";
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
+ return "TLSv1.1";
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
+ return "TLSv1.2";
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
+ return "TLSv1.3";
+ default:
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.formatSecurityProtocol",
+ "protocolVersion " + version + " is unknown."
+ );
+ return "Unknown";
+ }
+ },
+
+ /**
+ * Takes the securityState bitfield and returns reasons for weak connection
+ * as an array of strings.
+ *
+ * @param Number state
+ * nsITransportSecurityInfo.securityState.
+ *
+ * @return Array[String]
+ * List of weakness reasons. A subset of { cipher } where
+ * * cipher: The cipher suite is consireded to be weak (RC4).
+ */
+ getReasonsForWeakness(state) {
+ const wpl = Ci.nsIWebProgressListener;
+
+ // If there's non-fatal security issues the request has STATE_IS_BROKEN
+ // flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119
+ // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
+ const reasons = [];
+
+ if (state & wpl.STATE_IS_BROKEN) {
+ const isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;
+
+ if (isCipher) {
+ reasons.push("cipher");
+ }
+
+ if (!isCipher) {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.getReasonsForWeakness",
+ "STATE_IS_BROKEN without a known reason. Full state was: " + state
+ );
+ }
+ }
+
+ return reasons;
+ },
+
+ /**
+ * Parse a url's query string into its components
+ *
+ * @param string queryString
+ * The query part of a url
+ * @return array
+ * Array of query params {name, value}
+ */
+ parseQueryString(queryString) {
+ // Make sure there's at least one param available.
+ // Be careful here, params don't necessarily need to have values, so
+ // no need to verify the existence of a "=".
+ if (!queryString) {
+ return null;
+ }
+
+ // Turn the params string into an array containing { name: value } tuples.
+ const paramsArray = queryString
+ .replace(/^[?&]/, "")
+ .split("&")
+ .map(e => {
+ const param = e.split("=");
+ return {
+ name: param[0]
+ ? NetworkHelper.convertToUnicode(unescape(param[0]))
+ : "",
+ value: param[1]
+ ? NetworkHelper.convertToUnicode(unescape(param[1]))
+ : "",
+ };
+ });
+
+ return paramsArray;
+ },
+};
diff --git a/devtools/shared/network-observer/NetworkObserver.sys.mjs b/devtools/shared/network-observer/NetworkObserver.sys.mjs
new file mode 100644
index 0000000000..35e66c9d5b
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkObserver.sys.mjs
@@ -0,0 +1,1532 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * NetworkObserver is the main class in DevTools to observe network requests
+ * out of many events fired by the platform code.
+ */
+
+// Enable logging all platform events this module listen to
+const DEBUG_PLATFORM_EVENTS = false;
+// Enables defining criteria to filter the logs
+const DEBUG_PLATFORM_EVENTS_FILTER = (eventName, channel) => {
+ // e.g return eventName == "HTTP_TRANSACTION:REQUEST_HEADER" && channel.URI.spec == "http://foo.com";
+ return true;
+};
+
+const lazy = {};
+
+import { DevToolsInfaillibleUtils } from "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs";
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ChannelMap: "resource://devtools/shared/network-observer/ChannelMap.sys.mjs",
+ NetworkAuthListener:
+ "resource://devtools/shared/network-observer/NetworkAuthListener.sys.mjs",
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+ NetworkOverride:
+ "resource://devtools/shared/network-observer/NetworkOverride.sys.mjs",
+ NetworkResponseListener:
+ "resource://devtools/shared/network-observer/NetworkResponseListener.sys.mjs",
+ NetworkThrottleManager:
+ "resource://devtools/shared/network-observer/NetworkThrottleManager.sys.mjs",
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+ wildcardToRegExp:
+ "resource://devtools/shared/network-observer/WildcardToRegexp.sys.mjs",
+});
+
+const gActivityDistributor = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+].getService(Ci.nsIHttpActivityDistributor);
+
+function logPlatformEvent(eventName, channel, message = "") {
+ if (!DEBUG_PLATFORM_EVENTS) {
+ return;
+ }
+ if (DEBUG_PLATFORM_EVENTS_FILTER(eventName, channel)) {
+ dump(
+ `[netmonitor] ${channel.channelId} - ${eventName} ${message} - ${channel.URI.spec}\n`
+ );
+ }
+}
+
+// The maximum uint32 value.
+const PR_UINT32_MAX = 4294967295;
+
+const HTTP_TRANSACTION_CODES = {
+ 0x5001: "REQUEST_HEADER",
+ 0x5002: "REQUEST_BODY_SENT",
+ 0x5003: "RESPONSE_START",
+ 0x5004: "RESPONSE_HEADER",
+ 0x5005: "RESPONSE_COMPLETE",
+ 0x5006: "TRANSACTION_CLOSE",
+
+ 0x4b0003: "STATUS_RESOLVING",
+ 0x4b000b: "STATUS_RESOLVED",
+ 0x4b0007: "STATUS_CONNECTING_TO",
+ 0x4b0004: "STATUS_CONNECTED_TO",
+ 0x4b0005: "STATUS_SENDING_TO",
+ 0x4b000a: "STATUS_WAITING_FOR",
+ 0x4b0006: "STATUS_RECEIVING_FROM",
+ 0x4b000c: "STATUS_TLS_STARTING",
+ 0x4b000d: "STATUS_TLS_ENDING",
+};
+
+const HTTP_DOWNLOAD_ACTIVITIES = [
+ gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START,
+ gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER,
+ gActivityDistributor.ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER,
+ gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE,
+ gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE,
+];
+
+/**
+ * The network monitor uses the nsIHttpActivityDistributor to monitor network
+ * requests. The nsIObserverService is also used for monitoring
+ * http-on-examine-response notifications. All network request information is
+ * routed to the remote Web Console.
+ *
+ * @constructor
+ * @param {Object} options
+ * @param {Function(nsIChannel): boolean} options.ignoreChannelFunction
+ * This function will be called for every detected channel to decide if it
+ * should be monitored or not.
+ * @param {Function(NetworkEvent): owner} options.onNetworkEvent
+ * This method is invoked once for every new network request with two
+ * arguments:
+ * - {Object} networkEvent: object created by NetworkUtils:createNetworkEvent,
+ * containing initial network request information as an argument.
+ * - {nsIChannel} channel: the channel for which the request was detected
+ *
+ * `onNetworkEvent()` must return an "owner" object which holds several add*()
+ * methods which are used to add further network request/response information.
+ */
+export class NetworkObserver {
+ /**
+ * Map of URL patterns to RegExp
+ *
+ * @type {Map}
+ */
+ #blockedURLs = new Map();
+
+ /**
+ * Map of URL to local file path in order to redirect URL
+ * to local file overrides.
+ *
+ * This will replace the content of some request with the content of local files.
+ */
+ #overrides = new Map();
+
+ /**
+ * Used by NetworkHelper.parseSecurityInfo to skip decoding known certificates.
+ *
+ * @type {Map}
+ */
+ #decodedCertificateCache = new Map();
+ /**
+ * Whether the consumer supports listening and handling auth prompts.
+ *
+ * @type {boolean}
+ */
+ #authPromptListenerEnabled = false;
+ /**
+ * See constructor argument of the same name.
+ *
+ * @type {Function}
+ */
+ #ignoreChannelFunction;
+ /**
+ * Used to store channels intercepted for service-worker requests.
+ *
+ * @type {WeakSet}
+ */
+ #interceptedChannels = new WeakSet();
+ /**
+ * Explicit flag to check if this observer was already destroyed.
+ *
+ * @type {boolean}
+ */
+ #isDestroyed = false;
+ /**
+ * See constructor argument of the same name.
+ *
+ * @type {Function}
+ */
+ #onNetworkEvent;
+ /**
+ * Object that holds the activity objects for ongoing requests.
+ *
+ * @type {ChannelMap}
+ */
+ #openRequests = new lazy.ChannelMap();
+ /**
+ * Network response bodies are piped through a buffer of the given size
+ * (in bytes).
+ *
+ * @type {Number}
+ */
+ #responsePipeSegmentSize = Services.prefs.getIntPref(
+ "network.buffer.cache.size"
+ );
+ /**
+ * Whether to save the bodies of network requests and responses.
+ *
+ * @type {boolean}
+ */
+ #saveRequestAndResponseBodies = true;
+ /**
+ * Throttling configuration, see constructor of NetworkThrottleManager
+ *
+ * @type {Object}
+ */
+ #throttleData = null;
+ /**
+ * NetworkThrottleManager instance, created when a valid throttleData is set.
+ * @type {NetworkThrottleManager}
+ */
+ #throttler = null;
+
+ constructor(options = {}) {
+ const { ignoreChannelFunction, onNetworkEvent } = options;
+ if (typeof ignoreChannelFunction !== "function") {
+ throw new Error(
+ `Expected "ignoreChannelFunction" to be a function, got ${ignoreChannelFunction} (${typeof ignoreChannelFunction})`
+ );
+ }
+
+ if (typeof onNetworkEvent !== "function") {
+ throw new Error(
+ `Expected "onNetworkEvent" to be a function, got ${onNetworkEvent} (${typeof onNetworkEvent})`
+ );
+ }
+
+ this.#ignoreChannelFunction = ignoreChannelFunction;
+ this.#onNetworkEvent = onNetworkEvent;
+
+ // Start all platform observers.
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ gActivityDistributor.addObserver(this);
+ gActivityDistributor.observeProxyResponse = true;
+
+ Services.obs.addObserver(
+ this.#httpResponseExaminer,
+ "http-on-examine-response"
+ );
+ Services.obs.addObserver(
+ this.#httpResponseExaminer,
+ "http-on-examine-cached-response"
+ );
+ Services.obs.addObserver(
+ this.#httpModifyExaminer,
+ "http-on-modify-request"
+ );
+ Services.obs.addObserver(
+ this.#fileChannelExaminer,
+ "file-channel-opened"
+ );
+ Services.obs.addObserver(this.#httpStopRequest, "http-on-stop-request");
+ } else {
+ Services.obs.addObserver(
+ this.#httpFailedOpening,
+ "http-on-failed-opening-request"
+ );
+ }
+ // In child processes, only watch for service worker requests
+ // everything else only happens in the parent process
+ Services.obs.addObserver(
+ this.#serviceWorkerRequest,
+ "service-worker-synthesized-response"
+ );
+ }
+
+ setAuthPromptListenerEnabled(enabled) {
+ this.#authPromptListenerEnabled = enabled;
+ }
+
+ setSaveRequestAndResponseBodies(save) {
+ this.#saveRequestAndResponseBodies = save;
+ }
+
+ getThrottleData() {
+ return this.#throttleData;
+ }
+
+ setThrottleData(value) {
+ this.#throttleData = value;
+ // Clear out any existing throttlers
+ this.#throttler = null;
+ }
+
+ #getThrottler() {
+ if (this.#throttleData !== null && this.#throttler === null) {
+ this.#throttler = new lazy.NetworkThrottleManager(this.#throttleData);
+ }
+ return this.#throttler;
+ }
+
+ #serviceWorkerRequest = DevToolsInfaillibleUtils.makeInfallible(
+ (subject, topic, data) => {
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+
+ logPlatformEvent(topic, channel);
+
+ this.#interceptedChannels.add(subject);
+
+ // Service workers never fire http-on-examine-cached-response, so fake one.
+ this.#httpResponseExaminer(channel, "http-on-examine-cached-response");
+ }
+ );
+
+ /**
+ * Observes for http-on-failed-opening-request notification to catch any
+ * channels for which asyncOpen has synchronously failed. This is the only
+ * place to catch early security check failures.
+ */
+ #httpFailedOpening = DevToolsInfaillibleUtils.makeInfallible(
+ (subject, topic) => {
+ if (
+ this.#isDestroyed ||
+ topic != "http-on-failed-opening-request" ||
+ !(subject instanceof Ci.nsIHttpChannel)
+ ) {
+ return;
+ }
+
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+
+ logPlatformEvent(topic, channel);
+
+ // Ignore preload requests to avoid duplicity request entries in
+ // the Network panel. If a preload fails (for whatever reason)
+ // then the platform kicks off another 'real' request.
+ if (lazy.NetworkUtils.isPreloadRequest(channel)) {
+ return;
+ }
+
+ this.#httpResponseExaminer(subject, topic);
+ }
+ );
+
+ #httpStopRequest = DevToolsInfaillibleUtils.makeInfallible(
+ (subject, topic) => {
+ if (
+ this.#isDestroyed ||
+ topic != "http-on-stop-request" ||
+ !(subject instanceof Ci.nsIHttpChannel)
+ ) {
+ return;
+ }
+
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+
+ logPlatformEvent(topic, channel);
+
+ const httpActivity = this.#createOrGetActivityObject(channel);
+ const serverTimings = this.#extractServerTimings(channel);
+
+ if (httpActivity.owner) {
+ // Try extracting server timings. Note that they will be sent to the client
+ // in the `_onTransactionClose` method together with network event timings.
+ httpActivity.owner.addServerTimings(serverTimings);
+
+ // If the owner isn't set we need to create the network event and send
+ // it to the client. This happens in case where:
+ // - the request has been blocked (e.g. CORS) and "http-on-stop-request" is the first notification.
+ // - the NetworkObserver is start *after* the request started and we only receive the http-stop notification,
+ // but that doesn't mean the request is blocked, so check for its status.
+ } else if (Components.isSuccessCode(channel.status)) {
+ // Do not pass any blocked reason, as this request is just fine.
+ // Bug 1489217 - Prevent watching for this request response content,
+ // as this request is already running, this is too late to watch for it.
+ this.#createNetworkEvent(subject, { inProgressRequest: true });
+ } else {
+ // Handles any early blockings e.g by Web Extensions or by CORS
+ const { blockingExtension, blockedReason } =
+ lazy.NetworkUtils.getBlockedReason(channel, httpActivity.fromCache);
+ this.#createNetworkEvent(subject, { blockedReason, blockingExtension });
+ }
+ }
+ );
+
+ /**
+ * Check if the current channel has its content being overriden
+ * by the content of some local file.
+ */
+ #checkForContentOverride(channel) {
+ const overridePath = this.#overrides.get(channel.URI.spec);
+ if (!overridePath) {
+ return false;
+ }
+
+ dump(" Override " + channel.URI.spec + " to " + overridePath + "\n");
+ try {
+ lazy.NetworkOverride.overrideChannelWithFilePath(channel, overridePath);
+ } catch (e) {
+ dump("Exception while trying to override request content: " + e + "\n");
+ }
+
+ return true;
+ }
+
+ /**
+ * Observe notifications for the http-on-examine-response topic, coming from
+ * the nsIObserverService.
+ *
+ * @private
+ * @param nsIHttpChannel subject
+ * @param string topic
+ * @returns void
+ */
+ #httpResponseExaminer = DevToolsInfaillibleUtils.makeInfallible(
+ (subject, topic) => {
+ // The httpResponseExaminer is used to retrieve the uncached response
+ // headers.
+ if (
+ this.#isDestroyed ||
+ (topic != "http-on-examine-response" &&
+ topic != "http-on-examine-cached-response" &&
+ topic != "http-on-failed-opening-request") ||
+ !(subject instanceof Ci.nsIHttpChannel) ||
+ !(subject instanceof Ci.nsIClassifiedChannel)
+ ) {
+ return;
+ }
+
+ const blockedOrFailed = topic === "http-on-failed-opening-request";
+
+ subject.QueryInterface(Ci.nsIClassifiedChannel);
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+
+ logPlatformEvent(
+ topic,
+ subject,
+ blockedOrFailed
+ ? "blockedOrFailed:" + channel.loadInfo.requestBlockingReason
+ : channel.responseStatus
+ );
+
+ this.#checkForContentOverride(channel);
+
+ channel.QueryInterface(Ci.nsIHttpChannelInternal);
+
+ let httpActivity = this.#createOrGetActivityObject(channel);
+ if (topic === "http-on-examine-cached-response") {
+ // Service worker requests emits cached-response notification on non-e10s,
+ // and we fake one on e10s.
+ const fromServiceWorker = this.#interceptedChannels.has(channel);
+ this.#interceptedChannels.delete(channel);
+
+ // If this is a cached response (which are also emitted by service worker requests),
+ // there never was a request event so we need to construct one here
+ // so the frontend gets all the expected events.
+ if (!httpActivity.owner) {
+ httpActivity = this.#createNetworkEvent(channel, {
+ fromCache: !fromServiceWorker,
+ fromServiceWorker,
+ });
+ }
+
+ // We need to send the request body to the frontend for
+ // the faked (cached/service worker request) event.
+ this.#prepareRequestBody(httpActivity);
+ this.#sendRequestBody(httpActivity);
+
+ // There also is never any timing events, so we can fire this
+ // event with zeroed out values.
+ const timings = this.#setupHarTimings(httpActivity);
+ const serverTimings = this.#extractServerTimings(httpActivity.channel);
+ const serviceWorkerTimings =
+ this.#extractServiceWorkerTimings(httpActivity);
+
+ httpActivity.owner.addServerTimings(serverTimings);
+ httpActivity.owner.addServiceWorkerTimings(serviceWorkerTimings);
+ httpActivity.owner.addEventTimings(
+ timings.total,
+ timings.timings,
+ timings.offsets
+ );
+ } else if (topic === "http-on-failed-opening-request") {
+ const { blockedReason } = lazy.NetworkUtils.getBlockedReason(
+ channel,
+ httpActivity.fromCache
+ );
+ this.#createNetworkEvent(channel, { blockedReason });
+ }
+
+ if (httpActivity.owner) {
+ httpActivity.owner.addResponseStart({
+ channel: httpActivity.channel,
+ fromCache: httpActivity.fromCache || httpActivity.fromServiceWorker,
+ rawHeaders: httpActivity.responseRawHeaders,
+ proxyResponseRawHeaders: httpActivity.proxyResponseRawHeaders,
+ });
+ }
+ }
+ );
+
+ /**
+ * Observe notifications for the http-on-modify-request topic, coming from
+ * the nsIObserverService.
+ *
+ * @private
+ * @param nsIHttpChannel aSubject
+ * @returns void
+ */
+ #httpModifyExaminer = DevToolsInfaillibleUtils.makeInfallible(subject => {
+ const throttler = this.#getThrottler();
+ if (throttler) {
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+ logPlatformEvent("http-on-modify-request", channel);
+
+ // Read any request body here, before it is throttled.
+ const httpActivity = this.#createOrGetActivityObject(channel);
+ this.#prepareRequestBody(httpActivity);
+ throttler.manageUpload(channel);
+ }
+ });
+
+ /**
+ * Observe notifications for the file-channel-opened topic
+ *
+ * @private
+ * @param nsIFileChannel subject
+ * @param string topic
+ * @returns void
+ */
+ #fileChannelExaminer = DevToolsInfaillibleUtils.makeInfallible(
+ (subject, topic) => {
+ if (
+ this.#isDestroyed ||
+ topic != "file-channel-opened" ||
+ !(subject instanceof Ci.nsIFileChannel)
+ ) {
+ return;
+ }
+ const channel = subject.QueryInterface(Ci.nsIFileChannel);
+ channel.QueryInterface(Ci.nsIIdentChannel);
+ channel.QueryInterface(Ci.nsIChannel);
+
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+
+ logPlatformEvent(topic, channel);
+
+ const fileActivity = this.#createOrGetActivityObject(channel);
+
+ this.#createNetworkEvent(subject, {});
+
+ if (fileActivity.owner) {
+ fileActivity.owner.addResponseStart({
+ channel: fileActivity.channel,
+ fromCache: fileActivity.fromCache || fileActivity.fromServiceWorker,
+ rawHeaders: fileActivity.responseRawHeaders,
+ proxyResponseRawHeaders: fileActivity.proxyResponseRawHeaders,
+ });
+ }
+ }
+ );
+
+ /**
+ * A helper function for observeActivity. This does whatever work
+ * is required by a particular http activity event. Arguments are
+ * the same as for observeActivity.
+ */
+ #dispatchActivity(
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ timestamp,
+ extraSizeData,
+ extraStringData
+ ) {
+ // Store the time information for this activity subtype.
+ if (activitySubtype in HTTP_TRANSACTION_CODES) {
+ const stage = HTTP_TRANSACTION_CODES[activitySubtype];
+ if (stage in httpActivity.timings) {
+ httpActivity.timings[stage].last = timestamp;
+ } else {
+ httpActivity.timings[stage] = {
+ first: timestamp,
+ last: timestamp,
+ };
+ }
+ }
+
+ switch (activitySubtype) {
+ case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
+ this.#prepareRequestBody(httpActivity);
+ this.#sendRequestBody(httpActivity);
+ break;
+ case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
+ httpActivity.responseRawHeaders = extraStringData;
+ httpActivity.headersSize = extraStringData.length;
+ break;
+ case gActivityDistributor.ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER:
+ httpActivity.proxyResponseRawHeaders = extraStringData;
+ break;
+ case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
+ this.#onTransactionClose(httpActivity);
+ break;
+ default:
+ break;
+ }
+ }
+
+ getActivityTypeString(activityType, activitySubtype) {
+ if (
+ activityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_SOCKET_TRANSPORT
+ ) {
+ for (const name in Ci.nsISocketTransport) {
+ if (Ci.nsISocketTransport[name] === activitySubtype) {
+ return "SOCKET_TRANSPORT:" + name;
+ }
+ }
+ } else if (
+ activityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION
+ ) {
+ for (const name in Ci.nsIHttpActivityObserver) {
+ if (Ci.nsIHttpActivityObserver[name] === activitySubtype) {
+ return "HTTP_TRANSACTION:" + name.replace("ACTIVITY_SUBTYPE_", "");
+ }
+ }
+ }
+ return "unexpected-activity-types:" + activityType + ":" + activitySubtype;
+ }
+
+ /**
+ * Begin observing HTTP traffic that originates inside the current tab.
+ *
+ * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver
+ *
+ * @param nsIHttpChannel channel
+ * @param number activityType
+ * @param number activitySubtype
+ * @param number timestamp
+ * @param number extraSizeData
+ * @param string extraStringData
+ */
+ observeActivity = DevToolsInfaillibleUtils.makeInfallible(function (
+ channel,
+ activityType,
+ activitySubtype,
+ timestamp,
+ extraSizeData,
+ extraStringData
+ ) {
+ if (
+ this.#isDestroyed ||
+ (activityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION &&
+ activityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT)
+ ) {
+ return;
+ }
+
+ if (
+ !(channel instanceof Ci.nsIHttpChannel) ||
+ !(channel instanceof Ci.nsIClassifiedChannel)
+ ) {
+ return;
+ }
+
+ channel = channel.QueryInterface(Ci.nsIHttpChannel);
+ channel = channel.QueryInterface(Ci.nsIClassifiedChannel);
+
+ if (DEBUG_PLATFORM_EVENTS) {
+ logPlatformEvent(
+ this.getActivityTypeString(activityType, activitySubtype),
+ channel
+ );
+ }
+
+ if (
+ activitySubtype == gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER
+ ) {
+ this.#onRequestHeader(channel, timestamp, extraStringData);
+ return;
+ }
+
+ // Iterate over all currently ongoing requests. If channel can't
+ // be found within them, then exit this function.
+ const httpActivity = this.#findActivityObject(channel);
+ if (!httpActivity) {
+ return;
+ }
+
+ // If we're throttling, we must not report events as they arrive
+ // from platform, but instead let the throttler emit the events
+ // after some time has elapsed.
+ if (
+ httpActivity.downloadThrottle &&
+ HTTP_DOWNLOAD_ACTIVITIES.includes(activitySubtype)
+ ) {
+ const callback = this.#dispatchActivity.bind(this);
+ httpActivity.downloadThrottle.addActivityCallback(
+ callback,
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ timestamp,
+ extraSizeData,
+ extraStringData
+ );
+ } else {
+ this.#dispatchActivity(
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ timestamp,
+ extraSizeData,
+ extraStringData
+ );
+ }
+ });
+
+ /**
+ * Craft the "event" object passed to the Watcher class in order
+ * to instantiate the NetworkEventActor.
+ *
+ * /!\ This method does many other important things:
+ * - Cancel requests blocked by DevTools
+ * - Fetch request headers/cookies
+ * - Set a few attributes on http activity object
+ * - Set a few attributes on file activity object
+ * - Register listener to record response content
+ */
+ #createNetworkEvent(
+ channel,
+ {
+ timestamp,
+ rawHeaders,
+ fromCache,
+ fromServiceWorker,
+ blockedReason,
+ blockingExtension,
+ inProgressRequest,
+ }
+ ) {
+ if (channel instanceof Ci.nsIFileChannel) {
+ const fileActivity = this.#createOrGetActivityObject(channel);
+
+ if (timestamp) {
+ fileActivity.timings.REQUEST_HEADER = {
+ first: timestamp,
+ last: timestamp,
+ };
+ }
+
+ fileActivity.owner = this.#onNetworkEvent({}, channel);
+
+ return fileActivity;
+ }
+
+ const httpActivity = this.#createOrGetActivityObject(channel);
+
+ if (timestamp) {
+ httpActivity.timings.REQUEST_HEADER = {
+ first: timestamp,
+ last: timestamp,
+ };
+ }
+
+ if (blockedReason === undefined && this.#shouldBlockChannel(channel)) {
+ // Check the request URL with ones manually blocked by the user in DevTools.
+ // If it's meant to be blocked, we cancel the request and annotate the event.
+ channel.cancel(Cr.NS_BINDING_ABORTED);
+ blockedReason = "devtools";
+ }
+
+ httpActivity.owner = this.#onNetworkEvent(
+ {
+ timestamp,
+ fromCache,
+ fromServiceWorker,
+ rawHeaders,
+ blockedReason,
+ blockingExtension,
+ discardRequestBody: !this.#saveRequestAndResponseBodies,
+ discardResponseBody: !this.#saveRequestAndResponseBodies,
+ },
+ channel
+ );
+ httpActivity.fromCache = fromCache;
+ httpActivity.fromServiceWorker = fromServiceWorker;
+
+ // Bug 1489217 - Avoid watching for response content for blocked or in-progress requests
+ // as it can't be observed and would throw if we try.
+ if (blockedReason === undefined && !inProgressRequest) {
+ this.#setupResponseListener(httpActivity, {
+ fromCache,
+ fromServiceWorker,
+ });
+ }
+
+ if (this.#authPromptListenerEnabled) {
+ new lazy.NetworkAuthListener(httpActivity.channel, httpActivity.owner);
+ }
+
+ return httpActivity;
+ }
+
+ /**
+ * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the
+ * headers are sent to the server. This method creates the |httpActivity|
+ * object where we store the request and response information that is
+ * collected through its lifetime.
+ *
+ * @private
+ * @param nsIHttpChannel channel
+ * @param number timestamp
+ * @param string rawHeaders
+ * @return void
+ */
+ #onRequestHeader(channel, timestamp, rawHeaders) {
+ if (this.#ignoreChannelFunction(channel)) {
+ return;
+ }
+
+ this.#createNetworkEvent(channel, {
+ timestamp,
+ rawHeaders,
+ });
+ }
+
+ /**
+ * Check if the provided channel should be blocked given the current
+ * blocked URLs configured for this network observer.
+ */
+ #shouldBlockChannel(channel) {
+ for (const regexp of this.#blockedURLs.values()) {
+ if (regexp.test(channel.URI.spec)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Find an HTTP activity object for the channel.
+ *
+ * @param nsIHttpChannel channel
+ * The HTTP channel whose activity object we want to find.
+ * @return object
+ * The HTTP activity object, or null if it is not found.
+ */
+ #findActivityObject(channel) {
+ return this.#openRequests.get(channel);
+ }
+
+ /**
+ * Find an existing activity object, or create a new one. This
+ * object is used for storing all the request and response
+ * information.
+ *
+ * This is a HAR-like object. Conformance to the spec is not guaranteed at
+ * this point.
+ *
+ * @see http://www.softwareishard.com/blog/har-12-spec
+ * @param {(nsIHttpChannel|nsIFileChannel)} channel
+ * The HTTP channel for which the HTTP activity object is created.
+ * @return object
+ * The new HTTP activity object.
+ */
+ #createOrGetActivityObject(channel) {
+ let activity = this.#findActivityObject(channel);
+ if (!activity) {
+ const isHttpChannel = channel instanceof Ci.nsIHttpChannel;
+
+ if (isHttpChannel) {
+ // Most of the data needed from the channel is only available via the
+ // nsIHttpChannelInternal interface.
+ channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ } else {
+ channel.QueryInterface(Ci.nsIChannel);
+ }
+
+ activity = {
+ // The nsIChannel for which this activity object was created.
+ channel,
+ // See #prepareRequestBody()
+ charset: isHttpChannel ? lazy.NetworkUtils.getCharset(channel) : null,
+ // The postData sent by this request.
+ sentBody: null,
+ // The URL for the current channel.
+ url: channel.URI.spec,
+ // The encoded response body size.
+ bodySize: 0,
+ // The response headers size.
+ headersSize: 0,
+ // needed for host specific security info but file urls do not have hostname
+ hostname: isHttpChannel ? channel.URI.host : null,
+ discardRequestBody: isHttpChannel
+ ? !this.#saveRequestAndResponseBodies
+ : false,
+ discardResponseBody: isHttpChannel
+ ? !this.#saveRequestAndResponseBodies
+ : false,
+ // internal timing information, see observeActivity()
+ timings: {},
+ // the activity owner which is notified when changes happen
+ owner: null,
+ };
+
+ this.#openRequests.set(channel, activity);
+ }
+
+ return activity;
+ }
+
+ /**
+ * Block a request based on certain filtering options.
+ *
+ * Currently, exact URL match or URL patterns are supported.
+ */
+ blockRequest(filter) {
+ if (!filter || !filter.url) {
+ // In the future, there may be other types of filters, such as domain.
+ // For now, ignore anything other than URL.
+ return;
+ }
+
+ this.#addBlockedUrl(filter.url);
+ }
+
+ /**
+ * Unblock a request based on certain filtering options.
+ *
+ * Currently, exact URL match or URL patterns are supported.
+ */
+ unblockRequest(filter) {
+ if (!filter || !filter.url) {
+ // In the future, there may be other types of filters, such as domain.
+ // For now, ignore anything other than URL.
+ return;
+ }
+
+ this.#blockedURLs.delete(filter.url);
+ }
+
+ /**
+ * Updates the list of blocked request strings
+ *
+ * This match will be a (String).includes match, not an exact URL match
+ */
+ setBlockedUrls(urls) {
+ urls = urls || [];
+ this.#blockedURLs = new Map();
+ urls.forEach(url => this.#addBlockedUrl(url));
+ }
+
+ #addBlockedUrl(url) {
+ this.#blockedURLs.set(url, lazy.wildcardToRegExp(url));
+ }
+
+ /**
+ * Returns a list of blocked requests
+ * Useful as blockedURLs is mutated by both console & netmonitor
+ */
+ getBlockedUrls() {
+ return this.#blockedURLs.keys();
+ }
+
+ override(url, path) {
+ this.#overrides.set(url, path);
+ }
+
+ removeOverride(url) {
+ this.#overrides.delete(url);
+ }
+
+ /**
+ * Setup the network response listener for the given HTTP activity. The
+ * NetworkResponseListener is responsible for storing the response body.
+ *
+ * @private
+ * @param object httpActivity
+ * The HTTP activity object we are tracking.
+ */
+ #setupResponseListener(httpActivity, { fromCache, fromServiceWorker }) {
+ const channel = httpActivity.channel;
+ channel.QueryInterface(Ci.nsITraceableChannel);
+
+ if (!fromCache) {
+ const throttler = this.#getThrottler();
+ if (throttler) {
+ httpActivity.downloadThrottle = throttler.manage(channel);
+ }
+ }
+
+ // The response will be written into the outputStream of this pipe.
+ // This allows us to buffer the data we are receiving and read it
+ // asynchronously.
+ // Both ends of the pipe must be blocking.
+ const sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+
+ // The streams need to be blocking because this is required by the
+ // stream tee.
+ sink.init(false, false, this.#responsePipeSegmentSize, PR_UINT32_MAX, null);
+
+ // Add listener for the response body.
+ const newListener = new lazy.NetworkResponseListener(
+ httpActivity,
+ this.#decodedCertificateCache,
+ fromServiceWorker
+ );
+
+ // Remember the input stream, so it isn't released by GC.
+ newListener.inputStream = sink.inputStream;
+ newListener.sink = sink;
+
+ const tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+
+ const originalListener = channel.setNewListener(tee);
+
+ tee.init(originalListener, sink.outputStream, newListener);
+ }
+
+ /**
+ * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. Read and record the request
+ * body here. It will be available in addResponseStart.
+ *
+ * @private
+ * @param object httpActivity
+ * The HTTP activity object we are working with.
+ */
+ #prepareRequestBody(httpActivity) {
+ // Return early if we don't need the request body, or if we've
+ // already found it.
+ if (httpActivity.discardRequestBody || httpActivity.sentBody !== null) {
+ return;
+ }
+
+ let sentBody = lazy.NetworkHelper.readPostTextFromRequest(
+ httpActivity.channel,
+ httpActivity.charset
+ );
+
+ if (
+ sentBody !== null &&
+ this.window &&
+ httpActivity.url == this.window.location.href
+ ) {
+ // If the request URL is the same as the current page URL, then
+ // we can try to get the posted text from the page directly.
+ // This check is necessary as otherwise the
+ // lazy.NetworkHelper.readPostTextFromPageViaWebNav()
+ // function is called for image requests as well but these
+ // are not web pages and as such don't store the posted text
+ // in the cache of the webpage.
+ const webNav = this.window.docShell.QueryInterface(Ci.nsIWebNavigation);
+ sentBody = lazy.NetworkHelper.readPostTextFromPageViaWebNav(
+ webNav,
+ httpActivity.charset
+ );
+ }
+
+ if (sentBody !== null) {
+ httpActivity.sentBody = sentBody;
+ }
+ }
+
+ /**
+ * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR
+ * timing information on the HTTP activity object and clears the request
+ * from the list of known open requests.
+ *
+ * @private
+ * @param object httpActivity
+ * The HTTP activity object we work with.
+ */
+ #onTransactionClose(httpActivity) {
+ if (httpActivity.owner) {
+ const result = this.#setupHarTimings(httpActivity);
+ const serverTimings = this.#extractServerTimings(httpActivity.channel);
+
+ httpActivity.owner.addServerTimings(serverTimings);
+ httpActivity.owner.addEventTimings(
+ result.total,
+ result.timings,
+ result.offsets
+ );
+ }
+ }
+
+ #getBlockedTiming(timings) {
+ if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) {
+ return timings.STATUS_RESOLVING.first - timings.REQUEST_HEADER.first;
+ } else if (timings.STATUS_SENDING_TO) {
+ return timings.STATUS_SENDING_TO.first - timings.REQUEST_HEADER.first;
+ }
+
+ return -1;
+ }
+
+ #getDnsTiming(timings) {
+ if (timings.STATUS_RESOLVING && timings.STATUS_RESOLVED) {
+ return timings.STATUS_RESOLVED.last - timings.STATUS_RESOLVING.first;
+ }
+
+ return -1;
+ }
+
+ #getConnectTiming(timings) {
+ if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) {
+ return (
+ timings.STATUS_CONNECTED_TO.last - timings.STATUS_CONNECTING_TO.first
+ );
+ }
+
+ return -1;
+ }
+
+ #getReceiveTiming(timings) {
+ if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) {
+ return timings.RESPONSE_COMPLETE.last - timings.RESPONSE_START.first;
+ }
+
+ return -1;
+ }
+
+ #getWaitTiming(timings) {
+ if (timings.RESPONSE_START) {
+ return (
+ timings.RESPONSE_START.first -
+ (timings.REQUEST_BODY_SENT || timings.STATUS_SENDING_TO).last
+ );
+ }
+
+ return -1;
+ }
+
+ #getSslTiming(timings) {
+ if (timings.STATUS_TLS_STARTING && timings.STATUS_TLS_ENDING) {
+ return timings.STATUS_TLS_ENDING.last - timings.STATUS_TLS_STARTING.first;
+ }
+
+ return -1;
+ }
+
+ #getSendTiming(timings) {
+ if (timings.STATUS_SENDING_TO) {
+ return timings.STATUS_SENDING_TO.last - timings.STATUS_SENDING_TO.first;
+ } else if (timings.REQUEST_HEADER && timings.REQUEST_BODY_SENT) {
+ return timings.REQUEST_BODY_SENT.last - timings.REQUEST_HEADER.first;
+ }
+
+ return -1;
+ }
+
+ #getDataFromTimedChannel(timedChannel) {
+ const lookUpArr = [
+ "tcpConnectEndTime",
+ "connectStartTime",
+ "connectEndTime",
+ "secureConnectionStartTime",
+ "domainLookupEndTime",
+ "domainLookupStartTime",
+ ];
+
+ return lookUpArr.reduce((prev, prop) => {
+ const propName = prop + "Tc";
+ return {
+ ...prev,
+ [propName]: (() => {
+ if (!timedChannel) {
+ return 0;
+ }
+
+ const value = timedChannel[prop];
+
+ if (
+ value != 0 &&
+ timedChannel.asyncOpenTime &&
+ value < timedChannel.asyncOpenTime
+ ) {
+ return 0;
+ }
+
+ return value;
+ })(),
+ };
+ }, {});
+ }
+
+ #getSecureConnectionStartTimeInfo(timings) {
+ let secureConnectionStartTime = 0;
+ let secureConnectionStartTimeRelative = false;
+
+ if (timings.STATUS_TLS_STARTING && timings.STATUS_TLS_ENDING) {
+ if (timings.STATUS_CONNECTING_TO) {
+ secureConnectionStartTime =
+ timings.STATUS_TLS_STARTING.first -
+ timings.STATUS_CONNECTING_TO.first;
+ }
+
+ if (secureConnectionStartTime < 0) {
+ secureConnectionStartTime = 0;
+ }
+ secureConnectionStartTimeRelative = true;
+ }
+
+ return {
+ secureConnectionStartTime,
+ secureConnectionStartTimeRelative,
+ };
+ }
+
+ #getStartSendingTimeInfo(timings, connectStartTimeTc) {
+ let startSendingTime = 0;
+ let startSendingTimeRelative = false;
+
+ if (timings.STATUS_SENDING_TO) {
+ if (timings.STATUS_CONNECTING_TO) {
+ startSendingTime =
+ timings.STATUS_SENDING_TO.first - timings.STATUS_CONNECTING_TO.first;
+ startSendingTimeRelative = true;
+ } else if (connectStartTimeTc != 0) {
+ startSendingTime = timings.STATUS_SENDING_TO.first - connectStartTimeTc;
+ startSendingTimeRelative = true;
+ }
+
+ if (startSendingTime < 0) {
+ startSendingTime = 0;
+ }
+ }
+ return { startSendingTime, startSendingTimeRelative };
+ }
+
+ /**
+ * Update the HTTP activity object to include timing information as in the HAR
+ * spec. The HTTP activity object holds the raw timing information in
+ * |timings| - these are timings stored for each activity notification. The
+ * HAR timing information is constructed based on these lower level
+ * data.
+ *
+ * @param {Object} httpActivity
+ * The HTTP activity object we are working with.
+ * @return {Object}
+ * This object holds three properties:
+ * - {Object} offsets: the timings computed as offsets from the initial
+ * request start time.
+ * - {Object} timings: the HAR timings object
+ * - {number} total: the total time for all of the request and response
+ */
+ #setupHarTimings(httpActivity) {
+ if (httpActivity.fromCache) {
+ // If it came from the browser cache, we have no timing
+ // information and these should all be 0
+ return {
+ total: 0,
+ timings: {
+ blocked: 0,
+ dns: 0,
+ ssl: 0,
+ connect: 0,
+ send: 0,
+ wait: 0,
+ receive: 0,
+ },
+ offsets: {
+ blocked: 0,
+ dns: 0,
+ ssl: 0,
+ connect: 0,
+ send: 0,
+ wait: 0,
+ receive: 0,
+ },
+ };
+ }
+
+ const timings = httpActivity.timings;
+ const harTimings = {};
+ // If the TCP Fast Open option or tls1.3 0RTT is used tls and data can
+ // be dispatched in SYN packet and not after tcp socket is connected.
+ // To demostrate this properly we will calculated TLS and send start time
+ // relative to CONNECTING_TO.
+ // Similary if 0RTT is used, data can be sent as soon as a TLS handshake
+ // starts.
+
+ harTimings.blocked = this.#getBlockedTiming(timings);
+ // DNS timing information is available only in when the DNS record is not
+ // cached.
+ harTimings.dns = this.#getDnsTiming(timings);
+ harTimings.connect = this.#getConnectTiming(timings);
+ harTimings.ssl = this.#getSslTiming(timings);
+
+ let { secureConnectionStartTime, secureConnectionStartTimeRelative } =
+ this.#getSecureConnectionStartTimeInfo(timings);
+
+ // sometimes the connection information events are attached to a speculative
+ // channel instead of this one, but necko might glue them back together in the
+ // nsITimedChannel interface used by Resource and Navigation Timing
+ const timedChannel = httpActivity.channel.QueryInterface(
+ Ci.nsITimedChannel
+ );
+
+ const {
+ tcpConnectEndTimeTc,
+ connectStartTimeTc,
+ connectEndTimeTc,
+ secureConnectionStartTimeTc,
+ domainLookupEndTimeTc,
+ domainLookupStartTimeTc,
+ } = this.#getDataFromTimedChannel(timedChannel);
+
+ if (
+ harTimings.connect <= 0 &&
+ timedChannel &&
+ tcpConnectEndTimeTc != 0 &&
+ connectStartTimeTc != 0
+ ) {
+ harTimings.connect = tcpConnectEndTimeTc - connectStartTimeTc;
+ if (secureConnectionStartTimeTc != 0) {
+ harTimings.ssl = connectEndTimeTc - secureConnectionStartTimeTc;
+ secureConnectionStartTime =
+ secureConnectionStartTimeTc - connectStartTimeTc;
+ secureConnectionStartTimeRelative = true;
+ } else {
+ harTimings.ssl = -1;
+ }
+ } else if (
+ timedChannel &&
+ timings.STATUS_TLS_STARTING &&
+ secureConnectionStartTimeTc != 0
+ ) {
+ // It can happen that TCP Fast Open actually have not sent any data and
+ // timings.STATUS_TLS_STARTING.first value will be corrected in
+ // timedChannel.secureConnectionStartTime
+ if (secureConnectionStartTimeTc > timings.STATUS_TLS_STARTING.first) {
+ // TCP Fast Open actually did not sent any data.
+ harTimings.ssl = connectEndTimeTc - secureConnectionStartTimeTc;
+ secureConnectionStartTimeRelative = false;
+ }
+ }
+
+ if (
+ harTimings.dns <= 0 &&
+ timedChannel &&
+ domainLookupEndTimeTc != 0 &&
+ domainLookupStartTimeTc != 0
+ ) {
+ harTimings.dns = domainLookupEndTimeTc - domainLookupStartTimeTc;
+ }
+
+ harTimings.send = this.#getSendTiming(timings);
+ harTimings.wait = this.#getWaitTiming(timings);
+ harTimings.receive = this.#getReceiveTiming(timings);
+ let { startSendingTime, startSendingTimeRelative } =
+ this.#getStartSendingTimeInfo(timings, connectStartTimeTc);
+
+ if (secureConnectionStartTimeRelative) {
+ const time = Math.max(Math.round(secureConnectionStartTime / 1000), -1);
+ secureConnectionStartTime = time;
+ }
+ if (startSendingTimeRelative) {
+ const time = Math.max(Math.round(startSendingTime / 1000), -1);
+ startSendingTime = time;
+ }
+
+ const ot = this.#calculateOffsetAndTotalTime(
+ harTimings,
+ secureConnectionStartTime,
+ startSendingTimeRelative,
+ secureConnectionStartTimeRelative,
+ startSendingTime
+ );
+ return {
+ total: ot.total,
+ timings: harTimings,
+ offsets: ot.offsets,
+ };
+ }
+
+ #extractServerTimings(channel) {
+ if (!channel || !channel.serverTiming) {
+ return null;
+ }
+
+ const serverTimings = new Array(channel.serverTiming.length);
+
+ for (let i = 0; i < channel.serverTiming.length; ++i) {
+ const { name, duration, description } =
+ channel.serverTiming.queryElementAt(i, Ci.nsIServerTiming);
+ serverTimings[i] = { name, duration, description };
+ }
+
+ return serverTimings;
+ }
+
+ #extractServiceWorkerTimings({ fromServiceWorker, channel }) {
+ if (!fromServiceWorker) {
+ return null;
+ }
+ const timedChannel = channel.QueryInterface(Ci.nsITimedChannel);
+
+ return {
+ launchServiceWorker:
+ timedChannel.launchServiceWorkerEndTime -
+ timedChannel.launchServiceWorkerStartTime,
+ requestToServiceWorker:
+ timedChannel.dispatchFetchEventEndTime -
+ timedChannel.dispatchFetchEventStartTime,
+ handledByServiceWorker:
+ timedChannel.handleFetchEventEndTime -
+ timedChannel.handleFetchEventStartTime,
+ };
+ }
+
+ #convertTimeToMs(timing) {
+ return Math.max(Math.round(timing / 1000), -1);
+ }
+
+ #calculateOffsetAndTotalTime(
+ harTimings,
+ secureConnectionStartTime,
+ startSendingTimeRelative,
+ secureConnectionStartTimeRelative,
+ startSendingTime
+ ) {
+ let totalTime = 0;
+ for (const timing in harTimings) {
+ const time = this.#convertTimeToMs(harTimings[timing]);
+ harTimings[timing] = time;
+ if (time > -1 && timing != "connect" && timing != "ssl") {
+ totalTime += time;
+ }
+ }
+
+ // connect, ssl and send times can be overlapped.
+ if (startSendingTimeRelative) {
+ totalTime += startSendingTime;
+ } else if (secureConnectionStartTimeRelative) {
+ totalTime += secureConnectionStartTime;
+ totalTime += harTimings.ssl;
+ }
+
+ const offsets = {};
+ offsets.blocked = 0;
+ offsets.dns = harTimings.blocked;
+ offsets.connect = offsets.dns + harTimings.dns;
+ if (secureConnectionStartTimeRelative) {
+ offsets.ssl = offsets.connect + secureConnectionStartTime;
+ } else {
+ offsets.ssl = offsets.connect + harTimings.connect;
+ }
+ if (startSendingTimeRelative) {
+ offsets.send = offsets.connect + startSendingTime;
+ if (!secureConnectionStartTimeRelative) {
+ offsets.ssl = offsets.send - harTimings.ssl;
+ }
+ } else {
+ offsets.send = offsets.ssl + harTimings.ssl;
+ }
+ offsets.wait = offsets.send + harTimings.send;
+ offsets.receive = offsets.wait + harTimings.wait;
+
+ return {
+ total: totalTime,
+ offsets,
+ };
+ }
+
+ #sendRequestBody(httpActivity) {
+ if (httpActivity.sentBody !== null) {
+ const limit = Services.prefs.getIntPref(
+ "devtools.netmonitor.requestBodyLimit"
+ );
+ const size = httpActivity.sentBody.length;
+ if (size > limit && limit > 0) {
+ httpActivity.sentBody = httpActivity.sentBody.substr(0, limit);
+ }
+ httpActivity.owner.addRequestPostData({
+ text: httpActivity.sentBody,
+ size,
+ });
+ httpActivity.sentBody = null;
+ }
+ }
+
+ /*
+ * Clears the open requests channel map.
+ */
+ clear() {
+ this.#openRequests.clear();
+ }
+
+ /**
+ * Suspend observer activity. This is called when the Network monitor actor stops
+ * listening.
+ */
+ destroy() {
+ if (this.#isDestroyed) {
+ return;
+ }
+
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ gActivityDistributor.removeObserver(this);
+ Services.obs.removeObserver(
+ this.#httpResponseExaminer,
+ "http-on-examine-response"
+ );
+ Services.obs.removeObserver(
+ this.#httpResponseExaminer,
+ "http-on-examine-cached-response"
+ );
+ Services.obs.removeObserver(
+ this.#httpModifyExaminer,
+ "http-on-modify-request"
+ );
+ Services.obs.removeObserver(
+ this.#fileChannelExaminer,
+ "file-channel-opened"
+ );
+ Services.obs.removeObserver(
+ this.#httpStopRequest,
+ "http-on-stop-request"
+ );
+ } else {
+ Services.obs.removeObserver(
+ this.#httpFailedOpening,
+ "http-on-failed-opening-request"
+ );
+ }
+
+ Services.obs.removeObserver(
+ this.#serviceWorkerRequest,
+ "service-worker-synthesized-response"
+ );
+
+ this.#ignoreChannelFunction = null;
+ this.#onNetworkEvent = null;
+ this.#throttler = null;
+ this.#decodedCertificateCache.clear();
+ this.clear();
+
+ this.#isDestroyed = true;
+ }
+}
diff --git a/devtools/shared/network-observer/NetworkOverride.sys.mjs b/devtools/shared/network-observer/NetworkOverride.sys.mjs
new file mode 100644
index 0000000000..1b9ef6c873
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkOverride.sys.mjs
@@ -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/. */
+
+/**
+ * This modules focuses on redirecting requests to a particular local file.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "mimeService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+
+function readFile(file) {
+ const fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ const data = NetUtil.readInputStreamToString(fstream, fstream.available());
+ fstream.close();
+ return data;
+}
+
+/**
+ * Given an in-flight channel, we will force to replace the content of this request
+ * with the content of a local file.
+ *
+ * @param {nsIHttpChannel} channel
+ * The request to replace content for.
+ * @param {String} path
+ * The absolute path to the local file to read content from.
+ */
+function overrideChannelWithFilePath(channel, path) {
+ // For JS it isn't important, but for HTML we ought to set the right content type on the data URI.
+ let mimeType = "";
+ try {
+ // getTypeFromURI will throw if there is no extension at the end of the URI
+ mimeType = lazy.mimeService.getTypeFromURI(channel.URI);
+ } catch (e) {}
+
+ // Redirect to a data: URI as we can't redirect to file:// URI
+ // without many security issues. We are leveraging the `allowInsecureRedirectToDataURI`
+ // attribute used by WebExtension.
+ const file = lazy.FileUtils.File(path);
+ const data = readFile(file);
+ const redirectURI = Services.io.newURI(
+ `data:${mimeType};base64,${btoa(data)}`
+ );
+
+ channel.redirectTo(redirectURI);
+
+ // Prevents having CORS exception and various issues because of redirecting to data: URI.
+ channel.loadInfo.allowInsecureRedirectToDataURI = true;
+}
+
+export const NetworkOverride = {
+ overrideChannelWithFilePath,
+};
diff --git a/devtools/shared/network-observer/NetworkResponseListener.sys.mjs b/devtools/shared/network-observer/NetworkResponseListener.sys.mjs
new file mode 100644
index 0000000000..642773c8b2
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkResponseListener.sys.mjs
@@ -0,0 +1,608 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+ getResponseCacheObject:
+ "resource://devtools/shared/platform/CacheEntry.sys.mjs",
+});
+
+// Network logging
+
+/**
+ * The network response listener implements the nsIStreamListener and
+ * nsIRequestObserver interfaces. This is used within the NetworkObserver feature
+ * to get the response body of the request.
+ *
+ * The code is mostly based on code listings from:
+ *
+ * http://www.softwareishard.com/blog/firebug/
+ * nsitraceablechannel-intercept-http-traffic/
+ *
+ * @constructor
+ * @param {Object} httpActivity
+ * HttpActivity object associated with this request. See NetworkObserver
+ * more information.
+ * @param {Map} decodedCertificateCache
+ * A Map of certificate fingerprints to decoded certificates, to avoid
+ * repeatedly decoding previously-seen certificates.
+ */
+export class NetworkResponseListener {
+ /**
+ * The compressed and encoded response body size. Will progressively increase
+ * until the full response is received.
+ *
+ * @type {Number}
+ */
+ #bodySize = 0;
+ /**
+ * The uncompressed, decoded response body size.
+ *
+ * @type {Number}
+ */
+ #decodedBodySize = 0;
+ /**
+ * nsIStreamListener created by nsIStreamConverterService.asyncConvertData
+ *
+ * @type {nsIStreamListener}
+ */
+ #converter = null;
+ /**
+ * See constructor argument of the same name.
+ *
+ * @type {Map}
+ */
+ #decodedCertificateCache;
+ /**
+ * Is the channel from a service worker
+ *
+ * @type {boolean}
+ */
+ #fromServiceWorker;
+ /**
+ * See constructor argument of the same name.
+ *
+ * @type {Object}
+ */
+ #httpActivity;
+ /**
+ * Set from sink.inputStream, mainly to prevent GC.
+ *
+ * @type {nsIInputStream}
+ */
+ #inputStream = null;
+ /**
+ * Explicit flag to check if this listener was already destroyed.
+ *
+ * @type {boolean}
+ */
+ #isDestroyed = false;
+ /**
+ * Internal promise used to hold the completion of #getSecurityInfo.
+ *
+ * @type {Promise}
+ */
+ #onSecurityInfo = null;
+ /**
+ * Offset for the onDataAvailable calls where we pass the data from our pipe
+ * to the converter.
+ *
+ * @type {Number}
+ */
+ #offset = 0;
+ /**
+ * Stores the received data as a string.
+ *
+ * @type {string}
+ */
+ #receivedData = "";
+ /**
+ * The nsIRequest we are started for.
+ *
+ * @type {nsIRequest}
+ */
+ #request = null;
+ /**
+ * The response will be written into the outputStream of this nsIPipe.
+ * Both ends of the pipe must be blocking.
+ *
+ * @type {nsIPipe}
+ */
+ #sink = null;
+ /**
+ * Indicates if the response had a size greater than response body limit.
+ *
+ * @type {boolean}
+ */
+ #truncated = false;
+ /**
+ * Backup for existing notificationCallbacks set on the monitored channel.
+ * Initialized in the constructor.
+ *
+ * @type {Object}
+ */
+ #wrappedNotificationCallbacks;
+
+ constructor(httpActivity, decodedCertificateCache, fromServiceWorker) {
+ this.#httpActivity = httpActivity;
+ this.#decodedCertificateCache = decodedCertificateCache;
+ this.#fromServiceWorker = fromServiceWorker;
+
+ // Note that this is really only needed for the non-e10s case.
+ // See bug 1309523.
+ const channel = this.#httpActivity.channel;
+ // If the channel already had notificationCallbacks, hold them here
+ // internally so that we can forward getInterface requests to that object.
+ this.#wrappedNotificationCallbacks = channel.notificationCallbacks;
+ channel.notificationCallbacks = this;
+ }
+
+ set inputStream(inputStream) {
+ this.#inputStream = inputStream;
+ }
+
+ set sink(sink) {
+ this.#sink = sink;
+ }
+
+ // nsIInterfaceRequestor implementation
+
+ /**
+ * This object implements nsIProgressEventSink, but also needs to forward
+ * interface requests to the notification callbacks of other objects.
+ */
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIProgressEventSink)) {
+ return this;
+ }
+ if (this.#wrappedNotificationCallbacks) {
+ return this.#wrappedNotificationCallbacks.getInterface(iid);
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+
+ /**
+ * Forward notifications for interfaces this object implements, in case other
+ * objects also implemented them.
+ */
+ #forwardNotification(iid, method, args) {
+ if (!this.#wrappedNotificationCallbacks) {
+ return;
+ }
+ try {
+ const impl = this.#wrappedNotificationCallbacks.getInterface(iid);
+ impl[method].apply(impl, args);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NO_INTERFACE) {
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Set the async listener for the given nsIAsyncInputStream. This allows us to
+ * wait asynchronously for any data coming from the stream.
+ *
+ * @param nsIAsyncInputStream stream
+ * The input stream from where we are waiting for data to come in.
+ * @param nsIInputStreamCallback listener
+ * The input stream callback you want. This is an object that must have
+ * the onInputStreamReady() method. If the argument is null, then the
+ * current callback is removed.
+ * @return void
+ */
+ setAsyncListener(stream, listener) {
+ // Asynchronously wait for the stream to be readable or closed.
+ stream.asyncWait(listener, 0, 0, Services.tm.mainThread);
+ }
+
+ /**
+ * Stores the received data, if request/response body logging is enabled. It
+ * also does limit the number of stored bytes, based on the
+ * `devtools.netmonitor.responseBodyLimit` pref.
+ *
+ * Learn more about nsIStreamListener at:
+ * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
+ *
+ * @param nsIRequest request
+ * @param nsISupports context
+ * @param nsIInputStream inputStream
+ * @param unsigned long offset
+ * @param unsigned long count
+ */
+ onDataAvailable(request, inputStream, offset, count) {
+ const data = lazy.NetUtil.readInputStreamToString(inputStream, count);
+
+ this.#decodedBodySize += count;
+
+ if (!this.#httpActivity.discardResponseBody) {
+ const limit = Services.prefs.getIntPref(
+ "devtools.netmonitor.responseBodyLimit"
+ );
+ if (this.#receivedData.length <= limit || limit == 0) {
+ this.#receivedData += lazy.NetworkHelper.convertToUnicode(
+ data,
+ request.contentCharset
+ );
+ }
+ if (this.#receivedData.length > limit && limit > 0) {
+ this.#receivedData = this.#receivedData.substr(0, limit);
+ this.#truncated = true;
+ }
+ }
+ }
+
+ /**
+ * See documentation at
+ * https://developer.mozilla.org/En/NsIRequestObserver
+ *
+ * @param nsIRequest request
+ * @param nsISupports context
+ */
+ onStartRequest(request) {
+ request = request.QueryInterface(Ci.nsIChannel);
+ // Converter will call this again, we should just ignore that.
+ if (this.#request) {
+ return;
+ }
+
+ this.#request = request;
+ this.#onSecurityInfo = this.#getSecurityInfo();
+ // We need to track the offset for the onDataAvailable calls where
+ // we pass the data from our pipe to the converter.
+ this.#offset = 0;
+
+ const channel = this.#request;
+
+ // Bug 1372115 - We should load bytecode cached requests from cache as the actual
+ // channel content is going to be optimized data that reflects platform internals
+ // instead of the content user expects (i.e. content served by HTTP server)
+ // Note that bytecode cached is one example, there may be wasm or other usecase in
+ // future.
+ let isOptimizedContent = false;
+ try {
+ if (channel instanceof Ci.nsICacheInfoChannel) {
+ isOptimizedContent = channel.alternativeDataType;
+ }
+ } catch (e) {
+ // Accessing `alternativeDataType` for some SW requests throws.
+ }
+ if (isOptimizedContent) {
+ let charset;
+ try {
+ charset = this.#request.contentCharset;
+ } catch (e) {
+ // Accessing the charset sometimes throws NS_ERROR_NOT_AVAILABLE when
+ // reloading the page
+ }
+ if (!charset) {
+ charset = this.#httpActivity.charset;
+ }
+ lazy.NetworkHelper.loadFromCache(
+ this.#httpActivity.url,
+ charset,
+ this.#onComplete.bind(this)
+ );
+ return;
+ }
+
+ // In the multi-process mode, the conversion happens on the child
+ // side while we can only monitor the channel on the parent
+ // side. If the content is gzipped, we have to unzip it
+ // ourself. For that we use the stream converter services. Do not
+ // do that for Service workers as they are run in the child
+ // process.
+ if (
+ !this.#fromServiceWorker &&
+ channel instanceof Ci.nsIEncodedChannel &&
+ channel.contentEncodings &&
+ !channel.applyConversion &&
+ !channel.hasContentDecompressed
+ ) {
+ const encodingHeader = channel.getResponseHeader("Content-Encoding");
+ const scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ const encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
+ let nextListener = this;
+ const acceptedEncodings = [
+ "gzip",
+ "deflate",
+ "br",
+ "x-gzip",
+ "x-deflate",
+ ];
+ for (const i in encodings) {
+ // There can be multiple conversions applied
+ const enc = encodings[i].toLowerCase();
+ if (acceptedEncodings.indexOf(enc) > -1) {
+ this.#converter = scs.asyncConvertData(
+ enc,
+ "uncompressed",
+ nextListener,
+ null
+ );
+ nextListener = this.#converter;
+ }
+ }
+ if (this.#converter) {
+ this.#converter.onStartRequest(this.#request, null);
+ }
+ }
+ // Asynchronously wait for the data coming from the request.
+ this.setAsyncListener(this.#sink.inputStream, this);
+ }
+
+ /**
+ * Parse security state of this request and report it to the client.
+ */
+ async #getSecurityInfo() {
+ // Many properties of the securityInfo (e.g., the server certificate or HPKP
+ // status) are not available in the content process and can't be even touched safely,
+ // because their C++ getters trigger assertions. This function is called in content
+ // process for synthesized responses from service workers, in the parent otherwise.
+ if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ return;
+ }
+
+ // Take the security information from the original nsIHTTPChannel instead of
+ // the nsIRequest received in onStartRequest. If response to this request
+ // was a redirect from http to https, the request object seems to contain
+ // security info for the https request after redirect.
+ const secinfo = this.#httpActivity.channel.securityInfo;
+ const info = await lazy.NetworkHelper.parseSecurityInfo(
+ secinfo,
+ this.#request.loadInfo.originAttributes,
+ this.#httpActivity,
+ this.#decodedCertificateCache
+ );
+ let isRacing = false;
+ try {
+ const channel = this.#httpActivity.channel;
+ if (channel instanceof Ci.nsICacheInfoChannel) {
+ isRacing = channel.isRacing();
+ }
+ } catch (err) {
+ // See the following bug for more details:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1582589
+ }
+
+ this.#httpActivity.owner.addSecurityInfo(info, isRacing);
+ }
+
+ /**
+ * Fetches cache information from CacheEntry
+ * @private
+ */
+ async #fetchCacheInformation() {
+ // TODO: This method is async and #httpActivity is nullified in the #destroy
+ // method of this class. Backup httpActivity to avoid errors here.
+ const httpActivity = this.#httpActivity;
+ const cacheEntry = await lazy.getResponseCacheObject(this.#request);
+ httpActivity.owner.addResponseCache({
+ responseCache: cacheEntry,
+ });
+ }
+
+ /**
+ * Handle the onStopRequest by closing the sink output stream.
+ *
+ * For more documentation about nsIRequestObserver go to:
+ * https://developer.mozilla.org/En/NsIRequestObserver
+ */
+ onStopRequest() {
+ // Bug 1429365: onStopRequest may be called after onComplete for resources loaded
+ // from bytecode cache.
+ if (!this.#httpActivity) {
+ return;
+ }
+ this.#sink.outputStream.close();
+ }
+
+ // nsIProgressEventSink implementation
+
+ /**
+ * Handle progress event as data is transferred. This is used to record the
+ * size on the wire, which may be compressed / encoded.
+ */
+ onProgress(request, progress, progressMax) {
+ this.#bodySize = progress;
+
+ // Need to forward as well to keep things like Download Manager's progress
+ // bar working properly.
+ this.#forwardNotification(Ci.nsIProgressEventSink, "onProgress", arguments);
+ }
+
+ onStatus() {
+ this.#forwardNotification(Ci.nsIProgressEventSink, "onStatus", arguments);
+ }
+
+ /**
+ * Clean up the response listener once the response input stream is closed.
+ * This is called from onStopRequest() or from onInputStreamReady() when the
+ * stream is closed.
+ * @return void
+ */
+ onStreamClose() {
+ if (!this.#httpActivity) {
+ return;
+ }
+ // Remove our listener from the request input stream.
+ this.setAsyncListener(this.#sink.inputStream, null);
+
+ let responseStatus;
+ try {
+ responseStatus = this.#httpActivity.channel.responseStatus;
+ } catch (e) {
+ // Will throw NS_ERROR_NOT_AVAILABLE if the response has not been received
+ // yet.
+ }
+ if (this.#request.fromCache || responseStatus == 304) {
+ this.#fetchCacheInformation();
+ }
+
+ if (!this.#httpActivity.discardResponseBody && this.#receivedData.length) {
+ this.#onComplete(this.#receivedData);
+ } else if (
+ !this.#httpActivity.discardResponseBody &&
+ responseStatus == 304
+ ) {
+ // Response is cached, so we load it from cache.
+ let charset;
+ try {
+ charset = this.#request.contentCharset;
+ } catch (e) {
+ // Accessing the charset sometimes throws NS_ERROR_NOT_AVAILABLE when
+ // reloading the page
+ }
+ if (!charset) {
+ charset = this.#httpActivity.charset;
+ }
+ lazy.NetworkHelper.loadFromCache(
+ this.#httpActivity.url,
+ charset,
+ this.#onComplete.bind(this)
+ );
+ } else {
+ this.#onComplete();
+ }
+ }
+
+ /**
+ * Handler for when the response completes. This function cleans up the
+ * response listener.
+ *
+ * @param string [data]
+ * Optional, the received data coming from the response listener or
+ * from the cache.
+ */
+ #onComplete(data) {
+ // Make sure all the security and response content info are sent
+ this.#getResponseContent(data);
+ this.#onSecurityInfo.then(() => this.#destroy());
+ }
+
+ /**
+ * Create the response object and send it to the client.
+ */
+ #getResponseContent(data) {
+ const response = {
+ mimeType: "",
+ text: data || "",
+ };
+
+ response.bodySize = this.#bodySize;
+ response.decodedBodySize = this.#decodedBodySize;
+ // TODO: Stop exposing the decodedBodySize as `size` which is ambiguous.
+ // Consumers should use `decodedBodySize` instead. See Bug 1808560.
+ response.size = this.#decodedBodySize;
+ response.headersSize = this.#httpActivity.headersSize;
+ response.transferredSize = this.#bodySize + this.#httpActivity.headersSize;
+
+ try {
+ response.mimeType = this.#request.contentType;
+ } catch (ex) {
+ // Ignore.
+ }
+
+ if (
+ !response.mimeType ||
+ !lazy.NetworkHelper.isTextMimeType(response.mimeType)
+ ) {
+ response.encoding = "base64";
+ try {
+ response.text = btoa(response.text);
+ } catch (err) {
+ // Ignore.
+ }
+ }
+
+ if (response.mimeType && this.#request.contentCharset) {
+ response.mimeType += "; charset=" + this.#request.contentCharset;
+ }
+
+ this.#receivedData = "";
+
+ // Check any errors or blocking scenarios which happen late in the cycle
+ // e.g If a host is not found (NS_ERROR_UNKNOWN_HOST) or CORS blocking.
+ const { blockingExtension, blockedReason } =
+ lazy.NetworkUtils.getBlockedReason(
+ this.#httpActivity.channel,
+ this.#httpActivity.fromCache
+ );
+
+ this.#httpActivity.owner.addResponseContent(response, {
+ discardResponseBody: this.#httpActivity.discardResponseBody,
+ truncated: this.#truncated,
+ blockedReason,
+ blockingExtension,
+ });
+ }
+
+ #destroy() {
+ this.#wrappedNotificationCallbacks = null;
+ this.#httpActivity = null;
+ this.#sink = null;
+ this.#inputStream = null;
+ this.#converter = null;
+ this.#request = null;
+
+ this.#isDestroyed = true;
+ }
+
+ /**
+ * The nsIInputStreamCallback for when the request input stream is ready -
+ * either it has more data or it is closed.
+ *
+ * @param nsIAsyncInputStream stream
+ * The sink input stream from which data is coming.
+ * @returns void
+ */
+ onInputStreamReady(stream) {
+ if (!(stream instanceof Ci.nsIAsyncInputStream) || !this.#httpActivity) {
+ return;
+ }
+
+ let available = -1;
+ try {
+ // This may throw if the stream is closed normally or due to an error.
+ available = stream.available();
+ } catch (ex) {
+ // Ignore.
+ }
+
+ if (available != -1) {
+ if (available != 0) {
+ if (this.#converter) {
+ this.#converter.onDataAvailable(
+ this.#request,
+ stream,
+ this.#offset,
+ available
+ );
+ } else {
+ this.onDataAvailable(this.#request, stream, this.#offset, available);
+ }
+ }
+ this.#offset += available;
+ this.setAsyncListener(stream, this);
+ } else {
+ this.onStreamClose();
+ this.#offset = 0;
+ }
+ }
+
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIInputStreamCallback",
+ "nsIRequestObserver",
+ "nsIInterfaceRequestor",
+ ]);
+}
diff --git a/devtools/shared/network-observer/NetworkThrottleManager.sys.mjs b/devtools/shared/network-observer/NetworkThrottleManager.sys.mjs
new file mode 100644
index 0000000000..a643861004
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkThrottleManager.sys.mjs
@@ -0,0 +1,495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ArrayBufferInputStream = Components.Constructor(
+ "@mozilla.org/io/arraybuffer-input-stream;1",
+ "nsIArrayBufferInputStream"
+);
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gActivityDistributor",
+ "@mozilla.org/network/http-activity-distributor;1",
+ "nsIHttpActivityDistributor"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+class NetworkThrottleListener {
+ #activities;
+ #offset;
+ #originalListener;
+ #pendingData;
+ #pendingException;
+ #queue;
+ #responseStarted;
+
+ /**
+ * Construct a new nsIStreamListener that buffers data and provides a
+ * method to notify another listener when data is available. This is
+ * used to throttle network data on a per-channel basis.
+ *
+ * After construction, @see setOriginalListener must be called on the
+ * new object.
+ *
+ * @param {NetworkThrottleQueue} queue the NetworkThrottleQueue to
+ * which status changes should be reported
+ */
+ constructor(queue) {
+ this.#activities = {};
+ this.#offset = 0;
+ this.#pendingData = [];
+ this.#pendingException = null;
+ this.#queue = queue;
+ this.#responseStarted = false;
+ }
+
+ /**
+ * Set the original listener for this object. The original listener
+ * will receive requests from this object when the queue allows data
+ * through.
+ *
+ * @param {nsIStreamListener} originalListener the original listener
+ * for the channel, to which all requests will be sent
+ */
+ setOriginalListener(originalListener) {
+ this.#originalListener = originalListener;
+ }
+
+ /**
+ * @see nsIStreamListener.onStartRequest.
+ */
+ onStartRequest(request) {
+ this.#originalListener.onStartRequest(request);
+ this.#queue.start(this);
+ }
+
+ /**
+ * @see nsIStreamListener.onStopRequest.
+ */
+ onStopRequest(request, statusCode) {
+ this.#pendingData.push({ request, statusCode });
+ this.#queue.dataAvailable(this);
+ }
+
+ /**
+ * @see nsIStreamListener.onDataAvailable.
+ */
+ onDataAvailable(request, inputStream, offset, count) {
+ if (this.#pendingException) {
+ throw this.#pendingException;
+ }
+
+ const bin = new BinaryInputStream(inputStream);
+ const bytes = new ArrayBuffer(count);
+ bin.readArrayBuffer(count, bytes);
+
+ const stream = new ArrayBufferInputStream();
+ stream.setData(bytes, 0, count);
+
+ this.#pendingData.push({ request, stream, count });
+ this.#queue.dataAvailable(this);
+ }
+
+ /**
+ * Allow some buffered data from this object to be forwarded to this
+ * object's originalListener.
+ *
+ * @param {Number} bytesPermitted The maximum number of bytes
+ * permitted to be sent.
+ * @return {Object} an object of the form {length, done}, where
+ * |length| is the number of bytes actually forwarded, and
+ * |done| is a boolean indicating whether this particular
+ * request has been completed. (A NetworkThrottleListener
+ * may be queued multiple times, so this does not mean that
+ * all available data has been sent.)
+ */
+ sendSomeData(bytesPermitted) {
+ if (this.#pendingData.length === 0) {
+ // Shouldn't happen.
+ return { length: 0, done: true };
+ }
+
+ const { request, stream, count, statusCode } = this.#pendingData[0];
+
+ if (statusCode !== undefined) {
+ this.#pendingData.shift();
+ this.#originalListener.onStopRequest(request, statusCode);
+ return { length: 0, done: true };
+ }
+
+ if (bytesPermitted > count) {
+ bytesPermitted = count;
+ }
+
+ try {
+ this.#originalListener.onDataAvailable(
+ request,
+ stream,
+ this.#offset,
+ bytesPermitted
+ );
+ } catch (e) {
+ this.#pendingException = e;
+ }
+
+ let done = false;
+ if (bytesPermitted === count) {
+ this.#pendingData.shift();
+ done = true;
+ } else {
+ this.#pendingData[0].count -= bytesPermitted;
+ }
+
+ this.#offset += bytesPermitted;
+ // Maybe our state has changed enough to emit an event.
+ this.#maybeEmitEvents();
+
+ return { length: bytesPermitted, done };
+ }
+
+ /**
+ * Return the number of pending data requests available for this
+ * listener.
+ */
+ pendingCount() {
+ return this.#pendingData.length;
+ }
+
+ /**
+ * This is called when an http activity event is delivered. This
+ * object delays the event until the appropriate moment.
+ */
+ addActivityCallback(
+ callback,
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ timestamp,
+ extraSizeData,
+ extraStringData
+ ) {
+ const datum = {
+ callback,
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ extraSizeData,
+ extraStringData,
+ };
+ this.#activities[activitySubtype] = datum;
+
+ if (
+ activitySubtype ===
+ lazy.gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE
+ ) {
+ this.totalSize = extraSizeData;
+ }
+
+ this.#maybeEmitEvents();
+ }
+
+ /**
+ * This is called for a download throttler when the latency timeout
+ * has ended.
+ */
+ responseStart() {
+ this.#responseStarted = true;
+ this.#maybeEmitEvents();
+ }
+
+ /**
+ * Check our internal state and emit any http activity events as
+ * needed. Note that we wait until both our internal state has
+ * changed and we've received the real http activity event from
+ * platform. This approach ensures we can both pass on the correct
+ * data from the original event, and update the reported time to be
+ * consistent with the delay we're introducing.
+ */
+ #maybeEmitEvents() {
+ if (this.#responseStarted) {
+ this.#maybeEmit(
+ lazy.gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START
+ );
+ this.#maybeEmit(
+ lazy.gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER
+ );
+ }
+
+ if (this.totalSize !== undefined && this.#offset >= this.totalSize) {
+ this.#maybeEmit(
+ lazy.gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE
+ );
+ this.#maybeEmit(
+ lazy.gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
+ );
+ }
+ }
+
+ /**
+ * Emit an event for |code|, if the appropriate entry in
+ * |activities| is defined.
+ */
+ #maybeEmit(code) {
+ if (this.#activities[code] !== undefined) {
+ const {
+ callback,
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ extraSizeData,
+ extraStringData,
+ } = this.#activities[code];
+ const now = Date.now() * 1000;
+ callback(
+ httpActivity,
+ channel,
+ activityType,
+ activitySubtype,
+ now,
+ extraSizeData,
+ extraStringData
+ );
+ this.#activities[code] = undefined;
+ }
+ }
+
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIInterfaceRequestor",
+ ]);
+}
+
+class NetworkThrottleQueue {
+ #downloadQueue;
+ #latencyMax;
+ #latencyMean;
+ #maxBPS;
+ #meanBPS;
+ #pendingRequests;
+ #previousReads;
+ #pumping;
+
+ /**
+ * Construct a new queue that can be used to throttle the network for
+ * a group of related network requests.
+ *
+ * meanBPS {Number} Mean bytes per second.
+ * maxBPS {Number} Maximum bytes per second.
+ * latencyMean {Number} Mean latency in milliseconds.
+ * latencyMax {Number} Maximum latency in milliseconds.
+ */
+ constructor(meanBPS, maxBPS, latencyMean, latencyMax) {
+ this.#meanBPS = meanBPS;
+ this.#maxBPS = maxBPS;
+ this.#latencyMean = latencyMean;
+ this.#latencyMax = latencyMax;
+
+ this.#pendingRequests = new Set();
+ this.#downloadQueue = [];
+ this.#previousReads = [];
+
+ this.#pumping = false;
+ }
+
+ /**
+ * A helper function that lets the indicating listener start sending
+ * data. This is called after the initial round trip time for the
+ * listener has elapsed.
+ */
+ #allowDataFrom(throttleListener) {
+ throttleListener.responseStart();
+ this.#pendingRequests.delete(throttleListener);
+ const count = throttleListener.pendingCount();
+ for (let i = 0; i < count; ++i) {
+ this.#downloadQueue.push(throttleListener);
+ }
+ this.#pump();
+ }
+
+ /**
+ * An internal function that permits individual listeners to send
+ * data.
+ */
+ #pump() {
+ // A redirect will cause two NetworkThrottleListeners to be on a
+ // listener chain. In this case, we might recursively call into
+ // this method. Avoid infinite recursion here.
+ if (this.#pumping) {
+ return;
+ }
+ this.#pumping = true;
+
+ const now = Date.now();
+ const oneSecondAgo = now - 1000;
+
+ while (
+ this.#previousReads.length &&
+ this.#previousReads[0].when < oneSecondAgo
+ ) {
+ this.#previousReads.shift();
+ }
+
+ const totalBytes = this.#previousReads.reduce((sum, elt) => {
+ return sum + elt.numBytes;
+ }, 0);
+
+ let thisSliceBytes = this.#random(this.#meanBPS, this.#maxBPS);
+ if (totalBytes < thisSliceBytes) {
+ thisSliceBytes -= totalBytes;
+ let readThisTime = 0;
+ while (thisSliceBytes > 0 && this.#downloadQueue.length) {
+ const { length, done } =
+ this.#downloadQueue[0].sendSomeData(thisSliceBytes);
+ thisSliceBytes -= length;
+ readThisTime += length;
+ if (done) {
+ this.#downloadQueue.shift();
+ }
+ }
+ this.#previousReads.push({ when: now, numBytes: readThisTime });
+ }
+
+ // If there is more data to download, then schedule ourselves for
+ // one second after the oldest previous read.
+ if (this.#downloadQueue.length) {
+ const when = this.#previousReads[0].when + 1000;
+ lazy.setTimeout(this.#pump.bind(this), when - now);
+ }
+
+ this.#pumping = false;
+ }
+
+ /**
+ * A helper function that, given a mean and a maximum, returns a
+ * random integer between (mean - (max - mean)) and max.
+ */
+ #random(mean, max) {
+ return mean - (max - mean) + Math.floor(2 * (max - mean) * Math.random());
+ }
+
+ /**
+ * Notice a new listener object. This is called by the
+ * NetworkThrottleListener when the request has started. Initially
+ * a new listener object is put into a "pending" state, until the
+ * round-trip time has elapsed. This is used to simulate latency.
+ *
+ * @param {NetworkThrottleListener} throttleListener the new listener
+ */
+ start(throttleListener) {
+ this.#pendingRequests.add(throttleListener);
+ const delay = this.#random(this.#latencyMean, this.#latencyMax);
+ if (delay > 0) {
+ lazy.setTimeout(() => this.#allowDataFrom(throttleListener), delay);
+ } else {
+ this.#allowDataFrom(throttleListener);
+ }
+ }
+
+ /**
+ * Note that new data is available for a given listener. Each time
+ * data is available, the listener will be re-queued.
+ *
+ * @param {NetworkThrottleListener} throttleListener the listener
+ * which has data available.
+ */
+ dataAvailable(throttleListener) {
+ if (!this.#pendingRequests.has(throttleListener)) {
+ this.#downloadQueue.push(throttleListener);
+ this.#pump();
+ }
+ }
+}
+
+/**
+ * Construct a new object that can be used to throttle the network for
+ * a group of related network requests.
+ *
+ * @param {Object} An object with the following attributes:
+ * latencyMean {Number} Mean latency in milliseconds.
+ * latencyMax {Number} Maximum latency in milliseconds.
+ * downloadBPSMean {Number} Mean bytes per second for downloads.
+ * downloadBPSMax {Number} Maximum bytes per second for downloads.
+ * uploadBPSMean {Number} Mean bytes per second for uploads.
+ * uploadBPSMax {Number} Maximum bytes per second for uploads.
+ *
+ * Download throttling will not be done if downloadBPSMean and
+ * downloadBPSMax are <= 0. Upload throttling will not be done if
+ * uploadBPSMean and uploadBPSMax are <= 0.
+ */
+export class NetworkThrottleManager {
+ #downloadQueue;
+
+ constructor({
+ latencyMean,
+ latencyMax,
+ downloadBPSMean,
+ downloadBPSMax,
+ uploadBPSMean,
+ uploadBPSMax,
+ }) {
+ if (downloadBPSMax <= 0 && downloadBPSMean <= 0) {
+ this.#downloadQueue = null;
+ } else {
+ this.#downloadQueue = new NetworkThrottleQueue(
+ downloadBPSMean,
+ downloadBPSMax,
+ latencyMean,
+ latencyMax
+ );
+ }
+ if (uploadBPSMax <= 0 && uploadBPSMean <= 0) {
+ this.uploadQueue = null;
+ } else {
+ this.uploadQueue = Cc[
+ "@mozilla.org/network/throttlequeue;1"
+ ].createInstance(Ci.nsIInputChannelThrottleQueue);
+ this.uploadQueue.init(uploadBPSMean, uploadBPSMax);
+ }
+ }
+
+ /**
+ * Create a new NetworkThrottleListener for a given channel and
+ * install it using |setNewListener|.
+ *
+ * @param {nsITraceableChannel} channel the channel to manage
+ * @return {NetworkThrottleListener} the new listener, or null if
+ * download throttling is not being done.
+ */
+ manage(channel) {
+ if (this.#downloadQueue) {
+ const listener = new NetworkThrottleListener(this.#downloadQueue);
+ const originalListener = channel.setNewListener(listener);
+ listener.setOriginalListener(originalListener);
+ return listener;
+ }
+ return null;
+ }
+
+ /**
+ * Throttle uploads taking place on the given channel.
+ *
+ * @param {nsITraceableChannel} channel the channel to manage
+ */
+ manageUpload(channel) {
+ if (this.uploadQueue) {
+ channel = channel.QueryInterface(Ci.nsIThrottledInputChannel);
+ channel.throttleQueue = this.uploadQueue;
+ }
+ }
+}
diff --git a/devtools/shared/network-observer/NetworkUtils.sys.mjs b/devtools/shared/network-observer/NetworkUtils.sys.mjs
new file mode 100644
index 0000000000..6f564a9b1a
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkUtils.sys.mjs
@@ -0,0 +1,693 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "tpFlagsMask", () => {
+ const trackingProtectionLevel2Enabled = Services.prefs
+ .getStringPref("urlclassifier.trackingTable")
+ .includes("content-track-digest256");
+
+ return trackingProtectionLevel2Enabled
+ ? ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
+ ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING
+ : ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
+ Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING;
+});
+
+/**
+ * Convert a nsIContentPolicy constant to a display string
+ */
+const LOAD_CAUSE_STRINGS = {
+ [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+ [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+ [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+ [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+ [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+ [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+ [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+ [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+ [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+ [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+ [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+ [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+ [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+ [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+ [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+ [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+ [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+ [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+ [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+ [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+ [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest",
+ [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "webidentity",
+};
+
+function causeTypeToString(causeType, loadFlags, internalContentPolicyType) {
+ let prefix = "";
+ if (
+ (causeType == Ci.nsIContentPolicy.TYPE_IMAGESET ||
+ internalContentPolicyType == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE) &&
+ loadFlags & Ci.nsIRequest.LOAD_BACKGROUND
+ ) {
+ prefix = "lazy-";
+ }
+
+ return prefix + LOAD_CAUSE_STRINGS[causeType] || "unknown";
+}
+
+function stringToCauseType(value) {
+ return Object.keys(LOAD_CAUSE_STRINGS).find(
+ key => LOAD_CAUSE_STRINGS[key] === value
+ );
+}
+
+function isChannelFromSystemPrincipal(channel) {
+ let principal = null;
+ let browsingContext = channel.loadInfo.browsingContext;
+ if (!browsingContext) {
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ if (topFrame) {
+ browsingContext = topFrame.browsingContext;
+ } else {
+ // Fallback to the triggering principal when browsingContext and topFrame is null
+ // e.g some chrome requests
+ principal = channel.loadInfo.triggeringPrincipal;
+ }
+ }
+
+ // When in the parent process, we can get the documentPrincipal from the
+ // WindowGlobal which is available on the BrowsingContext
+ if (!principal) {
+ principal = CanonicalBrowsingContext.isInstance(browsingContext)
+ ? browsingContext.currentWindowGlobal.documentPrincipal
+ : browsingContext.window.document.nodePrincipal;
+ }
+ return principal.isSystemPrincipal;
+}
+
+/**
+ * Get the browsing context id for the channel.
+ *
+ * @param {*} channel
+ * @returns {number}
+ */
+function getChannelBrowsingContextID(channel) {
+ // `frameBrowsingContextID` is non-0 if the channel is loading an iframe.
+ // If available, use it instead of `browsingContextID` which is exceptionally
+ // set to the parent's BrowsingContext id for such channels.
+ if (channel.loadInfo.frameBrowsingContextID) {
+ return channel.loadInfo.frameBrowsingContextID;
+ }
+
+ if (channel.loadInfo.browsingContextID) {
+ return channel.loadInfo.browsingContextID;
+ }
+ // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
+ // We fallback on top frame element, which works, but will be wrong for WebSocket
+ // in same-process iframes...
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // topFrame is typically null for some chrome requests like favicons
+ if (topFrame && topFrame.browsingContext) {
+ return topFrame.browsingContext.id;
+ }
+ return null;
+}
+
+/**
+ * Get the innerWindowId for the channel.
+ *
+ * @param {*} channel
+ * @returns {number}
+ */
+function getChannelInnerWindowId(channel) {
+ if (channel.loadInfo.innerWindowID) {
+ return channel.loadInfo.innerWindowID;
+ }
+ // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
+ // We fallback on top frame element, which works, but will be wrong for WebSocket
+ // in same-process iframes...
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // topFrame is typically null for some chrome requests like favicons
+ if (topFrame?.browsingContext?.currentWindowGlobal) {
+ return topFrame.browsingContext.currentWindowGlobal.innerWindowId;
+ }
+ return null;
+}
+
+/**
+ * Does this channel represent a Preload request.
+ *
+ * @param {*} channel
+ * @returns {boolean}
+ */
+function isPreloadRequest(channel) {
+ const type = channel.loadInfo.internalContentPolicyType;
+ return (
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_MODULE_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_FONT_PRELOAD
+ );
+}
+
+/**
+ * Get the channel cause details.
+ *
+ * @param {nsIChannel} channel
+ * @returns {Object}
+ * - loadingDocumentUri {string} uri of the document which created the
+ * channel
+ * - type {string} cause type as string
+ */
+function getCauseDetails(channel) {
+ // Determine the cause and if this is an XHR request.
+ let causeType = Ci.nsIContentPolicy.TYPE_OTHER;
+ let causeUri = null;
+
+ if (channel.loadInfo) {
+ causeType = channel.loadInfo.externalContentPolicyType;
+ const { loadingPrincipal } = channel.loadInfo;
+ if (loadingPrincipal) {
+ causeUri = loadingPrincipal.spec;
+ }
+ }
+
+ return {
+ loadingDocumentUri: causeUri,
+ type: causeTypeToString(
+ causeType,
+ channel.loadFlags,
+ channel.loadInfo.internalContentPolicyType
+ ),
+ };
+}
+
+/**
+ * Get the channel priority. Priority is a number which typically ranges from
+ * -20 (lowest priority) to 20 (highest priority). Can be null if the channel
+ * does not implement nsISupportsPriority.
+ *
+ * @param {nsIChannel} channel
+ * @returns {number|undefined}
+ */
+function getChannelPriority(channel) {
+ if (channel instanceof Ci.nsISupportsPriority) {
+ return channel.priority;
+ }
+
+ return null;
+}
+
+/**
+ * Get the channel HTTP version as an uppercase string starting with "HTTP/"
+ * (eg "HTTP/2").
+ *
+ * @param {nsIChannel} channel
+ * @returns {string}
+ */
+function getHttpVersion(channel) {
+ // Determine the HTTP version.
+ const httpVersionMaj = {};
+ const httpVersionMin = {};
+
+ channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ channel.getResponseVersion(httpVersionMaj, httpVersionMin);
+
+ // The official name HTTP version 2.0 and 3.0 are HTTP/2 and HTTP/3, omit the
+ // trailing `.0`.
+ if (httpVersionMin.value == 0) {
+ return "HTTP/" + httpVersionMaj.value;
+ }
+
+ return "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value;
+}
+
+const UNKNOWN_PROTOCOL_STRINGS = ["", "unknown"];
+const HTTP_PROTOCOL_STRINGS = ["http", "https"];
+/**
+ * Get the protocol for the provided httpActivity. Either the ALPN negotiated
+ * protocol or as a fallback a protocol computed from the scheme and the
+ * response status.
+ *
+ * TODO: The `protocol` is similar to another response property called
+ * `httpVersion`. `httpVersion` is uppercase and purely computed from the
+ * response status, whereas `protocol` uses nsIHttpChannel.protocolVersion by
+ * default and otherwise falls back on `httpVersion`. Ideally we should merge
+ * the two properties.
+ *
+ * @param {Object} httpActivity
+ * The httpActivity object for which we need to get the protocol.
+ *
+ * @returns {string}
+ * The protocol as a string.
+ */
+function getProtocol(channel) {
+ let protocol = "";
+ try {
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ // protocolVersion corresponds to ALPN negotiated protocol.
+ protocol = httpChannel.protocolVersion;
+ } catch (e) {
+ // Ignore errors reading protocolVersion.
+ }
+
+ if (UNKNOWN_PROTOCOL_STRINGS.includes(protocol)) {
+ protocol = channel.URI.scheme;
+ const httpVersion = getHttpVersion(channel);
+ if (
+ typeof httpVersion == "string" &&
+ HTTP_PROTOCOL_STRINGS.includes(protocol)
+ ) {
+ protocol = httpVersion.toLowerCase();
+ }
+ }
+
+ return protocol;
+}
+
+/**
+ * Get the channel referrer policy as a string
+ * (eg "strict-origin-when-cross-origin").
+ *
+ * @param {nsIChannel} channel
+ * @returns {string}
+ */
+function getReferrerPolicy(channel) {
+ return channel.referrerInfo
+ ? channel.referrerInfo.getReferrerPolicyString()
+ : "";
+}
+
+/**
+ * Check if the channel is private.
+ *
+ * @param {nsIChannel} channel
+ * @returns {boolean}
+ */
+function isChannelPrivate(channel) {
+ channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+ return channel.isChannelPrivate;
+}
+
+/**
+ * Check if the channel data is loaded from the cache or not.
+ *
+ * @param {nsIChannel} channel
+ * The channel for which we need to check the cache status.
+ *
+ * @returns {boolean}
+ * True if the channel data is loaded from the cache, false otherwise.
+ */
+function isFromCache(channel) {
+ if (channel instanceof Ci.nsICacheInfoChannel) {
+ return channel.isFromCache();
+ }
+
+ return false;
+}
+
+const REDIRECT_STATES = [
+ 301, // HTTP Moved Permanently
+ 302, // HTTP Found
+ 303, // HTTP See Other
+ 307, // HTTP Temporary Redirect
+];
+/**
+ * Check if the channel's status corresponds to a known redirect status.
+ *
+ * @param {nsIChannel} channel
+ * The channel for which we need to check the redirect status.
+ *
+ * @returns {boolean}
+ * True if the channel data is a redirect, false otherwise.
+ */
+function isRedirectedChannel(channel) {
+ try {
+ return REDIRECT_STATES.includes(channel.responseStatus);
+ } catch (e) {
+ // Throws NS_ERROR_NOT_AVAILABLE if the request was not sent yet.
+ }
+ return false;
+}
+
+/**
+ * isNavigationRequest is true for the one request used to load a new top level
+ * document of a given tab, or top level window. It will typically be false for
+ * navigation requests of iframes, i.e. the request loading another document in
+ * an iframe.
+ *
+ * @param {nsIChannel} channel
+ * @return {boolean}
+ */
+function isNavigationRequest(channel) {
+ return channel.isMainDocumentChannel && channel.loadInfo.isTopLevelLoad;
+}
+
+/**
+ * Returns true if the channel has been processed by URL-Classifier features
+ * and is considered third-party with the top window URI, and if it has loaded
+ * a resource that is classified as a tracker.
+ *
+ * @param {nsIChannel} channel
+ * @return {boolean}
+ */
+function isThirdPartyTrackingResource(channel) {
+ // Only consider channels classified as level-1 to be trackers if our preferences
+ // would not cause such channels to be blocked in strict content blocking mode.
+ // Make sure the value produced here is a boolean.
+ return !!(
+ channel instanceof Ci.nsIClassifiedChannel &&
+ channel.isThirdPartyTrackingResource() &&
+ (channel.thirdPartyClassificationFlags & lazy.tpFlagsMask) == 0
+ );
+}
+
+/**
+ * Retrieve the websocket channel for the provided channel, if available.
+ * Returns null otherwise.
+ *
+ * @param {nsIChannel} channel
+ * @returns {nsIWebSocketChannel|null}
+ */
+function getWebSocketChannel(channel) {
+ let wsChannel = null;
+ if (channel.notificationCallbacks) {
+ try {
+ wsChannel = channel.notificationCallbacks.QueryInterface(
+ Ci.nsIWebSocketChannel
+ );
+ } catch (e) {
+ // Not all channels implement nsIWebSocketChannel.
+ }
+ }
+ return wsChannel;
+}
+
+/**
+ * For a given channel, fetch the request's headers and cookies.
+ *
+ * @param {nsIChannel} channel
+ * @return {Object}
+ * An object with two properties:
+ * @property {Array<Object>} cookies
+ * Array of { name, value } objects.
+ * @property {Array<Object>} headers
+ * Array of { name, value } objects.
+ */
+function fetchRequestHeadersAndCookies(channel) {
+ const headers = [];
+ let cookies = [];
+ let cookieHeader = null;
+
+ // Copy the request header data.
+ channel.visitRequestHeaders({
+ visitHeader(name, value) {
+ // The `Proxy-Authorization` header even though it appears on the channel is not
+ // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel
+ // is setup by the proxy.
+ if (name == "Proxy-Authorization") {
+ return;
+ }
+ if (name == "Cookie") {
+ cookieHeader = value;
+ }
+ headers.push({ name, value });
+ },
+ });
+
+ if (cookieHeader) {
+ cookies = lazy.NetworkHelper.parseCookieHeader(cookieHeader);
+ }
+
+ return { cookies, headers };
+}
+
+/**
+ * For a given channel, fetch the response's headers and cookies.
+ *
+ * @param {nsIChannel} channel
+ * @return {Object}
+ * An object with two properties:
+ * @property {Array<Object>} cookies
+ * Array of { name, value } objects.
+ * @property {Array<Object>} headers
+ * Array of { name, value } objects.
+ */
+function fetchResponseHeadersAndCookies(channel) {
+ // Read response headers and cookies.
+ const headers = [];
+ const setCookieHeaders = [];
+
+ const SET_COOKIE_REGEXP = /set-cookie/i;
+ channel.visitOriginalResponseHeaders({
+ visitHeader(name, value) {
+ if (SET_COOKIE_REGEXP.test(name)) {
+ setCookieHeaders.push(value);
+ }
+ headers.push({ name, value });
+ },
+ });
+
+ return {
+ cookies: lazy.NetworkHelper.parseSetCookieHeaders(setCookieHeaders),
+ headers,
+ };
+}
+
+/**
+ * Check if a given network request should be logged by a network monitor
+ * based on the specified filters.
+ *
+ * @param {(nsIHttpChannel|nsIFileChannel)} channel
+ * Request to check.
+ * @param filters
+ * NetworkObserver filters to match against. An object with one of the following attributes:
+ * - sessionContext: When inspecting requests from the parent process, pass the WatcherActor's session context.
+ * This helps know what is the overall debugged scope.
+ * See watcher actor constructor for more info.
+ * - targetActor: When inspecting requests from the content process, pass the WindowGlobalTargetActor.
+ * This helps know what exact subset of request we should accept.
+ * This is especially useful to behave correctly regarding EFT, where we should include or not
+ * iframes requests.
+ * - browserId, addonId, window: All these attributes are legacy.
+ * Only browserId attribute is still used by the legacy WebConsoleActor startListener API.
+ * @return boolean
+ * True if the network request should be logged, false otherwise.
+ */
+function matchRequest(channel, filters) {
+ // NetworkEventWatcher should now pass a session context for the parent process codepath
+ if (filters.sessionContext) {
+ const { type } = filters.sessionContext;
+ if (type == "all") {
+ return true;
+ }
+
+ // Ignore requests from chrome or add-on code when we don't monitor the whole browser
+ if (
+ channel.loadInfo?.loadingDocument === null &&
+ (channel.loadInfo.loadingPrincipal ===
+ Services.scriptSecurityManager.getSystemPrincipal() ||
+ channel.loadInfo.isInDevToolsContext)
+ ) {
+ return false;
+ }
+
+ if (type == "browser-element") {
+ if (!channel.loadInfo.browsingContext) {
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // `topFrame` is typically null for some chrome requests like favicons
+ // And its `browsingContext` attribute might be null if the request happened
+ // while the tab is being closed.
+ return (
+ topFrame?.browsingContext?.browserId ==
+ filters.sessionContext.browserId
+ );
+ }
+ return (
+ channel.loadInfo.browsingContext.browserId ==
+ filters.sessionContext.browserId
+ );
+ }
+ if (type == "webextension") {
+ return (
+ channel.loadInfo?.loadingPrincipal?.addonId ===
+ filters.sessionContext.addonId
+ );
+ }
+ throw new Error("Unsupported session context type: " + type);
+ }
+
+ // NetworkEventContentWatcher and NetworkEventStackTraces pass a target actor instead, from the content processes
+ // Because of EFT, we can't use session context as we have to know what exact windows the target actor covers.
+ if (filters.targetActor) {
+ // Bug 1769982 the target actor might be destroying and accessing windows will throw.
+ // Ignore all further request when this happens.
+ let windows;
+ try {
+ windows = filters.targetActor.windows;
+ } catch (e) {
+ return false;
+ }
+ const win = lazy.NetworkHelper.getWindowForRequest(channel);
+ return windows.includes(win);
+ }
+
+ // This is fallback code for the legacy WebConsole.startListeners codepath,
+ // which may still pass individual browserId/window/addonId attributes.
+ // This should be removable once we drop the WebConsole codepath for network events
+ // (bug 1721592 and followups)
+ return legacyMatchRequest(channel, filters);
+}
+
+function legacyMatchRequest(channel, filters) {
+ // Log everything if no filter is specified
+ if (!filters.browserId && !filters.window && !filters.addonId) {
+ return true;
+ }
+
+ // Ignore requests from chrome or add-on code when we are monitoring
+ // content.
+ if (
+ channel.loadInfo?.loadingDocument === null &&
+ (channel.loadInfo.loadingPrincipal ===
+ Services.scriptSecurityManager.getSystemPrincipal() ||
+ channel.loadInfo.isInDevToolsContext)
+ ) {
+ return false;
+ }
+
+ if (filters.window) {
+ let win = lazy.NetworkHelper.getWindowForRequest(channel);
+ if (filters.matchExactWindow) {
+ return win == filters.window;
+ }
+
+ // Since frames support, this.window may not be the top level content
+ // frame, so that we can't only compare with win.top.
+ while (win) {
+ if (win == filters.window) {
+ return true;
+ }
+ if (win.parent == win) {
+ break;
+ }
+ win = win.parent;
+ }
+ return false;
+ }
+
+ if (filters.browserId) {
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // `topFrame` is typically null for some chrome requests like favicons
+ // And its `browsingContext` attribute might be null if the request happened
+ // while the tab is being closed.
+ if (topFrame?.browsingContext?.browserId == filters.browserId) {
+ return true;
+ }
+
+ // If we couldn't get the top frame BrowsingContext from the loadContext,
+ // look for it on channel.loadInfo instead.
+ if (channel.loadInfo?.browsingContext?.browserId == filters.browserId) {
+ return true;
+ }
+ }
+
+ if (
+ filters.addonId &&
+ channel.loadInfo?.loadingPrincipal?.addonId === filters.addonId
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+function getBlockedReason(channel, fromCache = false) {
+ let blockingExtension, blockedReason;
+ const { status } = channel;
+
+ try {
+ const request = channel.QueryInterface(Ci.nsIHttpChannel);
+ const properties = request.QueryInterface(Ci.nsIPropertyBag);
+
+ blockedReason = request.loadInfo.requestBlockingReason;
+ blockingExtension = properties.getProperty("cancelledByExtension");
+
+ // WebExtensionPolicy is not available for workers
+ if (typeof WebExtensionPolicy !== "undefined") {
+ blockingExtension = WebExtensionPolicy.getByID(blockingExtension).name;
+ }
+ } catch (err) {
+ // "cancelledByExtension" doesn't have to be available.
+ }
+ // These are platform errors which are not exposed to the users,
+ // usually the requests (with these errors) might be displayed with various
+ // other status codes.
+ const ignoreList = [
+ // These are emited when the request is already in the cache.
+ "NS_ERROR_PARSED_DATA_CACHED",
+ // This is emited when there is some issues around images e.g When the img.src
+ // links to a non existent url. This is typically shown as a 404 request.
+ "NS_IMAGELIB_ERROR_FAILURE",
+ // This is emited when there is a redirect. They are shown as 301 requests.
+ "NS_BINDING_REDIRECTED",
+ // E.g Emited by send beacon requests.
+ "NS_ERROR_ABORT",
+ ];
+
+ // NS_BINDING_ABORTED are emmited when request are abruptly halted, these are valid and should not be ignored.
+ // They can also be emmited for requests already cache which have the `cached` status, these should be ignored.
+ if (fromCache) {
+ ignoreList.push("NS_BINDING_ABORTED");
+ }
+
+ // If the request has not failed or is not blocked by a web extension, check for
+ // any errors not on the ignore list. e.g When a host is not found (NS_ERROR_UNKNOWN_HOST).
+ if (
+ blockedReason == 0 &&
+ !Components.isSuccessCode(status) &&
+ !ignoreList.includes(ChromeUtils.getXPCOMErrorName(status))
+ ) {
+ blockedReason = ChromeUtils.getXPCOMErrorName(status);
+ }
+
+ return { blockingExtension, blockedReason };
+}
+
+function getCharset(channel) {
+ const win = lazy.NetworkHelper.getWindowForRequest(channel);
+ return win ? win.document.characterSet : null;
+}
+
+export const NetworkUtils = {
+ causeTypeToString,
+ fetchRequestHeadersAndCookies,
+ fetchResponseHeadersAndCookies,
+ getCauseDetails,
+ getChannelBrowsingContextID,
+ getChannelInnerWindowId,
+ getChannelPriority,
+ getHttpVersion,
+ getProtocol,
+ getReferrerPolicy,
+ getWebSocketChannel,
+ isChannelFromSystemPrincipal,
+ isChannelPrivate,
+ isFromCache,
+ isNavigationRequest,
+ isPreloadRequest,
+ isRedirectedChannel,
+ isThirdPartyTrackingResource,
+ matchRequest,
+ stringToCauseType,
+ getBlockedReason,
+ getCharset,
+};
diff --git a/devtools/shared/network-observer/README.md b/devtools/shared/network-observer/README.md
new file mode 100644
index 0000000000..7c2d41a959
--- /dev/null
+++ b/devtools/shared/network-observer/README.md
@@ -0,0 +1,9 @@
+# Network Observer modules
+
+The NetworkObserver module and associated helpers allow to:
+- monitor network events (requests and responses)
+- block requests
+- throttle responses
+
+The DevTools network-observer modules are used both by DevTools and by WebDriver BiDi, found under /remote.
+Breaking changes should be discussed and reviewed both by devtools and webdriver peers.
diff --git a/devtools/shared/network-observer/WildcardToRegexp.sys.mjs b/devtools/shared/network-observer/WildcardToRegexp.sys.mjs
new file mode 100644
index 0000000000..4c495fdd78
--- /dev/null
+++ b/devtools/shared/network-observer/WildcardToRegexp.sys.mjs
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Converts a URL-like string which might include the `*` character as a wildcard
+ * to a regular expression. They are used to match against actual URLs for the
+ * request blocking feature from DevTools.
+ *
+ * The returned regular expression is case insensitive.
+ *
+ * @param {string} url
+ * A URL-like string which can contain one or several `*` as wildcard
+ * characters.
+ * @return {RegExp}
+ * A regular expression which can be used to match URLs compatible with the
+ * provided url "template".
+ */
+export function wildcardToRegExp(url) {
+ return new RegExp(url.split("*").map(regExpEscape).join(".*"), "i");
+}
+
+/**
+ * Escapes all special RegExp characters in the given string.
+ */
+const regExpEscape = s => {
+ return s.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+};
diff --git a/devtools/shared/network-observer/moz.build b/devtools/shared/network-observer/moz.build
new file mode 100644
index 0000000000..3c782ea9aa
--- /dev/null
+++ b/devtools/shared/network-observer/moz.build
@@ -0,0 +1,21 @@
+# -*- 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/.
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
+
+DevToolsModules(
+ "ChannelMap.sys.mjs",
+ "NetworkAuthListener.sys.mjs",
+ "NetworkHelper.sys.mjs",
+ "NetworkObserver.sys.mjs",
+ "NetworkOverride.sys.mjs",
+ "NetworkResponseListener.sys.mjs",
+ "NetworkThrottleManager.sys.mjs",
+ "NetworkUtils.sys.mjs",
+ "WildcardToRegexp.sys.mjs",
+)
diff --git a/devtools/shared/network-observer/test/browser/browser.toml b/devtools/shared/network-observer/test/browser/browser.toml
new file mode 100644
index 0000000000..3b3b44aaae
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "head.js",
+ "doc_network-observer-missing-service-worker.html",
+ "doc_network-observer.html",
+ "gzipped.sjs",
+ "override.html",
+ "override.js",
+ "serviceworker.js",
+ "sjs_network-auth-listener-test-server.sjs",
+ "sjs_network-observer-test-server.sjs",
+]
+
+["browser_networkobserver.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_networkobserver_auth_listener.js"]
+skip-if = [
+ "debug", # Disabled for frequent leaks in Bug 1873571.
+ "asan",
+]
+
+["browser_networkobserver_invalid_constructor.js"]
+
+["browser_networkobserver_override.js"]
+
+["browser_networkobserver_serviceworker.js"]
+fail-if = ["true"] # Disabled until Bug 1267119 and Bug 1246289
diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver.js b/devtools/shared/network-observer/test/browser/browser_networkobserver.js
new file mode 100644
index 0000000000..8f81ef6f86
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser_networkobserver.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = URL_ROOT + "doc_network-observer.html";
+const REQUEST_URL =
+ URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=html`;
+
+// Check that the NetworkObserver can detect basic requests and calls the
+// onNetworkEvent callback when expected.
+add_task(async function testSingleRequest() {
+ await addTab(TEST_URL);
+
+ const onNetworkEvents = waitForNetworkEvents(REQUEST_URL, 1);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [REQUEST_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ const events = await onNetworkEvents;
+ is(events.length, 1, "Received the expected number of network events");
+});
+
+add_task(async function testMultipleRequests() {
+ await addTab(TEST_URL);
+ const EXPECTED_REQUESTS_COUNT = 5;
+
+ const onNetworkEvents = waitForNetworkEvents(
+ REQUEST_URL,
+ EXPECTED_REQUESTS_COUNT
+ );
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL, EXPECTED_REQUESTS_COUNT],
+ (_url, _count) => {
+ for (let i = 0; i < _count; i++) {
+ content.wrappedJSObject.fetch(_url);
+ }
+ }
+ );
+
+ const events = await onNetworkEvents;
+ is(
+ events.length,
+ EXPECTED_REQUESTS_COUNT,
+ "Received the expected number of network events"
+ );
+});
+
+add_task(async function testOnNetworkEventArguments() {
+ await addTab(TEST_URL);
+
+ const onNetworkEvent = new Promise(resolve => {
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: () => false,
+ onNetworkEvent: (...args) => {
+ resolve(args);
+ return createNetworkEventOwner();
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [REQUEST_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ const args = await onNetworkEvent;
+ is(args.length, 2, "Received two arguments");
+ is(typeof args[0], "object", "First argument is an object");
+ ok(args[1] instanceof Ci.nsIChannel, "Second argument is a channel");
+});
diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js
new file mode 100644
index 0000000000..e3492c10ad
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js
@@ -0,0 +1,386 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+const TEST_URL = URL_ROOT + "doc_network-observer.html";
+const AUTH_URL = URL_ROOT + `sjs_network-auth-listener-test-server.sjs`;
+
+// Correct credentials for sjs_network-auth-listener-test-server.sjs.
+const USERNAME = "guest";
+const PASSWORD = "guest";
+const BAD_PASSWORD = "bad";
+
+// NetworkEventOwner which will cancel all auth prompt requests.
+class AuthCancellingOwner extends NetworkEventOwner {
+ hasAuthPrompt = false;
+
+ onAuthPrompt(authDetails, authCallbacks) {
+ this.hasAuthPrompt = true;
+ authCallbacks.cancelAuthPrompt();
+ }
+}
+
+// NetworkEventOwner which will forward all auth prompt requests to the browser.
+class AuthForwardingOwner extends NetworkEventOwner {
+ hasAuthPrompt = false;
+
+ onAuthPrompt(authDetails, authCallbacks) {
+ this.hasAuthPrompt = true;
+ authCallbacks.forwardAuthPrompt();
+ }
+}
+
+// NetworkEventOwner which will answer provided credentials to auth prompts.
+class AuthCredentialsProvidingOwner extends NetworkEventOwner {
+ hasAuthPrompt = false;
+
+ constructor(channel, username, password) {
+ super();
+
+ this.channel = channel;
+ this.username = username;
+ this.password = password;
+ }
+
+ async onAuthPrompt(authDetails, authCallbacks) {
+ this.hasAuthPrompt = true;
+
+ // Providing credentials immediately can lead to intermittent failures.
+ // TODO: Investigate and remove.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 100));
+
+ await authCallbacks.provideAuthCredentials(this.username, this.password);
+ }
+
+ addResponseContent(content) {
+ super.addResponseContent();
+ this.responseContent = content.text;
+ }
+}
+
+add_task(async function testAuthRequestWithoutListener() {
+ cleanupAuthManager();
+ const tab = await addTab(TEST_URL);
+
+ const events = [];
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
+ onNetworkEvent: event => {
+ const owner = new AuthForwardingOwner();
+ events.push(owner);
+ return owner;
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ const onAuthPrompt = waitForAuthPrompt(tab);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ info("Wait for a network event to be created");
+ await BrowserTestUtils.waitForCondition(() => events.length >= 1);
+ is(events.length, 1, "Received the expected number of network events");
+
+ info("Wait for the auth prompt to be displayed");
+ await onAuthPrompt;
+ Assert.equal(
+ getTabAuthPrompts(tab).length,
+ 1,
+ "The auth prompt was not blocked by the network observer"
+ );
+
+ // The event owner should have been called for ResponseStart and EventTimings
+ assertEventOwner(events[0], {
+ hasResponseStart: true,
+ hasEventTimings: true,
+ hasServerTimings: true,
+ });
+
+ networkObserver.destroy();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testAuthRequestWithForwardingListener() {
+ cleanupAuthManager();
+ const tab = await addTab(TEST_URL);
+
+ const events = [];
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
+ onNetworkEvent: event => {
+ info("waitForNetworkEvents received a new event");
+ const owner = new AuthForwardingOwner();
+ events.push(owner);
+ return owner;
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ info("Enable the auth prompt listener for this network observer");
+ networkObserver.setAuthPromptListenerEnabled(true);
+
+ const onAuthPrompt = waitForAuthPrompt(tab);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ info("Wait for a network event to be received");
+ await BrowserTestUtils.waitForCondition(() => events.length >= 1);
+ is(events.length, 1, "Received the expected number of network events");
+
+ // The auth prompt should still be displayed since the network event owner
+ // forwards the auth notification immediately.
+ info("Wait for the auth prompt to be displayed");
+ await onAuthPrompt;
+ Assert.equal(
+ getTabAuthPrompts(tab).length,
+ 1,
+ "The auth prompt was not blocked by the network observer"
+ );
+
+ // The event owner should have been called for ResponseStart, EventTimings and
+ // AuthPrompt
+ assertEventOwner(events[0], {
+ hasResponseStart: true,
+ hasEventTimings: true,
+ hasAuthPrompt: true,
+ hasServerTimings: true,
+ });
+
+ networkObserver.destroy();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testAuthRequestWithCancellingListener() {
+ cleanupAuthManager();
+ const tab = await addTab(TEST_URL);
+
+ const events = [];
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
+ onNetworkEvent: event => {
+ const owner = new AuthCancellingOwner();
+ events.push(owner);
+ return owner;
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ info("Enable the auth prompt listener for this network observer");
+ networkObserver.setAuthPromptListenerEnabled(true);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ info("Wait for a network event to be received");
+ await BrowserTestUtils.waitForCondition(() => events.length >= 1);
+ is(events.length, 1, "Received the expected number of network events");
+
+ await BrowserTestUtils.waitForCondition(
+ () => events[0].hasResponseContent && events[0].hasSecurityInfo
+ );
+
+ // The auth prompt should not be displayed since the authentication was
+ // cancelled.
+ ok(
+ !getTabAuthPrompts(tab).length,
+ "The auth prompt was cancelled by the network event owner"
+ );
+
+ assertEventOwner(events[0], {
+ hasResponseStart: true,
+ hasResponseContent: true,
+ hasEventTimings: true,
+ hasServerTimings: true,
+ hasAuthPrompt: true,
+ hasSecurityInfo: true,
+ });
+
+ networkObserver.destroy();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testAuthRequestWithWrongCredentialsListener() {
+ cleanupAuthManager();
+ const tab = await addTab(TEST_URL);
+
+ const events = [];
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
+ onNetworkEvent: (event, channel) => {
+ const owner = new AuthCredentialsProvidingOwner(
+ channel,
+ USERNAME,
+ BAD_PASSWORD
+ );
+ events.push(owner);
+ return owner;
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ info("Enable the auth prompt listener for this network observer");
+ networkObserver.setAuthPromptListenerEnabled(true);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ info("Wait for all network events to be received");
+ await BrowserTestUtils.waitForCondition(() => events.length >= 1);
+ is(events.length, 1, "Received the expected number of network events");
+
+ // Wait for authPrompt to be handled
+ await BrowserTestUtils.waitForCondition(() => events[0].hasAuthPrompt);
+
+ // The auth prompt should not be displayed since the authentication was
+ // fulfilled.
+ ok(
+ !getTabAuthPrompts(tab).length,
+ "The auth prompt was handled by the network event owner"
+ );
+
+ assertEventOwner(events[0], {
+ hasAuthPrompt: true,
+ hasResponseStart: true,
+ hasEventTimings: true,
+ hasServerTimings: true,
+ });
+
+ networkObserver.destroy();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testAuthRequestWithCredentialsListener() {
+ cleanupAuthManager();
+ const tab = await addTab(TEST_URL);
+
+ const events = [];
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
+ onNetworkEvent: (event, channel) => {
+ const owner = new AuthCredentialsProvidingOwner(
+ channel,
+ USERNAME,
+ PASSWORD
+ );
+ events.push(owner);
+ return owner;
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ info("Enable the auth prompt listener for this network observer");
+ networkObserver.setAuthPromptListenerEnabled(true);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
+ content.wrappedJSObject.fetch(_url);
+ });
+
+ // TODO: At the moment, providing credentials will result in additional
+ // network events collected by the NetworkObserver, whereas we would expect
+ // to keep the same event.
+ // For successful auth prompts, we receive an additional event.
+ // The last event will contain the responseContent flag.
+ info("Wait for all network events to be received");
+ await BrowserTestUtils.waitForCondition(() => events.length >= 2);
+ is(events.length, 2, "Received the expected number of network events");
+
+ // Since the auth prompt was canceled we should also receive the security
+ // information and the response content.
+ await BrowserTestUtils.waitForCondition(
+ () => events[1].hasResponseContent && events[1].hasSecurityInfo
+ );
+
+ // The auth prompt should not be displayed since the authentication was
+ // fulfilled.
+ ok(
+ !getTabAuthPrompts(tab).length,
+ "The auth prompt was handled by the network event owner"
+ );
+
+ assertEventOwner(events[1], {
+ hasResponseStart: true,
+ hasEventTimings: true,
+ hasSecurityInfo: true,
+ hasServerTimings: true,
+ hasResponseContent: true,
+ });
+
+ is(events[1].responseContent, "success", "Auth prompt was successful");
+
+ networkObserver.destroy();
+ gBrowser.removeTab(tab);
+});
+
+function assertEventOwner(event, expectedFlags) {
+ is(
+ event.hasResponseStart,
+ !!expectedFlags.hasResponseStart,
+ "network event has the expected ResponseStart flag"
+ );
+ is(
+ event.hasEventTimings,
+ !!expectedFlags.hasEventTimings,
+ "network event has the expected EventTimings flag"
+ );
+ is(
+ event.hasAuthPrompt,
+ !!expectedFlags.hasAuthPrompt,
+ "network event has the expected AuthPrompt flag"
+ );
+ is(
+ event.hasResponseCache,
+ !!expectedFlags.hasResponseCache,
+ "network event has the expected ResponseCache flag"
+ );
+ is(
+ event.hasResponseContent,
+ !!expectedFlags.hasResponseContent,
+ "network event has the expected ResponseContent flag"
+ );
+ is(
+ event.hasSecurityInfo,
+ !!expectedFlags.hasSecurityInfo,
+ "network event has the expected SecurityInfo flag"
+ );
+ is(
+ event.hasServerTimings,
+ !!expectedFlags.hasServerTimings,
+ "network event has the expected ServerTimings flag"
+ );
+}
+
+function getTabAuthPrompts(tab) {
+ const tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser);
+ return tabDialogBox
+ .getTabDialogManager()
+ ._dialogs.filter(
+ d => d.frameContentWindow?.Dialog.args.promptType == "promptUserAndPass"
+ );
+}
+
+function waitForAuthPrompt(tab) {
+ return PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "promptUserAndPass",
+ });
+}
+
+// Cleanup potentially stored credentials before running any test.
+function cleanupAuthManager() {
+ const authManager = SpecialPowers.Cc[
+ "@mozilla.org/network/http-auth-manager;1"
+ ].getService(SpecialPowers.Ci.nsIHttpAuthManager);
+ authManager.clearAll();
+}
diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js
new file mode 100644
index 0000000000..76a93d938a
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the NetworkObserver constructor validates its arguments.
+add_task(async function testInvalidConstructorArguments() {
+ Assert.throws(
+ () => new NetworkObserver(),
+ /Expected "ignoreChannelFunction" to be a function, got undefined/,
+ "NetworkObserver constructor should throw if no argument was provided"
+ );
+
+ Assert.throws(
+ () => new NetworkObserver({}),
+ /Expected "ignoreChannelFunction" to be a function, got undefined/,
+ "NetworkObserver constructor should throw if ignoreChannelFunction was not provided"
+ );
+
+ const invalidValues = [null, true, false, 12, "str", ["arr"], { obj: "obj" }];
+ for (const invalidValue of invalidValues) {
+ Assert.throws(
+ () => new NetworkObserver({ ignoreChannelFunction: invalidValue }),
+ /Expected "ignoreChannelFunction" to be a function, got/,
+ `NetworkObserver constructor should throw if a(n) ${typeof invalidValue} was provided for ignoreChannelFunction`
+ );
+ }
+
+ const EMPTY_FN = () => {};
+ Assert.throws(
+ () => new NetworkObserver({ ignoreChannelFunction: EMPTY_FN }),
+ /Expected "onNetworkEvent" to be a function, got undefined/,
+ "NetworkObserver constructor should throw if onNetworkEvent was not provided"
+ );
+
+ // Now we will pass a function for `ignoreChannelFunction`, and will do the
+ // same tests for onNetworkEvent
+ for (const invalidValue of invalidValues) {
+ Assert.throws(
+ () =>
+ new NetworkObserver({
+ ignoreChannelFunction: EMPTY_FN,
+ onNetworkEvent: invalidValue,
+ }),
+ /Expected "onNetworkEvent" to be a function, got/,
+ `NetworkObserver constructor should throw if a(n) ${typeof invalidValue} was provided for onNetworkEvent`
+ );
+ }
+});
diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js
new file mode 100644
index 0000000000..3b00c4b2e9
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = URL_ROOT + "doc_network-observer.html";
+const REQUEST_URL =
+ URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=html`;
+const GZIPPED_REQUEST_URL = URL_ROOT + `gzipped.sjs`;
+const OVERRIDE_FILENAME = "override.js";
+const OVERRIDE_HTML_FILENAME = "override.html";
+
+add_task(async function testLocalOverride() {
+ await addTab(TEST_URL);
+
+ let eventsCount = 0;
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== REQUEST_URL,
+ onNetworkEvent: event => {
+ info("received a network event");
+ eventsCount++;
+ return createNetworkEventOwner(event);
+ },
+ });
+
+ const overrideFile = getChromeDir(getResolvedURI(gTestPath));
+ overrideFile.append(OVERRIDE_FILENAME);
+ info(" override " + REQUEST_URL + " to " + overrideFile.path + "\n");
+ networkObserver.override(REQUEST_URL, overrideFile.path);
+
+ info("Assert that request and cached request are overriden");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL],
+ async _url => {
+ const request = await content.wrappedJSObject.fetch(_url);
+ const requestcontent = await request.text();
+ is(
+ requestcontent,
+ `"use strict";\ndocument.title = "evaluated";\n`,
+ "the request content has been overriden"
+ );
+ const secondRequest = await content.wrappedJSObject.fetch(_url);
+ const secondRequestcontent = await secondRequest.text();
+ is(
+ secondRequestcontent,
+ `"use strict";\ndocument.title = "evaluated";\n`,
+ "the cached request content has been overriden"
+ );
+ }
+ );
+
+ info("Assert that JS scripts can be overriden");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL],
+ async _url => {
+ const script = await content.document.createElement("script");
+ const onLoad = new Promise(resolve =>
+ script.addEventListener("load", resolve, { once: true })
+ );
+ script.src = _url;
+ content.document.body.appendChild(script);
+ await onLoad;
+ is(
+ content.document.title,
+ "evaluated",
+ "The <script> tag content has been overriden and correctly evaluated"
+ );
+ }
+ );
+
+ await BrowserTestUtils.waitForCondition(() => eventsCount >= 1);
+
+ networkObserver.destroy();
+});
+
+add_task(async function testHtmlFileOverride() {
+ let eventsCount = 0;
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== TEST_URL,
+ onNetworkEvent: event => {
+ info("received a network event");
+ eventsCount++;
+ return createNetworkEventOwner(event);
+ },
+ });
+
+ const overrideFile = getChromeDir(getResolvedURI(gTestPath));
+ overrideFile.append(OVERRIDE_HTML_FILENAME);
+ info(" override " + TEST_URL + " to " + overrideFile.path + "\n");
+ networkObserver.override(TEST_URL, overrideFile.path);
+
+ await addTab(TEST_URL);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [TEST_URL],
+ async pageUrl => {
+ is(
+ content.document.documentElement.outerHTML,
+ "<html><head></head><body>Overriden!\n</body></html>",
+ "The content of the HTML has been overriden"
+ );
+ // For now, all overriden request have their location changed to an internal data: URI
+ // Bug xxx aims at keeping the original URI.
+ todo_is(
+ content.location.href,
+ pageUrl,
+ "The location of the page is still the original one"
+ );
+ }
+ );
+ await BrowserTestUtils.waitForCondition(() => eventsCount >= 1);
+ networkObserver.destroy();
+});
+
+// Exact same test, but with a gzipped request, which requires very special treatment
+add_task(async function testLocalOverrideGzipped() {
+ await addTab(TEST_URL);
+
+ let eventsCount = 0;
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== GZIPPED_REQUEST_URL,
+ onNetworkEvent: event => {
+ info("received a network event");
+ eventsCount++;
+ return createNetworkEventOwner(event);
+ },
+ });
+
+ const overrideFile = getChromeDir(getResolvedURI(gTestPath));
+ overrideFile.append(OVERRIDE_FILENAME);
+ info(" override " + GZIPPED_REQUEST_URL + " to " + overrideFile.path + "\n");
+ networkObserver.override(GZIPPED_REQUEST_URL, overrideFile.path);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [GZIPPED_REQUEST_URL],
+ async _url => {
+ const request = await content.wrappedJSObject.fetch(_url);
+ const requestcontent = await request.text();
+ is(
+ requestcontent,
+ `"use strict";\ndocument.title = "evaluated";\n`,
+ "the request content has been overriden"
+ );
+ const secondRequest = await content.wrappedJSObject.fetch(_url);
+ const secondRequestcontent = await secondRequest.text();
+ is(
+ secondRequestcontent,
+ `"use strict";\ndocument.title = "evaluated";\n`,
+ "the cached request content has been overriden"
+ );
+ }
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [GZIPPED_REQUEST_URL],
+ async _url => {
+ const script = await content.document.createElement("script");
+ const onLoad = new Promise(resolve =>
+ script.addEventListener("load", resolve, { once: true })
+ );
+ script.src = _url;
+ content.document.body.appendChild(script);
+ await onLoad;
+ is(
+ content.document.title,
+ "evaluated",
+ "The <script> tag content has been overriden and correctly evaluated"
+ );
+ }
+ );
+
+ await BrowserTestUtils.waitForCondition(() => eventsCount >= 1);
+
+ networkObserver.destroy();
+});
diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js
new file mode 100644
index 0000000000..1680dc6005
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that all the expected service worker requests are received
+// by the network observer.
+add_task(async function testServiceWorkerSuccessRequests() {
+ await addTab(URL_ROOT + "doc_network-observer.html");
+
+ const REQUEST_URL =
+ URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=`;
+
+ const EXPECTED_REQUESTS = [
+ // The main service worker script request
+ `https://example.com/browser/devtools/shared/network-observer/test/browser/serviceworker.js`,
+ // The requests intercepted by the service worker
+ REQUEST_URL + "js",
+ REQUEST_URL + "css",
+ // The request initiated by the service worker
+ REQUEST_URL + "json",
+ ];
+
+ const onNetworkEvents = waitForNetworkEvents(null, 4);
+
+ info("Register the service worker and send requests...");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL],
+ async url => {
+ await content.wrappedJSObject.registerServiceWorker();
+ content.wrappedJSObject.fetch(url + "js");
+ content.wrappedJSObject.fetch(url + "css");
+ }
+ );
+ const events = await onNetworkEvents;
+
+ is(events.length, 4, "Received the expected number of network events");
+ for (const { options, channel } of events) {
+ info(`Assert the info for the request from ${channel.URI.spec}`);
+ ok(
+ EXPECTED_REQUESTS.includes(channel.URI.spec),
+ `The request for ${channel.URI.spec} is an expected service worker request`
+ );
+ Assert.notStrictEqual(
+ channel.loadInfo.browsingContextID,
+ 0,
+ `The request for ${channel.URI.spec} has a Browsing Context ID of ${channel.loadInfo.browsingContextID}`
+ );
+ // The main service worker script request is not from the service worker
+ if (channel.URI.spec.includes("serviceworker.js")) {
+ ok(
+ !options.fromServiceWorker,
+ `The request for ${channel.URI.spec} is not from the service worker\n`
+ );
+ } else {
+ ok(
+ options.fromServiceWorker,
+ `The request for ${channel.URI.spec} is from the service worker\n`
+ );
+ }
+ }
+
+ info("Unregistering the service worker...");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ await content.wrappedJSObject.unregisterServiceWorker();
+ });
+});
+
+// Tests that the expected failed service worker request is received by the network observer.
+add_task(async function testServiceWorkerFailedRequests() {
+ await addTab(URL_ROOT + "doc_network-observer-missing-service-worker.html");
+
+ const REQUEST_URL =
+ URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=js`;
+
+ const EXPECTED_REQUESTS = [
+ // The main service worker script request which should be missing
+ "https://example.com/browser/devtools/shared/network-observer/test/browser/serviceworker-missing.js",
+ // A notrmal request
+ "https://example.com/browser/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs?sts=200&fmt=js",
+ ];
+
+ const onNetworkEvents = waitForNetworkEvents(null, 2);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL],
+ async url => {
+ await content.wrappedJSObject.registerServiceWorker();
+ content.wrappedJSObject.fetch(url);
+ }
+ );
+
+ const events = await onNetworkEvents;
+ is(events.length, 2, "Received the expected number of network events");
+
+ for (const { options, channel } of events) {
+ info(`Assert the info for the request from ${channel.URI.spec}`);
+ ok(
+ EXPECTED_REQUESTS.includes(channel.URI.spec),
+ `The request for ${channel.URI.spec} is an expected request`
+ );
+ Assert.notStrictEqual(
+ channel.loadInfo.browsingContextID,
+ 0,
+ `The request for ${channel.URI.spec} has a Browsing Context ID of ${channel.loadInfo.browsingContextID}`
+ );
+ ok(
+ !options.fromServiceWorker,
+ `The request for ${channel.URI.spec} is not from the service worker\n`
+ );
+ }
+});
diff --git a/devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html b/devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html
new file mode 100644
index 0000000000..396e51677c
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Observer missing service worker test page</title>
+ </head>
+
+ <body>
+ <p>Network Observer test page</p>
+ <script type="text/javascript">
+ /* exported registerServiceWorker */
+ "use strict";
+
+ function registerServiceWorker() {
+ const sw = navigator.serviceWorker;
+ // NOTE: This service worker file does not exist which enables testing
+ // that a 404 requests is received.
+ return sw.register("serviceworker-missing.js")
+ .then(registration => {
+ throw new Error("The Service Worker file should not exist");
+ }).catch(err => {
+ console.log("Registration failed as expected");
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/network-observer/test/browser/doc_network-observer.html b/devtools/shared/network-observer/test/browser/doc_network-observer.html
new file mode 100644
index 0000000000..2ca400e0ae
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/doc_network-observer.html
@@ -0,0 +1,49 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Observer test page</title>
+ </head>
+ <body>
+ <p>Network Observer test page</p>
+ <script type="text/javascript">
+ /* exported registerServiceWorker, unregisterServiceWorker */
+ "use strict";
+
+ let swRegistration;
+
+ function registerServiceWorker() {
+ const sw = navigator.serviceWorker;
+ return sw.register("serviceworker.js")
+ .then(registration => {
+ swRegistration = registration;
+ console.log("Registered, scope is:", registration.scope);
+ return sw.ready;
+ }).then(() => {
+ // wait until the page is controlled
+ return new Promise(resolve => {
+ if (sw.controller) {
+ resolve();
+ } else {
+ sw.addEventListener("controllerchange", function () {
+ resolve();
+ }, { once: true });
+ }
+ });
+ }).catch(err => {
+ console.error("Registration failed");
+ });
+ }
+
+ function unregisterServiceWorker() {
+ return swRegistration.unregister();
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/network-observer/test/browser/gzipped.sjs b/devtools/shared/network-observer/test/browser/gzipped.sjs
new file mode 100644
index 0000000000..09d0b249b1
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/gzipped.sjs
@@ -0,0 +1,44 @@
+"use strict";
+
+function gzipCompressString(string, obs) {
+ const scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
+ Ci.nsIStreamLoader
+ );
+ listener.init(obs);
+ const converter = scs.asyncConvertData(
+ "uncompressed",
+ "gzip",
+ listener,
+ null
+ );
+ const stringStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ stringStream.data = string;
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, stringStream, 0, string.length);
+ converter.onStopRequest(null, null, null);
+}
+
+const ORIGINAL_JS_CONTENT = `console.log("original javascript content");`;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ // Generate data
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.setHeader("Content-Encoding", "gzip", false);
+
+ const observer = {
+ onStreamComplete(loader, context, status, length, result) {
+ const buffer = String.fromCharCode.apply(this, result);
+ response.setHeader("Content-Length", "" + buffer.length, false);
+ response.write(buffer);
+ response.finish();
+ },
+ };
+ gzipCompressString(ORIGINAL_JS_CONTENT, observer);
+}
diff --git a/devtools/shared/network-observer/test/browser/head.js b/devtools/shared/network-observer/test/browser/head.js
new file mode 100644
index 0000000000..deb7becff6
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/head.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NetworkObserver:
+ "resource://devtools/shared/network-observer/NetworkObserver.sys.mjs",
+});
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+const URL_ROOT = CHROME_URL_ROOT.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+/**
+ * Load the provided url in an existing browser.
+ * Returns a promise which will resolve when the page is loaded.
+ *
+ * @param {Browser} browser
+ * The browser element where the URL should be loaded.
+ * @param {String} url
+ * The URL to load in the new tab
+ */
+async function loadURL(browser, url) {
+ const loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ return loaded;
+}
+
+/**
+ * Create a new foreground tab loading the provided url.
+ * Returns a promise which will resolve when the page is loaded.
+ *
+ * @param {String} url
+ * The URL to load in the new tab
+ */
+async function addTab(url) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ registerCleanupFunction(() => {
+ gBrowser.removeTab(tab);
+ });
+ return tab;
+}
+
+/**
+ * Base network event owner class implementing all mandatory callbacks and
+ * keeping track of which callbacks have been called.
+ */
+class NetworkEventOwner {
+ hasEventTimings = false;
+ hasResponseCache = false;
+ hasResponseContent = false;
+ hasResponseStart = false;
+ hasSecurityInfo = false;
+ hasServerTimings = false;
+
+ addEventTimings() {
+ this.hasEventTimings = true;
+ }
+ addResponseCache() {
+ this.hasResponseCache = true;
+ }
+ addResponseContent() {
+ this.hasResponseContent = true;
+ }
+ addResponseStart() {
+ this.hasResponseStart = true;
+ }
+ addSecurityInfo() {
+ this.hasSecurityInfo = true;
+ }
+ addServerTimings() {
+ this.hasServerTimings = true;
+ }
+ addServiceWorkerTimings() {
+ this.hasServiceWorkerTimings = true;
+ }
+}
+
+/**
+ * Create a simple network event owner, with mock implementations of all
+ * the expected APIs for a NetworkEventOwner.
+ */
+function createNetworkEventOwner(event) {
+ return new NetworkEventOwner();
+}
+
+/**
+ * Wait for network events matching the provided URL, until the count reaches
+ * the provided expected count.
+ *
+ * @param {string|null} expectedUrl
+ * The URL which should be monitored by the NetworkObserver.If set to null watch for
+ * all requests
+ * @param {number} expectedRequestsCount
+ * How many different events (requests) are expected.
+ * @returns {Promise}
+ * A promise which will resolve with an array of network event owners, when
+ * the expected event count is reached.
+ */
+async function waitForNetworkEvents(expectedUrl = null, expectedRequestsCount) {
+ const events = [];
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel =>
+ expectedUrl ? channel.URI.spec !== expectedUrl : false,
+ onNetworkEvent: () => {
+ info("waitForNetworkEvents received a new event");
+ const owner = createNetworkEventOwner();
+ events.push(owner);
+ return owner;
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ info("Wait until the events count reaches " + expectedRequestsCount);
+ await BrowserTestUtils.waitForCondition(
+ () => events.length >= expectedRequestsCount
+ );
+ return events;
+}
diff --git a/devtools/shared/network-observer/test/browser/override.html b/devtools/shared/network-observer/test/browser/override.html
new file mode 100644
index 0000000000..0e3878e313
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/override.html
@@ -0,0 +1 @@
+<html><head></head><body>Overriden!</body></html>
diff --git a/devtools/shared/network-observer/test/browser/override.js b/devtools/shared/network-observer/test/browser/override.js
new file mode 100644
index 0000000000..7b000fcd0f
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/override.js
@@ -0,0 +1,2 @@
+"use strict";
+document.title = "evaluated";
diff --git a/devtools/shared/network-observer/test/browser/serviceworker.js b/devtools/shared/network-observer/test/browser/serviceworker.js
new file mode 100644
index 0000000000..3389581fb0
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/serviceworker.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+self.addEventListener("activate", async event => {
+ (
+ await fetch(
+ "https://example.com/browser/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs?sts=200&fmt=json"
+ )
+ )
+ .json()
+ .then(() => console.log("json downloaded"));
+ // start controlling the already loaded page
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("fetch", event => {
+ const response = new Response("Service worker response", {
+ statusText: "OK",
+ });
+ event.respondWith(response);
+});
diff --git a/devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs b/devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs
new file mode 100644
index 0000000000..028a26ebfe
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function handleRequest(request, response) {
+ let body;
+
+ // Expect guest/guest as correct credentials, but `btoa` is unavailable in sjs
+ // "Z3Vlc3Q6Z3Vlc3Q=" == btoa("guest:guest")
+ const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+
+ // correct login credentials provided
+ if (
+ request.hasHeader("Authorization") &&
+ request.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(request.httpVersion, 200, "OK, authorized");
+ response.setHeader("Content-Type", "text", false);
+
+ body = "success";
+ } else {
+ // incorrect credentials
+ response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ response.setHeader("Content-Type", "text", false);
+
+ body = "failed";
+ }
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs
new file mode 100644
index 0000000000..b0947cadd1
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple server which can handle several response types and states.
+// Trimmed down from devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
+// Additional features can be ported if needed.
+function handleRequest(request, response) {
+ response.processAsync();
+
+ const params = request.queryString.split("&");
+ const format = (params.filter(s => s.includes("fmt="))[0] || "").split(
+ "="
+ )[1];
+ const status =
+ (params.filter(s => s.includes("sts="))[0] || "").split("=")[1] || 200;
+
+ const cacheExpire = 60; // seconds
+
+ function setCacheHeaders() {
+ if (status != 304) {
+ response.setHeader(
+ "Cache-Control",
+ "no-cache, no-store, must-revalidate"
+ );
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ return;
+ }
+
+ response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false);
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ timer.initWithCallback(
+ // eslint-disable-next-line complexity
+ () => {
+ // to avoid garbage collection
+ timer = null;
+ switch (format) {
+ case "txt": {
+ response.setStatusLine(request.httpVersion, status, "DA DA DA");
+ response.setHeader("Content-Type", "text/plain", false);
+ setCacheHeaders();
+
+ function convertToUtf8(str) {
+ return String.fromCharCode(...new TextEncoder().encode(str));
+ }
+
+ // This script must be evaluated as UTF-8 for this to write out the
+ // bytes of the string in UTF-8. If it's evaluated as Latin-1, the
+ // written bytes will be the result of UTF-8-encoding this string
+ // *twice*.
+ const data = "Братан, ты вообще качаешься?";
+ const stringOfUtf8Bytes = convertToUtf8(data);
+ response.write(stringOfUtf8Bytes);
+
+ response.finish();
+ break;
+ }
+ case "xml": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<label value='greeting'>Hello XML!</label>");
+ response.finish();
+ break;
+ }
+ case "html": {
+ const content = (
+ params.filter(s => s.includes("res="))[0] || ""
+ ).split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(content || "<p>Hello HTML!</p>");
+ response.finish();
+ break;
+ }
+ case "xhtml": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader(
+ "Content-Type",
+ "application/xhtml+xml; charset=utf-8",
+ false
+ );
+ setCacheHeaders();
+ response.write("<p>Hello XHTML!</p>");
+ response.finish();
+ break;
+ }
+ case "html-long": {
+ const str = new Array(102400 /* 100 KB in bytes */).join(".");
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<p>" + str + "</p>");
+ response.finish();
+ break;
+ }
+ case "css": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/css; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("body:pre { content: 'Hello CSS!' }");
+ response.finish();
+ break;
+ }
+ case "js": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader(
+ "Content-Type",
+ "application/javascript; charset=utf-8",
+ false
+ );
+ setCacheHeaders();
+ response.write("function() { return 'Hello JS!'; }");
+ response.finish();
+ break;
+ }
+ case "json": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ setCacheHeaders();
+ response.write('{ "greeting": "Hello JSON!" }');
+ response.finish();
+ break;
+ }
+
+ case "font": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "font/woff", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "image": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "application-ogg": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/ogg", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "audio": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "audio/ogg", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "video": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "video/webm", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "ws": {
+ response.setStatusLine(
+ request.httpVersion,
+ 101,
+ "Switching Protocols"
+ );
+ response.setHeader("Connection", "upgrade", false);
+ response.setHeader("Upgrade", "websocket", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ default: {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<blink>Not Found</blink>");
+ response.finish();
+ break;
+ }
+ }
+ },
+ 10,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ ); // Make sure this request takes a few ms.
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/head.js b/devtools/shared/network-observer/test/xpcshell/head.js
new file mode 100644
index 0000000000..93b66e4632
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/head.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_network_helper.js b/devtools/shared/network-observer/test/xpcshell/test_network_helper.js
new file mode 100644
index 0000000000..ff514ab98e
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_network_helper.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+ test_isTextMimeType();
+ test_parseCookieHeader();
+}
+
+function test_isTextMimeType() {
+ Assert.equal(NetworkHelper.isTextMimeType("text/plain"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("application/javascript"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("application/json"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("text/css"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("text/html"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("image/svg+xml"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("application/xml"), true);
+
+ // Test custom JSON subtype
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+json"),
+ true
+ );
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-json"),
+ true
+ );
+ // Test custom XML subtype
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+xml"),
+ true
+ );
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-xml"),
+ false
+ );
+ // Test case-insensitive
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.BIG-CORP+json"),
+ true
+ );
+ // Test non-text type
+ Assert.equal(NetworkHelper.isTextMimeType("image/png"), false);
+ // Test invalid types
+ Assert.equal(NetworkHelper.isTextMimeType("application/foo-+json"), false);
+ Assert.equal(NetworkHelper.isTextMimeType("application/-foo+json"), false);
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/foo--bar+json"),
+ false
+ );
+
+ // Test we do not cause internal errors with unoptimized regex. Bug 961097
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.google.safebrowsing-chunk"),
+ false
+ );
+}
+
+function test_parseCookieHeader() {
+ let result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=Strict"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Strict" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=strict"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Strict" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=STRICT"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Strict" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=None"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "None" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=NONE"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "None" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=lax"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Lax" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=Lax"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Lax" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders([
+ "Test=1; SameSite=Lax",
+ "Foo=2; SameSite=None",
+ ]);
+ Assert.deepEqual(result, [
+ { name: "Test", value: "1", samesite: "Lax" },
+ { name: "Foo", value: "2", samesite: "None" },
+ ]);
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js
new file mode 100644
index 0000000000..00f482b8ca
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.parseCertificateInfo parses certificate information
+// correctly.
+
+const DUMMY_CERT = {
+ getBase64DERString() {
+ // This is the base64-encoded contents of the "DigiCert ECC Secure Server CA"
+ // intermediate certificate as issued by "DigiCert Global Root CA". It was
+ // chosen as a test certificate because it has an issuer common name,
+ // organization, and organizational unit that are somewhat distinct from
+ // its subject common name and organization name.
+ return "MIIDrDCCApSgAwIBAgIQCssoukZe5TkIdnRw883GEjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJjAkBgNVBAMTHURpZ2lDZXJ0IEVDQyBTZWN1cmUgU2VydmVyIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ghC6nfYJN6gLGSkE85AnCNyqQIKDjc/ITa4jVMU9tWRlUvzlgKNcR7E2Munn17voOZ/WpIRllNv68DLP679Wz9HJOeaBy6Wvqgvu1cYr3GkvXg6HuhbPGtkESvMNCuMo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAdBgNVHQ4EFgQUo53mH/naOU/AbuiRy5Wl2jHiCp8wHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEMBQADggEBAMeKoENL7HTJxavVHzA1Nm6YVntIrAVjrnuaVyRXzG/63qttnMe2uuzO58pzZNvfBDcKAEmzP58mrZGMIOgfiA4q+2Y3yDDo0sIkp0VILeoBUEoxlBPfjV/aKrtJPGHzecicZpIalir0ezZYoyxBEHQa0+1IttK7igZFcTMQMHp6mCHdJLnsnLWSB62DxsRq+HfmNb4TDydkskO/g+l3VtsIh5RHFPVfKK+jaEyDj2D3loB5hWp2Jp2VDCADjT7ueihlZGak2YPqmXTNbk19HOuNssWvFhtOyPNV6og4ETQdEa8/B6hPatJ0ES8q/HO3X8IVQwVs1n3aAr0im0/T+Xc=";
+ },
+};
+
+add_task(async function run_test() {
+ info("Testing NetworkHelper.parseCertificateInfo.");
+
+ const result = await NetworkHelper.parseCertificateInfo(
+ DUMMY_CERT,
+ new Map()
+ );
+
+ // Subject
+ equal(
+ result.subject.commonName,
+ "DigiCert ECC Secure Server CA",
+ "Common name is correct."
+ );
+ equal(
+ result.subject.organization,
+ "DigiCert Inc",
+ "Organization is correct."
+ );
+ equal(
+ result.subject.organizationUnit,
+ undefined,
+ "Organizational unit is correct."
+ );
+
+ // Issuer
+ equal(
+ result.issuer.commonName,
+ "DigiCert Global Root CA",
+ "Common name of the issuer is correct."
+ );
+ equal(
+ result.issuer.organization,
+ "DigiCert Inc",
+ "Organization of the issuer is correct."
+ );
+ equal(
+ result.issuer.organizationUnit,
+ "www.digicert.com",
+ "Organizational unit of the issuer is correct."
+ );
+
+ // Validity
+ equal(
+ result.validity.start,
+ "Fri, 08 Mar 2013 12:00:00 GMT",
+ "Start of the validity period is correct."
+ );
+ equal(
+ result.validity.end,
+ "Wed, 08 Mar 2023 12:00:00 GMT",
+ "End of the validity period is correct."
+ );
+
+ // Fingerprints
+ equal(
+ result.fingerprint.sha1,
+ "56:EE:7C:27:06:83:16:2D:83:BA:EA:CC:79:0E:22:47:1A:DA:AB:E8",
+ "Certificate SHA1 fingerprint is correct."
+ );
+ equal(
+ result.fingerprint.sha256,
+ "45:84:46:BA:75:D9:32:E9:14:F2:3C:2B:57:B7:D1:92:ED:DB:C2:18:1D:95:8E:11:81:AD:52:51:74:7A:1E:E8",
+ "Certificate SHA256 fingerprint is correct."
+ );
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js
new file mode 100644
index 0000000000..9515851a8b
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that NetworkHelper.parseSecurityInfo returns correctly formatted object.
+
+const wpl = Ci.nsIWebProgressListener;
+const MockCertificate = {
+ getBase64DERString() {
+ // This is the same test certificate as in
+ // test_security-info-certificate.js for consistency.
+ return "MIIDrDCCApSgAwIBAgIQCssoukZe5TkIdnRw883GEjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJjAkBgNVBAMTHURpZ2lDZXJ0IEVDQyBTZWN1cmUgU2VydmVyIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ghC6nfYJN6gLGSkE85AnCNyqQIKDjc/ITa4jVMU9tWRlUvzlgKNcR7E2Munn17voOZ/WpIRllNv68DLP679Wz9HJOeaBy6Wvqgvu1cYr3GkvXg6HuhbPGtkESvMNCuMo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAdBgNVHQ4EFgQUo53mH/naOU/AbuiRy5Wl2jHiCp8wHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEMBQADggEBAMeKoENL7HTJxavVHzA1Nm6YVntIrAVjrnuaVyRXzG/63qttnMe2uuzO58pzZNvfBDcKAEmzP58mrZGMIOgfiA4q+2Y3yDDo0sIkp0VILeoBUEoxlBPfjV/aKrtJPGHzecicZpIalir0ezZYoyxBEHQa0+1IttK7igZFcTMQMHp6mCHdJLnsnLWSB62DxsRq+HfmNb4TDydkskO/g+l3VtsIh5RHFPVfKK+jaEyDj2D3loB5hWp2Jp2VDCADjT7ueihlZGak2YPqmXTNbk19HOuNssWvFhtOyPNV6og4ETQdEa8/B6hPatJ0ES8q/HO3X8IVQwVs1n3aAr0im0/T+Xc=";
+ },
+};
+
+// This *cannot* be used as an nsITransportSecurityInfo (since that interface is
+// builtinclass) but the methods being tested aren't defined by XPCOM and aren't
+// calling QueryInterface, so this usage is fine.
+const MockSecurityInfo = {
+ securityState: wpl.STATE_IS_SECURE,
+ errorCode: 0,
+ cipherName: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+ // TLS_VERSION_1_2
+ protocolVersion: 3,
+ serverCert: MockCertificate,
+};
+
+add_task(async function run_test() {
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+
+ equal(result.state, "secure", "State is correct.");
+
+ equal(
+ result.cipherSuite,
+ MockSecurityInfo.cipherName,
+ "Cipher suite is correct."
+ );
+
+ equal(result.protocolVersion, "TLSv1.2", "Protocol version is correct.");
+
+ deepEqual(
+ result.cert,
+ await NetworkHelper.parseCertificateInfo(MockCertificate, new Map()),
+ "Certificate information is correct."
+ );
+
+ equal(result.hpkp, false, "HPKP is false when URI is not available.");
+ equal(result.hsts, false, "HSTS is false when URI is not available.");
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js
new file mode 100644
index 0000000000..a81f7ce73c
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.formatSecurityProtocol returns correct
+// protocol version strings.
+
+const TEST_CASES = [
+ {
+ description: "TLS_VERSION_1",
+ input: 1,
+ expected: "TLSv1",
+ },
+ {
+ description: "TLS_VERSION_1.1",
+ input: 2,
+ expected: "TLSv1.1",
+ },
+ {
+ description: "TLS_VERSION_1.2",
+ input: 3,
+ expected: "TLSv1.2",
+ },
+ {
+ description: "TLS_VERSION_1.3",
+ input: 4,
+ expected: "TLSv1.3",
+ },
+ {
+ description: "invalid version",
+ input: -1,
+ expected: "Unknown",
+ },
+];
+
+function run_test() {
+ info("Testing NetworkHelper.formatSecurityProtocol.");
+
+ for (const { description, input, expected } of TEST_CASES) {
+ info("Testing " + description);
+
+ equal(
+ NetworkHelper.formatSecurityProtocol(input),
+ expected,
+ "Got the expected protocol string."
+ );
+ }
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-state.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-state.js
new file mode 100644
index 0000000000..be622b2019
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-state.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that security info parser gives correct general security state for
+// different cases.
+
+const wpl = Ci.nsIWebProgressListener;
+
+// This *cannot* be used as an nsITransportSecurityInfo (since that interface is
+// builtinclass) but the methods being tested aren't defined by XPCOM and aren't
+// calling QueryInterface, so this usage is fine.
+const MockSecurityInfo = {
+ securityState: wpl.STATE_IS_BROKEN,
+ errorCode: 0,
+ // nsISSLStatus.TLS_VERSION_1_2
+ protocolVersion: 3,
+ cipherName: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+};
+
+add_task(async function run_test() {
+ await test_nullSecurityInfo();
+ await test_insecureSecurityInfoWithNSSError();
+ await test_insecureSecurityInfoWithoutNSSError();
+ await test_brokenSecurityInfo();
+ await test_secureSecurityInfo();
+});
+
+/**
+ * Test that undefined security information is returns "insecure".
+ */
+async function test_nullSecurityInfo() {
+ const result = await NetworkHelper.parseSecurityInfo(null, {}, {}, new Map());
+ equal(
+ result.state,
+ "insecure",
+ "state == 'insecure' when securityInfo was undefined"
+ );
+}
+
+/**
+ * Test that STATE_IS_INSECURE with NSSError returns "broken"
+ */
+async function test_insecureSecurityInfoWithNSSError() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+ // Taken from security/manager/ssl/tests/unit/head_psm.js.
+ MockSecurityInfo.errorCode = -8180;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "broken",
+ "state == 'broken' if securityState contains STATE_IS_INSECURE flag AND " +
+ "errorCode is NSS error."
+ );
+
+ MockSecurityInfo.errorCode = 0;
+}
+
+/**
+ * Test that STATE_IS_INSECURE without NSSError returns "insecure"
+ */
+async function test_insecureSecurityInfoWithoutNSSError() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "insecure",
+ "state == 'insecure' if securityState contains STATE_IS_INSECURE flag BUT " +
+ "errorCode is not NSS error."
+ );
+}
+
+/**
+ * Test that STATE_IS_SECURE returns "secure"
+ */
+async function test_secureSecurityInfo() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_SECURE;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "secure",
+ "state == 'secure' if securityState contains STATE_IS_SECURE flag"
+ );
+}
+
+/**
+ * Test that STATE_IS_BROKEN returns "weak"
+ */
+async function test_brokenSecurityInfo() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_BROKEN;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "weak",
+ "state == 'weak' if securityState contains STATE_IS_BROKEN flag"
+ );
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js
new file mode 100644
index 0000000000..f1aa883b93
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that NetworkHelper.parseSecurityInfo correctly detects static hpkp pins
+
+const wpl = Ci.nsIWebProgressListener;
+
+// This *cannot* be used as an nsITransportSecurityInfo (since that interface is
+// builtinclass) but the methods being tested aren't defined by XPCOM and aren't
+// calling QueryInterface, so this usage is fine.
+const MockSecurityInfo = {
+ securityState: wpl.STATE_IS_SECURE,
+ errorCode: 0,
+ cipherName: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+ // TLS_VERSION_1_2
+ protocolVersion: 3,
+ serverCert: {
+ getBase64DERString() {
+ // This is the same test certificate as in
+ // test_security-info-certificate.js for consistency.
+ return "MIIDrDCCApSgAwIBAgIQCssoukZe5TkIdnRw883GEjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJjAkBgNVBAMTHURpZ2lDZXJ0IEVDQyBTZWN1cmUgU2VydmVyIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ghC6nfYJN6gLGSkE85AnCNyqQIKDjc/ITa4jVMU9tWRlUvzlgKNcR7E2Munn17voOZ/WpIRllNv68DLP679Wz9HJOeaBy6Wvqgvu1cYr3GkvXg6HuhbPGtkESvMNCuMo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAdBgNVHQ4EFgQUo53mH/naOU/AbuiRy5Wl2jHiCp8wHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEMBQADggEBAMeKoENL7HTJxavVHzA1Nm6YVntIrAVjrnuaVyRXzG/63qttnMe2uuzO58pzZNvfBDcKAEmzP58mrZGMIOgfiA4q+2Y3yDDo0sIkp0VILeoBUEoxlBPfjV/aKrtJPGHzecicZpIalir0ezZYoyxBEHQa0+1IttK7igZFcTMQMHp6mCHdJLnsnLWSB62DxsRq+HfmNb4TDydkskO/g+l3VtsIh5RHFPVfKK+jaEyDj2D3loB5hWp2Jp2VDCADjT7ueihlZGak2YPqmXTNbk19HOuNssWvFhtOyPNV6og4ETQdEa8/B6hPatJ0ES8q/HO3X8IVQwVs1n3aAr0im0/T+Xc=";
+ },
+ },
+};
+
+const MockHttpInfo = {
+ hostname: "include-subdomains.pinning.example.com",
+ private: false,
+};
+
+add_task(async function run_test() {
+ Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 1);
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ MockHttpInfo,
+ new Map()
+ );
+ equal(result.hpkp, true, "Static HPKP detected.");
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js
new file mode 100644
index 0000000000..71d7675284
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.getReasonsForWeakness returns correct reasons for
+// weak requests.
+
+const wpl = Ci.nsIWebProgressListener;
+const TEST_CASES = [
+ {
+ description: "weak cipher",
+ input: wpl.STATE_IS_BROKEN | wpl.STATE_USES_WEAK_CRYPTO,
+ expected: ["cipher"],
+ },
+ {
+ description: "only STATE_IS_BROKEN flag",
+ input: wpl.STATE_IS_BROKEN,
+ expected: [],
+ },
+ {
+ description: "only STATE_IS_SECURE flag",
+ input: wpl.STATE_IS_SECURE,
+ expected: [],
+ },
+];
+
+function run_test() {
+ info("Testing NetworkHelper.getReasonsForWeakness.");
+
+ for (const { description, input, expected } of TEST_CASES) {
+ info("Testing " + description);
+
+ deepEqual(
+ NetworkHelper.getReasonsForWeakness(input),
+ expected,
+ "Got the expected reasons for weakness."
+ );
+ }
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_throttle.js b/devtools/shared/network-observer/test/xpcshell/test_throttle.js
new file mode 100644
index 0000000000..5f8ef589fb
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_throttle.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+const { NetworkThrottleManager } = ChromeUtils.importESModule(
+ "resource://devtools/shared/network-observer/NetworkThrottleManager.sys.mjs"
+);
+const nsIScriptableInputStream = Ci.nsIScriptableInputStream;
+
+function TestStreamListener() {
+ this.state = "initial";
+}
+TestStreamListener.prototype = {
+ onStartRequest() {
+ this.setState("start");
+ },
+
+ onStopRequest() {
+ this.setState("stop");
+ },
+
+ onDataAvailable(request, inputStream, offset, count) {
+ const sin = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ nsIScriptableInputStream
+ );
+ sin.init(inputStream);
+ this.data = sin.read(count);
+ this.setState("data");
+ },
+
+ setState(state) {
+ this.state = state;
+ if (this._deferred) {
+ this._deferred.resolve(state);
+ this._deferred = null;
+ }
+ },
+
+ onStateChanged() {
+ if (!this._deferred) {
+ let resolve, reject;
+ const promise = new Promise(function (res, rej) {
+ resolve = res;
+ reject = rej;
+ });
+ this._deferred = { resolve, reject, promise };
+ }
+ return this._deferred.promise;
+ },
+};
+
+function TestChannel() {
+ this.state = "initial";
+ this.testListener = new TestStreamListener();
+ this._throttleQueue = null;
+}
+TestChannel.prototype = {
+ QueryInterface() {
+ return this;
+ },
+
+ get throttleQueue() {
+ return this._throttleQueue;
+ },
+
+ set throttleQueue(q) {
+ this._throttleQueue = q;
+ this.state = "throttled";
+ },
+
+ setNewListener(listener) {
+ this.listener = listener;
+ this.state = "listener";
+ return this.testListener;
+ },
+};
+
+add_task(async function () {
+ const throttler = new NetworkThrottleManager({
+ latencyMean: 1,
+ latencyMax: 1,
+ downloadBPSMean: 500,
+ downloadBPSMax: 500,
+ uploadBPSMean: 500,
+ uploadBPSMax: 500,
+ });
+
+ const uploadChannel = new TestChannel();
+ throttler.manageUpload(uploadChannel);
+ equal(
+ uploadChannel.state,
+ "throttled",
+ "NetworkThrottleManager set throttleQueue"
+ );
+
+ const downloadChannel = new TestChannel();
+ const testListener = downloadChannel.testListener;
+
+ const listener = throttler.manage(downloadChannel);
+ equal(
+ downloadChannel.state,
+ "listener",
+ "NetworkThrottleManager called setNewListener"
+ );
+
+ equal(testListener.state, "initial", "test listener in initial state");
+
+ // This method must be passed through immediately.
+ listener.onStartRequest(null);
+ equal(testListener.state, "start", "test listener started");
+
+ const TEST_INPUT = "hi bob";
+
+ const testStream = Cc["@mozilla.org/storagestream;1"].createInstance(
+ Ci.nsIStorageStream
+ );
+ testStream.init(512, 512);
+ const out = testStream.getOutputStream(0);
+ out.write(TEST_INPUT, TEST_INPUT.length);
+ out.close();
+ const testInputStream = testStream.newInputStream(0);
+
+ const activityDistributor = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ let activitySeen = false;
+ listener.addActivityCallback(
+ () => {
+ activitySeen = true;
+ },
+ null,
+ null,
+ null,
+ activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE,
+ null,
+ TEST_INPUT.length,
+ null
+ );
+
+ // onDataAvailable is required to immediately read the data.
+ listener.onDataAvailable(null, testInputStream, 0, 6);
+ equal(testInputStream.available(), 0, "no more data should be available");
+ equal(
+ testListener.state,
+ "start",
+ "test listener should not have received data"
+ );
+ equal(activitySeen, false, "activity not distributed yet");
+
+ let newState = await testListener.onStateChanged();
+ equal(newState, "data", "test listener received data");
+ equal(testListener.data, TEST_INPUT, "test listener received all the data");
+ equal(activitySeen, true, "activity has been distributed");
+
+ const onChange = testListener.onStateChanged();
+ listener.onStopRequest(null, null);
+ newState = await onChange;
+ equal(newState, "stop", "onStateChanged reported");
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/xpcshell.toml b/devtools/shared/network-observer/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..d10f08ac82
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/xpcshell.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = "devtools"
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = ""
+
+["test_network_helper.js"]
+
+["test_security-info-certificate.js"]
+
+["test_security-info-parser.js"]
+
+["test_security-info-protocol-version.js"]
+
+["test_security-info-state.js"]
+
+["test_security-info-static-hpkp.js"]
+
+["test_security-info-weakness-reasons.js"]
+
+["test_throttle.js"]
diff --git a/devtools/shared/node-properties/UPGRADING.md b/devtools/shared/node-properties/UPGRADING.md
new file mode 100644
index 0000000000..086621f2b4
--- /dev/null
+++ b/devtools/shared/node-properties/UPGRADING.md
@@ -0,0 +1,12 @@
+NODE PROPERTIES UPGRADING
+
+Original library at https://github.com/gagle/node-properties
+The original library is intended for node and not for the browser. Most files are not
+needed here.
+
+To update
+- copy https://github.com/gagle/node-properties/blob/master/lib/parse.js
+- update the initial "module.exports" to "var parse" in parse.js
+- copy https://github.com/gagle/node-properties/blob/master/lib/read.js
+- remove the require statements at the beginning
+- merge the two files, parse.js first, read.js second \ No newline at end of file
diff --git a/devtools/shared/node-properties/moz.build b/devtools/shared/node-properties/moz.build
new file mode 100644
index 0000000000..e58b1fdb69
--- /dev/null
+++ b/devtools/shared/node-properties/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ 'node-properties.js'
+)
diff --git a/devtools/shared/node-properties/node-properties.js b/devtools/shared/node-properties/node-properties.js
new file mode 100644
index 0000000000..05feba857b
--- /dev/null
+++ b/devtools/shared/node-properties/node-properties.js
@@ -0,0 +1,776 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014 Gabriel Llamas
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+"use strict";
+
+var hex = function (c){
+ switch (c){
+ case "0": return 0;
+ case "1": return 1;
+ case "2": return 2;
+ case "3": return 3;
+ case "4": return 4;
+ case "5": return 5;
+ case "6": return 6;
+ case "7": return 7;
+ case "8": return 8;
+ case "9": return 9;
+ case "a": case "A": return 10;
+ case "b": case "B": return 11;
+ case "c": case "C": return 12;
+ case "d": case "D": return 13;
+ case "e": case "E": return 14;
+ case "f": case "F": return 15;
+ }
+};
+
+var parse = function (data, options, handlers, control){
+ var c;
+ var code;
+ var escape;
+ var skipSpace = true;
+ var isCommentLine;
+ var isSectionLine;
+ var newLine = true;
+ var multiLine;
+ var isKey = true;
+ var key = "";
+ var value = "";
+ var section;
+ var unicode;
+ var unicodeRemaining;
+ var escapingUnicode;
+ var keySpace;
+ var sep;
+ var ignoreLine;
+
+ var line = function (){
+ if (key || value || sep){
+ handlers.line (key, value);
+ key = "";
+ value = "";
+ sep = false;
+ }
+ };
+
+ var escapeString = function (key, c, code){
+ if (escapingUnicode && unicodeRemaining){
+ unicode = (unicode << 4) + hex (c);
+ if (--unicodeRemaining) return key;
+ escape = false;
+ escapingUnicode = false;
+ return key + String.fromCharCode (unicode);
+ }
+
+ //code 117: u
+ if (code === 117){
+ unicode = 0;
+ escapingUnicode = true;
+ unicodeRemaining = 4;
+ return key;
+ }
+
+ escape = false;
+
+ //code 116: t
+ //code 114: r
+ //code 110: n
+ //code 102: f
+ if (code === 116) return key + "\t";
+ else if (code === 114) return key + "\r";
+ else if (code === 110) return key + "\n";
+ else if (code === 102) return key + "\f";
+
+ return key + c;
+ };
+
+ var isComment;
+ var isSeparator;
+
+ if (options._strict){
+ isComment = function (c, code, options){
+ return options._comments[c];
+ };
+
+ isSeparator = function (c, code, options){
+ return options._separators[c];
+ };
+ }else{
+ isComment = function (c, code, options){
+ //code 35: #
+ //code 33: !
+ return code === 35 || code === 33 || options._comments[c];
+ };
+
+ isSeparator = function (c, code, options){
+ //code 61: =
+ //code 58: :
+ return code === 61 || code === 58 || options._separators[c];
+ };
+ }
+
+ for (var i=~~control.resume; i<data.length; i++){
+ if (control.abort) return;
+ if (control.pause){
+ //The next index is always the start of a new line, it's a like a fresh
+ //start, there's no need to save the current state
+ control.resume = i;
+ return;
+ }
+
+ c = data[i];
+ code = data.charCodeAt (i);
+
+ //code 13: \r
+ if (code === 13) continue;
+
+ if (isCommentLine){
+ //code 10: \n
+ if (code === 10){
+ isCommentLine = false;
+ newLine = true;
+ skipSpace = true;
+ }
+ continue;
+ }
+
+ //code 93: ]
+ if (isSectionLine && code === 93){
+ handlers.section (section);
+ //Ignore chars after the section in the same line
+ ignoreLine = true;
+ continue;
+ }
+
+ if (skipSpace){
+ //code 32: " " (space)
+ //code 9: \t
+ //code 12: \f
+ if (code === 32 || code === 9 || code === 12){
+ continue;
+ }
+ //code 10: \n
+ if (!multiLine && code === 10){
+ //Empty line or key w/ separator and w/o value
+ isKey = true;
+ keySpace = false;
+ newLine = true;
+ line ();
+ continue;
+ }
+ skipSpace = false;
+ multiLine = false;
+ }
+
+ if (newLine){
+ newLine = false;
+ if (isComment (c, code, options)){
+ isCommentLine = true;
+ continue;
+ }
+ //code 91: [
+ if (options.sections && code === 91){
+ section = "";
+ isSectionLine = true;
+ control.skipSection = false;
+ continue;
+ }
+ }
+
+ //code 10: \n
+ if (code !== 10){
+ if (control.skipSection || ignoreLine) continue;
+
+ if (!isSectionLine){
+ if (!escape && isKey && isSeparator (c, code, options)){
+ //sep is needed to detect empty key and empty value with a
+ //non-whitespace separator
+ sep = true;
+ isKey = false;
+ keySpace = false;
+ //Skip whitespace between separator and value
+ skipSpace = true;
+ continue;
+ }
+ }
+
+ //code 92: "\" (backslash)
+ if (code === 92){
+ if (escape){
+ if (escapingUnicode) continue;
+
+ if (keySpace){
+ //Line with whitespace separator
+ keySpace = false;
+ isKey = false;
+ }
+
+ if (isSectionLine) section += "\\";
+ else if (isKey) key += "\\";
+ else value += "\\";
+ }
+ escape = !escape;
+ }else{
+ if (keySpace){
+ //Line with whitespace separator
+ keySpace = false;
+ isKey = false;
+ }
+
+ if (isSectionLine){
+ if (escape) section = escapeString (section, c, code);
+ else section += c;
+ }else if (isKey){
+ if (escape){
+ key = escapeString (key, c, code);
+ }else{
+ //code 32: " " (space)
+ //code 9: \t
+ //code 12: \f
+ if (code === 32 || code === 9 || code === 12){
+ keySpace = true;
+ //Skip whitespace between key and separator
+ skipSpace = true;
+ continue;
+ }
+ key += c;
+ }
+ }else{
+ if (escape) value = escapeString (value, c, code);
+ else value += c;
+ }
+ }
+ }else{
+ if (escape){
+ if (!escapingUnicode){
+ escape = false;
+ }
+ skipSpace = true;
+ multiLine = true;
+ }else{
+ if (isSectionLine){
+ isSectionLine = false;
+ if (!ignoreLine){
+ //The section doesn't end with ], it's a key
+ control.error = new Error ("The section line \"" + section +
+ "\" must end with \"]\"");
+ return;
+ }
+ ignoreLine = false;
+ }
+ newLine = true;
+ skipSpace = true;
+ isKey = true;
+
+ line ();
+ }
+ }
+ }
+
+ control.parsed = true;
+
+ if (isSectionLine && !ignoreLine){
+ //The section doesn't end with ], it's a key
+ control.error = new Error ("The section line \"" + section + "\" must end" +
+ "with \"]\"");
+ return;
+ }
+ line ();
+};
+
+var INCLUDE_KEY = "include";
+var INDEX_FILE = "index.properties";
+
+var cast = function (value){
+ if (value === null || value === "null") return null;
+ if (value === "undefined") return undefined;
+ if (value === "true") return true;
+ if (value === "false") return false;
+ var v = Number (value);
+ return isNaN (v) ? value : v;
+};
+
+var expand = function (o, str, options, cb){
+ if (!options.variables || !str) return cb (null, str);
+
+ var stack = [];
+ var c;
+ var cp;
+ var key = "";
+ var section = null;
+ var v;
+ var holder;
+ var t;
+ var n;
+
+ for (var i=0; i<str.length; i++){
+ c = str[i];
+
+ if (cp === "$" && c === "{"){
+ key = key.substring (0, key.length - 1);
+ stack.push ({
+ key: key,
+ section: section
+ });
+ key = "";
+ section = null;
+ continue;
+ }else if (stack.length){
+ if (options.sections && c === "|"){
+ section = key;
+ key = "";
+ continue;
+ }else if (c === "}"){
+ holder = section !== null ? searchValue (o, section, true) : o;
+ if (!holder){
+ return cb (new Error ("The section \"" + section + "\" does not " +
+ "exist"));
+ }
+
+ v = options.namespaces ? searchValue (holder, key) : holder[key];
+ if (v === undefined){
+ //Read the external vars
+ v = options.namespaces
+ ? searchValue (options._vars, key)
+ : options._vars[key]
+
+ if (v === undefined){
+ return cb (new Error ("The property \"" + key + "\" does not " +
+ "exist"));
+ }
+ }
+
+ t = stack.pop ();
+ section = t.section;
+ key = t.key + (v === null ? "" : v);
+ continue;
+ }
+ }
+
+ cp = c;
+ key += c;
+ }
+
+ if (stack.length !== 0){
+ return cb (new Error ("Malformed variable: " + str));
+ }
+
+ cb (null, key);
+};
+
+var searchValue = function (o, chain, section){
+ var n = chain.split (".");
+ var str;
+
+ for (var i=0; i<n.length-1; i++){
+ str = n[i];
+ if (o[str] === undefined) return;
+ o = o[str];
+ }
+
+ var v = o[n[n.length - 1]];
+ if (section){
+ if (typeof v !== "object") return;
+ return v;
+ }else{
+ if (typeof v === "object") return;
+ return v;
+ }
+};
+
+var namespaceKey = function (o, key, value){
+ var n = key.split (".");
+ var str;
+
+ for (var i=0; i<n.length-1; i++){
+ str = n[i];
+ if (o[str] === undefined){
+ o[str] = {};
+ }else if (typeof o[str] !== "object"){
+ throw new Error ("Invalid namespace chain in the property name '" +
+ key + "' ('" + str + "' has already a value)");
+ }
+ o = o[str];
+ }
+
+ o[n[n.length - 1]] = value;
+};
+
+var namespaceSection = function (o, section){
+ var n = section.split (".");
+ var str;
+
+ for (var i=0; i<n.length; i++){
+ str = n[i];
+ if (o[str] === undefined){
+ o[str] = {};
+ }else if (typeof o[str] !== "object"){
+ throw new Error ("Invalid namespace chain in the section name '" +
+ section + "' ('" + str + "' has already a value)");
+ }
+ o = o[str];
+ }
+
+ return o;
+};
+
+var merge = function (o1, o2){
+ for (var p in o2){
+ try{
+ if (o1[p].constructor === Object){
+ o1[p] = merge (o1[p], o2[p]);
+ }else{
+ o1[p] = o2[p];
+ }
+ }catch (e){
+ o1[p] = o2[p];
+ }
+ }
+ return o1;
+}
+
+var build = function (data, options, dirname, cb){
+ var o = {};
+
+ if (options.namespaces){
+ var n = {};
+ }
+
+ var control = {
+ abort: false,
+ skipSection: false
+ };
+
+ if (options.include){
+ var remainingIncluded = 0;
+
+ var include = function (value){
+ if (currentSection !== null){
+ return abort (new Error ("Cannot include files from inside a " +
+ "section: " + currentSection));
+ }
+
+ var p = path.resolve (dirname, value);
+ if (options._included[p]) return;
+
+ options._included[p] = true;
+ remainingIncluded++;
+ control.pause = true;
+
+ read (p, options, function (error, included){
+ if (error) return abort (error);
+
+ remainingIncluded--;
+ merge (options.namespaces ? n : o, included);
+ control.pause = false;
+
+ if (!control.parsed){
+ parse (data, options, handlers, control);
+ if (control.error) return abort (control.error);
+ }
+
+ if (!remainingIncluded) cb (null, options.namespaces ? n : o);
+ });
+ };
+ }
+
+ if (!data){
+ if (cb) return cb (null, o);
+ return o;
+ }
+
+ var currentSection = null;
+ var currentSectionStr = null;
+
+ var abort = function (error){
+ control.abort = true;
+ if (cb) return cb (error);
+ throw error;
+ };
+
+ var handlers = {};
+ var reviver = {
+ assert: function (){
+ return this.isProperty ? reviverLine.value : true;
+ }
+ };
+ var reviverLine = {};
+
+ //Line handler
+ //For speed reasons, if "namespaces" is enabled, the old object is still
+ //populated, e.g.: ${a.b} reads the "a.b" property from { "a.b": 1 }, instead
+ //of having a unique object { a: { b: 1 } } which is slower to search for
+ //the "a.b" value
+ //If "a.b" is not found, then the external vars are read. If "namespaces" is
+ //enabled, the var "a.b" is split and it searches the a.b value. If it is not
+ //enabled, then the var "a.b" searches the "a.b" value
+
+ var line;
+ var error;
+
+ if (options.reviver){
+ if (options.sections){
+ line = function (key, value){
+ if (options.include && key === INCLUDE_KEY) return include (value);
+
+ reviverLine.value = value;
+ reviver.isProperty = true;
+ reviver.isSection = false;
+
+ value = options.reviver.call (reviver, key, value, currentSectionStr);
+ if (value !== undefined){
+ if (options.namespaces){
+ try{
+ namespaceKey (currentSection === null ? n : currentSection,
+ key, value);
+ }catch (error){
+ abort (error);
+ }
+ }else{
+ if (currentSection === null) o[key] = value;
+ else currentSection[key] = value;
+ }
+ }
+ };
+ }else{
+ line = function (key, value){
+ if (options.include && key === INCLUDE_KEY) return include (value);
+
+ reviverLine.value = value;
+ reviver.isProperty = true;
+ reviver.isSection = false;
+
+ value = options.reviver.call (reviver, key, value);
+ if (value !== undefined){
+ if (options.namespaces){
+ try{
+ namespaceKey (n, key, value);
+ }catch (error){
+ abort (error);
+ }
+ }else{
+ o[key] = value;
+ }
+ }
+ };
+ }
+ }else{
+ if (options.sections){
+ line = function (key, value){
+ if (options.include && key === INCLUDE_KEY) return include (value);
+
+ if (options.namespaces){
+ try{
+ namespaceKey (currentSection === null ? n : currentSection, key,
+ value);
+ }catch (error){
+ abort (error);
+ }
+ }else{
+ if (currentSection === null) o[key] = value;
+ else currentSection[key] = value;
+ }
+ };
+ }else{
+ line = function (key, value){
+ if (options.include && key === INCLUDE_KEY) return include (value);
+
+ if (options.namespaces){
+ try{
+ namespaceKey (n, key, value);
+ }catch (error){
+ abort (error);
+ }
+ }else{
+ o[key] = value;
+ }
+ };
+ }
+ }
+
+ //Section handler
+ var section;
+ if (options.sections){
+ if (options.reviver){
+ section = function (section){
+ currentSectionStr = section;
+ reviverLine.section = section;
+ reviver.isProperty = false;
+ reviver.isSection = true;
+
+ var add = options.reviver.call (reviver, null, null, section);
+ if (add){
+ if (options.namespaces){
+ try{
+ currentSection = namespaceSection (n, section);
+ }catch (error){
+ abort (error);
+ }
+ }else{
+ currentSection = o[section] = {};
+ }
+ }else{
+ control.skipSection = true;
+ }
+ };
+ }else{
+ section = function (section){
+ currentSectionStr = section;
+ if (options.namespaces){
+ try{
+ currentSection = namespaceSection (n, section);
+ }catch (error){
+ abort (error);
+ }
+ }else{
+ currentSection = o[section] = {};
+ }
+ };
+ }
+ }
+
+ //Variables
+ if (options.variables){
+ handlers.line = function (key, value){
+ expand (options.namespaces ? n : o, key, options, function (error, key){
+ if (error) return abort (error);
+
+ expand (options.namespaces ? n : o, value, options,
+ function (error, value){
+ if (error) return abort (error);
+
+ line (key, cast (value || null));
+ });
+ });
+ };
+
+ if (options.sections){
+ handlers.section = function (s){
+ expand (options.namespaces ? n : o, s, options, function (error, s){
+ if (error) return abort (error);
+
+ section (s);
+ });
+ };
+ }
+ }else{
+ handlers.line = function (key, value){
+ line (key, cast (value || null));
+ };
+
+ if (options.sections){
+ handlers.section = section;
+ }
+ }
+
+ parse (data, options, handlers, control);
+ if (control.error) return abort (control.error);
+
+ if (control.abort || control.pause) return;
+
+ if (cb) return cb (null, options.namespaces ? n : o);
+ return options.namespaces ? n : o;
+};
+
+var read = function (f, options, cb){
+ fs.stat (f, function (error, stats){
+ if (error) return cb (error);
+
+ var dirname;
+
+ if (stats.isDirectory ()){
+ dirname = f;
+ f = path.join (f, INDEX_FILE);
+ }else{
+ dirname = path.dirname (f);
+ }
+
+ fs.readFile (f, { encoding: "utf8" }, function (error, data){
+ if (error) return cb (error);
+ build (data, options, dirname, cb);
+ });
+ });
+};
+
+module.exports = function (data, options, cb){
+ if (typeof options === "function"){
+ cb = options;
+ options = {};
+ }
+
+ options = options || {};
+ var code;
+
+ if (options.include){
+ if (!cb) throw new Error ("A callback must be passed if the 'include' " +
+ "option is enabled");
+ options._included = {};
+ }
+
+ options = options || {};
+ options._strict = options.strict && (options.comments || options.separators);
+ options._vars = options.vars || {};
+
+ var comments = options.comments || [];
+ if (!Array.isArray (comments)) comments = [comments];
+ var c = {};
+ comments.forEach (function (comment){
+ code = comment.charCodeAt (0);
+ if (comment.length > 1 || code < 33 || code > 126){
+ throw new Error ("The comment token must be a single printable ASCII " +
+ "character");
+ }
+ c[comment] = true;
+ });
+ options._comments = c;
+
+ var separators = options.separators || [];
+ if (!Array.isArray (separators)) separators = [separators];
+ var s = {};
+ separators.forEach (function (separator){
+ code = separator.charCodeAt (0);
+ if (separator.length > 1 || code < 33 || code > 126){
+ throw new Error ("The separator token must be a single printable ASCII " +
+ "character");
+ }
+ s[separator] = true;
+ });
+ options._separators = s;
+
+ if (options.path){
+ if (!cb) throw new Error ("A callback must be passed if the 'path' " +
+ "option is enabled");
+ if (options.include){
+ read (data, options, cb);
+ }else{
+ fs.readFile (data, { encoding: "utf8" }, function (error, data){
+ if (error) return cb (error);
+ build (data, options, ".", cb);
+ });
+ }
+ }else{
+ return build (data, options, ".", cb);
+ }
+};
diff --git a/devtools/shared/path.js b/devtools/shared/path.js
new file mode 100644
index 0000000000..759311fdee
--- /dev/null
+++ b/devtools/shared/path.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/*
+ * Join all the arguments together and normalize the resulting URI.
+ * The initial path must be an full URI with a protocol (i.e. http://).
+ */
+exports.joinURI = (initialPath, ...paths) => {
+ let url;
+
+ try {
+ url = new URL(initialPath);
+ } catch (e) {
+ return null;
+ }
+
+ for (const path of paths) {
+ if (path) {
+ url = new URL(path, url);
+ }
+ }
+
+ return url.href;
+};
diff --git a/devtools/shared/performance-new/moz.build b/devtools/shared/performance-new/moz.build
new file mode 100644
index 0000000000..1d67ce1cc7
--- /dev/null
+++ b/devtools/shared/performance-new/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(
+ "recording-utils.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")
diff --git a/devtools/shared/performance-new/recording-utils.js b/devtools/shared/performance-new/recording-utils.js
new file mode 100644
index 0000000000..c221ab6cd5
--- /dev/null
+++ b/devtools/shared/performance-new/recording-utils.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/. */
+// @ts-check
+"use strict";
+
+/**
+ * This file is for the new performance panel that targets profiler.firefox.com,
+ * not the default-enabled DevTools performance panel.
+ */
+
+/**
+ * @typedef {import("../../client/performance-new/@types/perf").GetActiveBrowserID} GetActiveBrowserID
+ */
+
+/**
+ * Gets the ID of active tab from the browser.
+ *
+ * @type {GetActiveBrowserID}
+ */
+function getActiveBrowserID() {
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ const browserId = win?.gBrowser?.selectedBrowser?.browsingContext?.browserId;
+ if (browserId) {
+ return browserId;
+ }
+
+ console.error(
+ "Failed to get the active browserId while starting the profiler."
+ );
+ // `0` mean that we failed to ge the active browserId, and it's
+ // treated as null value in the platform.
+ return 0;
+}
+
+module.exports = {
+ getActiveBrowserID,
+};
diff --git a/devtools/shared/picker-constants.js b/devtools/shared/picker-constants.js
new file mode 100644
index 0000000000..2e5556cd6a
--- /dev/null
+++ b/devtools/shared/picker-constants.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";
+
+/**
+ * Types of content pickers that can be triggered from DevTools.
+ * Used for instance by RDM to keep track of which picker is currently enabled.
+ */
+module.exports = {
+ ELEMENT: "ELEMENT",
+ EYEDROPPER: "EYEDROPPER",
+};
diff --git a/devtools/shared/platform/CacheEntry.sys.mjs b/devtools/shared/platform/CacheEntry.sys.mjs
new file mode 100644
index 0000000000..761f0d2946
--- /dev/null
+++ b/devtools/shared/platform/CacheEntry.sys.mjs
@@ -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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+});
+
+/**
+ * Global cache session object.
+ */
+let gCacheSession = null;
+
+/**
+ * Get (and create if necessary) a cache session / cache storage session.
+ *
+ * @param {nsIRequest} request
+ */
+function getCacheSession(request) {
+ if (!gCacheSession) {
+ try {
+ const cacheService = Services.cache2;
+ if (cacheService) {
+ let loadContext = lazy.NetworkHelper.getRequestLoadContext(request);
+ if (!loadContext) {
+ // Get default load context if we can't fetch.
+ loadContext = Services.loadContextInfo.default;
+ }
+ gCacheSession = cacheService.diskCacheStorage(loadContext);
+ }
+ } catch (e) {
+ gCacheSession = null;
+ }
+ }
+
+ return gCacheSession;
+}
+
+/**
+ * Parses a cache entry returned from the backend to build a response cache
+ * object.
+ *
+ * @param {nsICacheEntry} cacheEntry
+ * The cache entry from the backend.
+ *
+ * @returns {Object}
+ * A responseCache object expected by RDP.
+ */
+function buildResponseCacheObject(cacheEntry) {
+ const cacheObject = {};
+ try {
+ if (cacheEntry.storageDataSize) {
+ cacheObject.storageDataSize = cacheEntry.storageDataSize;
+ }
+ } catch (e) {
+ // We just need to handle this in case it's a js file of 0B.
+ }
+ if (cacheEntry.expirationTime) {
+ cacheObject.expirationTime = cacheEntry.expirationTime;
+ }
+ if (cacheEntry.fetchCount) {
+ cacheObject.fetchCount = cacheEntry.fetchCount;
+ }
+ if (cacheEntry.lastFetched) {
+ cacheObject.lastFetched = cacheEntry.lastFetched;
+ }
+ if (cacheEntry.lastModified) {
+ cacheObject.lastModified = cacheEntry.lastModified;
+ }
+ if (cacheEntry.deviceID) {
+ cacheObject.deviceID = cacheEntry.deviceID;
+ }
+ return cacheObject;
+}
+
+/**
+ * Does the fetch for the cache entry from the session.
+ *
+ * @param {nsIRequest} request
+ * The request object.
+ *
+ * @returns {Promise}
+ * Promise which resolve a response cache object object, or null if none
+ * was available.
+ */
+export function getResponseCacheObject(request) {
+ const cacheSession = getCacheSession(request);
+ if (!cacheSession) {
+ return null;
+ }
+
+ return new Promise(resolve => {
+ cacheSession.asyncOpenURI(
+ request.URI,
+ "",
+ Ci.nsICacheStorage.OPEN_SECRETLY,
+ {
+ onCacheEntryCheck: entry => {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+ onCacheEntryAvailable: (cacheEntry, isnew, status) => {
+ if (cacheEntry) {
+ const cacheObject = buildResponseCacheObject(cacheEntry);
+ resolve(cacheObject);
+ } else {
+ resolve(null);
+ }
+ },
+ }
+ );
+ });
+}
diff --git a/devtools/shared/platform/clipboard.js b/devtools/shared/platform/clipboard.js
new file mode 100644
index 0000000000..46ea6c5fe6
--- /dev/null
+++ b/devtools/shared/platform/clipboard.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/. */
+
+// Helpers for clipboard handling.
+
+"use strict";
+
+const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+);
+
+function copyString(string) {
+ clipboardHelper.copyString(string);
+}
+
+/**
+ * Retrieve the current clipboard data matching the flavor "text/plain".
+ *
+ * @return {String} Clipboard text content, null if no text clipboard data is available.
+ */
+function getText() {
+ const flavor = "text/plain";
+
+ const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+
+ if (!xferable) {
+ throw new Error(
+ "Couldn't get the clipboard data due to an internal error " +
+ "(couldn't create a Transferable object)."
+ );
+ }
+
+ xferable.init(null);
+ xferable.addDataFlavor(flavor);
+
+ // Get the data into our transferable.
+ Services.clipboard.getData(xferable, Services.clipboard.kGlobalClipboard);
+
+ const data = {};
+ try {
+ xferable.getTransferData(flavor, data);
+ } catch (e) {
+ // Clipboard doesn't contain data in flavor, return null.
+ return null;
+ }
+
+ // There's no data available, return.
+ if (!data.value) {
+ return null;
+ }
+
+ return data.value.QueryInterface(Ci.nsISupportsString).data;
+}
+
+exports.copyString = copyString;
+exports.getText = getText;
diff --git a/devtools/shared/platform/moz.build b/devtools/shared/platform/moz.build
new file mode 100644
index 0000000000..edbf580c1c
--- /dev/null
+++ b/devtools/shared/platform/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(
+ "CacheEntry.sys.mjs",
+ "clipboard.js",
+ "stack.js",
+)
diff --git a/devtools/shared/platform/stack.js b/devtools/shared/platform/stack.js
new file mode 100644
index 0000000000..65ff644091
--- /dev/null
+++ b/devtools/shared/platform/stack.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/. */
+
+// A few wrappers for stack-manipulation. This version of the module
+// is used in chrome code.
+
+"use strict";
+
+/**
+ * Return the Nth path from the stack excluding substr.
+ *
+ * @param {Number}
+ * n the Nth path from the stack to describe.
+ * @param {String} substr
+ * A segment of the path that should be excluded.
+ */
+function getNthPathExcluding(n, substr) {
+ let stack = Components.stack.formattedStack.split("\n");
+
+ // Remove this method from the stack
+ stack = stack.splice(1);
+
+ stack = stack.map(line => {
+ if (line.includes(" -> ")) {
+ return line.split(" -> ")[1];
+ }
+ return line;
+ });
+
+ if (substr) {
+ stack = stack.filter(line => {
+ return line && !line.includes(substr);
+ });
+ }
+
+ if (!stack[n]) {
+ n = 0;
+ }
+ return stack[n] || "";
+}
+
+/**
+ * Return a stack object that can be serialized and, when
+ * deserialized, passed to callFunctionWithAsyncStack.
+ */
+function getStack() {
+ return Components.stack.caller;
+}
+
+/**
+ * Like Cu.callFunctionWithAsyncStack but handles the isWorker case
+ * -- |Cu| isn't defined in workers.
+ */
+function callFunctionWithAsyncStack(callee, stack, id) {
+ if (isWorker) {
+ return callee();
+ }
+ return Cu.callFunctionWithAsyncStack(callee, stack, id);
+}
+
+exports.callFunctionWithAsyncStack = callFunctionWithAsyncStack;
+exports.getNthPathExcluding = getNthPathExcluding;
+exports.getStack = getStack;
diff --git a/devtools/shared/plural-form.js b/devtools/shared/plural-form.js
new file mode 100644
index 0000000000..a87a11fe5a
--- /dev/null
+++ b/devtools/shared/plural-form.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The code below is mostly is a slight modification of the now removed
+ * intl/locale/PluralForm.jsm that removes dependencies on chrome privileged
+ * APIs. To make maintenance easier, this file is kept as close as possible to
+ * the original in terms of implementation. The modified methods here are
+ * - makeGetter (remove code adding the caller name to the log)
+ * - get ruleNum() (rely on LocalizationHelper instead of String.services)
+ * - log() (rely on console.log)
+ *
+ * Disable eslint warnings to preserve original code style.
+ */
+
+/* eslint-disable */
+
+/**
+ * This module provides the PluralForm object which contains a method to figure
+ * out which plural form of a word to use for a given number based on the
+ * current localization. There is also a makeGetter method that creates a get
+ * function for the desired plural rule. This is useful for extensions that
+ * specify their own plural rule instead of relying on the browser default.
+ * (I.e., the extension hasn't been localized to the browser's locale.)
+ *
+ * See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+ *
+ * List of methods:
+ *
+ * string pluralForm
+ * get(int aNum, string aWords)
+ *
+ * int numForms
+ * numForms()
+ *
+ * [string pluralForm get(int aNum, string aWords), int numForms numForms()]
+ * makeGetter(int aRuleNum)
+ * Note: Basically, makeGetter returns 2 functions that do "get" and "numForm"
+ */
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper("toolkit/locales/intl.properties");
+
+// These are the available plural functions that give the appropriate index
+// based on the plural rule number specified. The first element is the number
+// of plural forms and the second is the function to figure out the index.
+const gFunctions = [
+ // 0: Chinese
+ [1, (n) => 0],
+ // 1: English
+ [2, (n) => n!=1?1:0],
+ // 2: French
+ [2, (n) => n>1?1:0],
+ // 3: Latvian
+ [3, (n) => n%10==1&&n%100!=11?1:n%10==0?0:2],
+ // 4: Scottish Gaelic
+ [4, (n) => n==1||n==11?0:n==2||n==12?1:n>0&&n<20?2:3],
+ // 5: Romanian
+ [3, (n) => n==1?0:n==0||n%100>0&&n%100<20?1:2],
+ // 6: Lithuanian
+ [3, (n) => n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?2:1],
+ // 7: Russian
+ [3, (n) => n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2],
+ // 8: Slovak
+ [3, (n) => n==1?0:n>=2&&n<=4?1:2],
+ // 9: Polish
+ [3, (n) => n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2],
+ // 10: Slovenian
+ [4, (n) => n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3],
+ // 11: Irish Gaeilge
+ [5, (n) => n==1?0:n==2?1:n>=3&&n<=6?2:n>=7&&n<=10?3:4],
+ // 12: Arabic
+ [6, (n) => n==0?5:n==1?0:n==2?1:n%100>=3&&n%100<=10?2:n%100>=11&&n%100<=99?3:4],
+ // 13: Maltese
+ [4, (n) => n==1?0:n==0||n%100>0&&n%100<=10?1:n%100>10&&n%100<20?2:3],
+ // 14: Unused
+ [3, (n) => n%10==1?0:n%10==2?1:2],
+ // 15: Icelandic, Macedonian
+ [2, (n) => n%10==1&&n%100!=11?0:1],
+ // 16: Breton
+ [5, (n) => n%10==1&&n%100!=11&&n%100!=71&&n%100!=91?0:n%10==2&&n%100!=12&&n%100!=72&&n%100!=92?1:(n%10==3||n%10==4||n%10==9)&&n%100!=13&&n%100!=14&&n%100!=19&&n%100!=73&&n%100!=74&&n%100!=79&&n%100!=93&&n%100!=94&&n%100!=99?2:n%1000000==0&&n!=0?3:4],
+ // 17: Shuar
+ [2, (n) => n!=0?1:0],
+ // 18: Welsh
+ [6, (n) => n==0?0:n==1?1:n==2?2:n==3?3:n==6?4:5],
+ // 19: Bosnian, Croatian, Serbian
+ [3, (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2],
+];
+
+const PluralForm = {
+ /**
+ * Get the correct plural form of a word based on the number
+ *
+ * @param aNum
+ * The number to decide which plural form to use
+ * @param aWords
+ * A semi-colon (;) separated string of words to pick the plural form
+ * @return The appropriate plural form of the word
+ */
+ get get()
+ {
+ // This method will lazily load to avoid perf when it is first needed and
+ // creates getPluralForm function. The function it creates is based on the
+ // value of pluralRule specified in the intl stringbundle.
+ // See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+
+ // Delete the getters to be overwritten
+ delete this.numForms;
+ delete this.get;
+
+ // Make the plural form get function and set it as the default get
+ [this.get, this.numForms] = this.makeGetter(this.ruleNum);
+ return this.get;
+ },
+
+ /**
+ * Create a pair of plural form functions for the given plural rule number.
+ *
+ * @param aRuleNum
+ * The plural rule number to create functions
+ * @return A pair: [function that gets the right plural form,
+ * function that returns the number of plural forms]
+ */
+ makeGetter: function(aRuleNum)
+ {
+ // Default to "all plural" if the value is out of bounds or invalid
+ if (aRuleNum < 0 || aRuleNum >= gFunctions.length || isNaN(aRuleNum)) {
+ log(["Invalid rule number: ", aRuleNum, " -- defaulting to 0"]);
+ aRuleNum = 0;
+ }
+
+ // Get the desired pluralRule function
+ let [numForms, pluralFunc] = gFunctions[aRuleNum];
+
+ // Return functions that give 1) the number of forms and 2) gets the right
+ // plural form
+ return [function(aNum, aWords) {
+ // Figure out which index to use for the semi-colon separated words
+ let index = pluralFunc(aNum ? Number(aNum) : 0);
+ let words = aWords ? aWords.split(/;/) : [""];
+
+ // Explicitly check bounds to avoid strict warnings
+ let ret = index < words.length ? words[index] : undefined;
+
+ // Check for array out of bounds or empty strings
+ if ((ret == undefined) || (ret == "")) {
+ // Display a message in the error console
+ log(["Index #", index, " of '", aWords, "' for value ", aNum,
+ " is invalid -- plural rule #", aRuleNum, ";"]);
+
+ // Default to the first entry (which might be empty, but not undefined)
+ ret = words[0];
+ }
+
+ return ret;
+ }, () => numForms];
+ },
+
+ /**
+ * Get the number of forms for the current plural rule
+ *
+ * @return The number of forms
+ */
+ get numForms()
+ {
+ // We lazily load numForms, so trigger the init logic with get()
+ this.get();
+ return this.numForms;
+ },
+
+ /**
+ * Get the plural rule number from the intl stringbundle
+ *
+ * @return The plural rule number
+ */
+ get ruleNum()
+ {
+ try {
+ return parseInt(L10N.getStr("pluralRule"), 10);
+ } catch (e) {
+ // Fallback to English if the pluralRule property is not available.
+ return 1;
+ }
+ }
+};
+
+/**
+ * Private helper function to log errors to the error console and command line
+ *
+ * @param aMsg
+ * Error message to log or an array of strings to concat
+ */
+function log(aMsg)
+{
+ let msg = "plural-form.js: " + (aMsg.join ? aMsg.join("") : aMsg);
+ console.log(msg + "\n");
+}
+
+exports.PluralForm = PluralForm;
+exports.get = PluralForm.get;
+
+/* eslint-enable */
diff --git a/devtools/shared/protocol.js b/devtools/shared/protocol.js
new file mode 100644
index 0000000000..5ed63fbc23
--- /dev/null
+++ b/devtools/shared/protocol.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";
+
+var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+var {
+ types,
+ registerFront,
+ getFront,
+ createRootFront,
+} = require("resource://devtools/shared/protocol/types.js");
+var { Front } = require("resource://devtools/shared/protocol/Front.js");
+var {
+ FrontClassWithSpec,
+} = require("resource://devtools/shared/protocol/Front/FrontClassWithSpec.js");
+var { Arg, Option } = require("resource://devtools/shared/protocol/Request.js");
+const { RetVal } = require("resource://devtools/shared/protocol/Response.js");
+const {
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol/Actor/generateActorSpec.js");
+
+exports.Front = Front;
+exports.Pool = Pool;
+exports.Actor = Actor;
+exports.types = types;
+exports.generateActorSpec = generateActorSpec;
+exports.FrontClassWithSpec = FrontClassWithSpec;
+exports.Arg = Arg;
+exports.Option = Option;
+exports.RetVal = RetVal;
+exports.registerFront = registerFront;
+exports.getFront = getFront;
+exports.createRootFront = createRootFront;
diff --git a/devtools/shared/protocol/Actor.js b/devtools/shared/protocol/Actor.js
new file mode 100644
index 0000000000..ea37f6fea2
--- /dev/null
+++ b/devtools/shared/protocol/Actor.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/. */
+
+"use strict";
+
+var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+
+/**
+ * Keep track of which actorSpecs have been created. If a replica of a spec
+ * is created, it can be caught, and specs which inherit from other specs will
+ * not overwrite eachother.
+ */
+var actorSpecs = new WeakMap();
+
+exports.actorSpecs = actorSpecs;
+
+/**
+ * An actor in the actor tree.
+ *
+ * @param optional conn
+ * Either a DevToolsServerConnection or a DevToolsClient. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @constructor
+ */
+
+class Actor extends Pool {
+ constructor(conn, spec) {
+ super(conn);
+
+ this.typeName = spec.typeName;
+
+ // Will contain the actor's ID
+ this.actorID = null;
+
+ // Ensure computing requestTypes only one time per class
+ const proto = Object.getPrototypeOf(this);
+ if (!proto.requestTypes) {
+ proto.requestTypes = generateRequestTypes(spec);
+ }
+
+ // Forward events to the connection.
+ if (spec.events) {
+ for (const [name, request] of spec.events.entries()) {
+ this.on(name, (...args) => {
+ this._sendEvent(name, request, ...args);
+ });
+ }
+ }
+ }
+
+ toString() {
+ return "[Actor " + this.typeName + "/" + this.actorID + "]";
+ }
+
+ _sendEvent(name, request, ...args) {
+ if (this.isDestroyed()) {
+ console.error(
+ `Tried to send a '${name}' event on an already destroyed actor` +
+ ` '${this.typeName}'`
+ );
+ return;
+ }
+ let packet;
+ try {
+ packet = request.write(args, this);
+ } catch (ex) {
+ console.error("Error sending event: " + name);
+ throw ex;
+ }
+ packet.from = packet.from || this.actorID;
+ this.conn.send(packet);
+
+ // This can really be a hot path, even computing the marker label can
+ // have some performance impact.
+ // Guard against missing `Services.profiler` because Services is mocked to
+ // an empty object in the worker loader.
+ if (Services.profiler?.IsActive()) {
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Actor",
+ null,
+ `${this.typeName}.${name}`
+ );
+ }
+ }
+
+ destroy() {
+ super.destroy();
+ this.actorID = null;
+ this._isDestroyed = true;
+ }
+
+ /**
+ * Override this method in subclasses to serialize the actor.
+ * @param [optional] string hint
+ * Optional string to customize the form.
+ * @returns A jsonable object.
+ */
+ form(hint) {
+ return { actor: this.actorID };
+ }
+
+ writeError(error, typeName, method) {
+ console.error(
+ `Error while calling actor '${typeName}'s method '${method}'`,
+ error.message || error
+ );
+ // Also log the error object as-is in order to log the server side stack
+ // nicely in the console, while the previous log will log the client side stack only.
+ if (error.stack) {
+ console.error(error);
+ }
+
+ // Do not try to send the error if the actor is destroyed
+ // as the connection is probably also destroyed and may throw.
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this.conn.send({
+ from: this.actorID,
+ // error.error -> errors created using the throwError() helper
+ // error.name -> errors created using `new Error` or Components.exception
+ // typeof(error)=="string" -> a method thrown like this `throw "a string"`
+ error:
+ error.error ||
+ error.name ||
+ (typeof error == "string" ? error : "unknownError"),
+ message: error.message,
+ // error.fileName -> regular Error instances
+ // error.filename -> errors created using Components.exception
+ fileName: error.fileName || error.filename,
+ lineNumber: error.lineNumber,
+ columnNumber: error.columnNumber,
+ });
+ }
+
+ _queueResponse(create) {
+ const pending = this._pendingResponse || Promise.resolve(null);
+ const response = create(pending);
+ this._pendingResponse = response;
+ }
+
+ /**
+ * Throw an error with the passed message and attach an `error` property to the Error
+ * object so it can be consumed by the writeError function.
+ * @param {String} error: A string (usually a single word serving as an id) that will
+ * be assign to error.error.
+ * @param {String} message: The string that will be passed to the Error constructor.
+ * @throws This always throw.
+ */
+ throwError(error, message) {
+ const err = new Error(message);
+ err.error = error;
+ throw err;
+ }
+}
+
+exports.Actor = Actor;
+
+/**
+ * Generate the "requestTypes" object used by DevToolsServerConnection to implement RDP.
+ * When a RDP packet is received for calling an actor method, this lookup for
+ * the method name in this object and call the function holded on this attribute.
+ *
+ * @params {Object} actorSpec
+ * The procotol-js actor specific coming from devtools/shared/specs/*.js files
+ * This describes the types for methods and events implemented by all actors.
+ * @return {Object} requestTypes
+ * An object where attributes are actor method names
+ * and values are function implementing these methods.
+ * These methods receive a RDP Packet (JSON-serializable object) and a DevToolsServerConnection.
+ * We expect them to return a promise that reserves with the response object
+ * to send back to the client (JSON-serializable object).
+ */
+var generateRequestTypes = function (actorSpec) {
+ // Generate request handlers for each method definition
+ const requestTypes = Object.create(null);
+ actorSpec.methods.forEach(spec => {
+ const handler = function (packet, conn) {
+ try {
+ const startTime = isWorker ? null : Cu.now();
+ let args;
+ try {
+ args = spec.request.read(packet, this);
+ } catch (ex) {
+ console.error("Error reading request: " + packet.type);
+ throw ex;
+ }
+
+ if (!this[spec.name]) {
+ throw new Error(
+ `Spec for '${actorSpec.typeName}' specifies a '${spec.name}'` +
+ ` method that isn't implemented by the actor`
+ );
+ }
+ const ret = this[spec.name].apply(this, args);
+
+ const sendReturn = retToSend => {
+ if (spec.oneway) {
+ // No need to send a response.
+ return;
+ }
+ if (this.isDestroyed()) {
+ console.error(
+ `Tried to send a '${spec.name}' method reply on an already destroyed actor` +
+ ` '${this.typeName}'`
+ );
+ return;
+ }
+
+ let response;
+ try {
+ response = spec.response.write(retToSend, this);
+ } catch (ex) {
+ console.error("Error writing response to: " + spec.name);
+ throw ex;
+ }
+ response.from = this.actorID;
+ // If spec.release has been specified, destroy the object.
+ if (spec.release) {
+ try {
+ this.destroy();
+ } catch (e) {
+ this.writeError(e, actorSpec.typeName, spec.name);
+ return;
+ }
+ }
+
+ conn.send(response);
+
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Actor",
+ startTime,
+ `${actorSpec.typeName}:${spec.name}()`
+ );
+ };
+
+ this._queueResponse(p => {
+ return p
+ .then(() => ret)
+ .then(sendReturn)
+ .catch(e => this.writeError(e, actorSpec.typeName, spec.name));
+ });
+ } catch (e) {
+ this._queueResponse(p => {
+ return p.then(() =>
+ this.writeError(e, actorSpec.typeName, spec.name)
+ );
+ });
+ }
+ };
+
+ requestTypes[spec.request.type] = handler;
+ });
+
+ return requestTypes;
+};
+exports.generateRequestTypes = generateRequestTypes;
diff --git a/devtools/shared/protocol/Actor/generateActorSpec.js b/devtools/shared/protocol/Actor/generateActorSpec.js
new file mode 100644
index 0000000000..9b3b166c8b
--- /dev/null
+++ b/devtools/shared/protocol/Actor/generateActorSpec.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";
+
+var { Request } = require("resource://devtools/shared/protocol/Request.js");
+const { Response } = require("resource://devtools/shared/protocol/Response.js");
+var {
+ types,
+ registeredTypes,
+} = require("resource://devtools/shared/protocol/types.js");
+
+/**
+ * Generates an actor specification from an actor description.
+ */
+var generateActorSpec = function (actorDesc) {
+ const actorSpec = {
+ typeName: actorDesc.typeName,
+ methods: [],
+ };
+
+ // Find additional method specifications
+ if (actorDesc.methods) {
+ for (const name in actorDesc.methods) {
+ const methodSpec = actorDesc.methods[name];
+ const spec = {};
+
+ spec.name = methodSpec.name || name;
+ spec.request = new Request(
+ Object.assign({ type: spec.name }, methodSpec.request || undefined)
+ );
+ spec.response = new Response(methodSpec.response || undefined);
+ spec.release = methodSpec.release;
+ spec.oneway = methodSpec.oneway;
+
+ actorSpec.methods.push(spec);
+ }
+ }
+
+ // Find event specifications
+ if (actorDesc.events) {
+ actorSpec.events = new Map();
+ for (const name in actorDesc.events) {
+ const eventRequest = actorDesc.events[name];
+ Object.freeze(eventRequest);
+ actorSpec.events.set(
+ name,
+ new Request(Object.assign({ type: name }, eventRequest))
+ );
+ }
+ }
+
+ if (!registeredTypes.has(actorSpec.typeName)) {
+ types.addActorType(actorSpec.typeName);
+ }
+ registeredTypes.get(actorSpec.typeName).actorSpec = actorSpec;
+
+ return actorSpec;
+};
+
+exports.generateActorSpec = generateActorSpec;
diff --git a/devtools/shared/protocol/Actor/moz.build b/devtools/shared/protocol/Actor/moz.build
new file mode 100644
index 0000000000..3e3c46d49b
--- /dev/null
+++ b/devtools/shared/protocol/Actor/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ "generateActorSpec.js",
+)
diff --git a/devtools/shared/protocol/Front.js b/devtools/shared/protocol/Front.js
new file mode 100644
index 0000000000..1298f3a075
--- /dev/null
+++ b/devtools/shared/protocol/Front.js
@@ -0,0 +1,410 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+var { settleAll } = require("resource://devtools/shared/DevToolsUtils.js");
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+var {
+ getStack,
+ callFunctionWithAsyncStack,
+} = require("resource://devtools/shared/platform/stack.js");
+
+/**
+ * Base class for client-side actor fronts.
+ *
+ * @param [DevToolsClient|null] conn
+ * The conn must either be DevToolsClient or null. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @param [Target|null] target
+ * If we are instantiating a target-scoped front, this is a reference to the front's
+ * Target instance, otherwise this is null.
+ * @param [Front|null] parentFront
+ * The parent front. This is only available if the Front being initialized is a child
+ * of a parent front.
+ * @constructor
+ */
+class Front extends Pool {
+ constructor(conn = null, targetFront = null, parentFront = null) {
+ super(conn);
+ if (!conn) {
+ throw new Error("Front without conn");
+ }
+ this.actorID = null;
+ // The targetFront attribute represents the debuggable context. Only target-scoped
+ // fronts and their children fronts will have the targetFront attribute set.
+ this.targetFront = targetFront;
+ // The parentFront attribute points to its parent front. Only children of
+ // target-scoped fronts will have the parentFront attribute set.
+ this.parentFront = parentFront;
+ this._requests = [];
+
+ // Front listener functions registered via `watchFronts`
+ this._frontCreationListeners = null;
+ this._frontDestructionListeners = null;
+
+ // List of optional listener for each event, that is processed immediatly on packet
+ // receival, before emitting event via EventEmitter on the Front.
+ // These listeners are register via Front.before function.
+ // Map(Event Name[string] => Event Listener[function])
+ this._beforeListeners = new Map();
+
+ // This flag allows to check if the `initialize` method has resolved.
+ // Used to avoid notifying about initialized fronts in `watchFronts`.
+ this._initializeResolved = false;
+ }
+
+ /**
+ * Return the parent front.
+ */
+ getParent() {
+ return this.parentFront && this.parentFront.actorID
+ ? this.parentFront
+ : null;
+ }
+
+ destroy() {
+ // Prevent destroying twice if a `forwardCancelling` event has already been received
+ // and already called `baseFrontClassDestroy`
+ this.baseFrontClassDestroy();
+
+ // Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be
+ // able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests.
+ this.clearEvents();
+ }
+
+ // This method is also called from `DevToolsClient`, when a connector is destroyed
+ // and we should:
+ // - reject all pending request made to the remote process/target/thread.
+ // - avoid trying to do new request against this remote context.
+ // - unmanage this front, so that DevToolsClient.getFront no longer returns it.
+ //
+ // When a connector is destroyed a `forwardCancelling` RDP event is sent by the server.
+ // This is done in a distinct method from `destroy` in order to do all that immediately,
+ // even if `Front.destroy` is overloaded by an async method.
+ baseFrontClassDestroy() {
+ // Reject all outstanding requests, they won't make sense after
+ // the front is destroyed.
+ while (this._requests.length) {
+ const { deferred, to, type, stack } = this._requests.shift();
+ // Note: many tests are ignoring `Connection closed` promise rejections,
+ // via PromiseTestUtils.allowMatchingRejectionsGlobally.
+ // Do not update the message without updating the tests.
+ const msg =
+ "Connection closed, pending request to " +
+ to +
+ ", type " +
+ type +
+ " failed" +
+ "\n\nRequest stack:\n" +
+ stack.formattedStack;
+ deferred.reject(new Error(msg));
+ }
+
+ if (this.actorID) {
+ super.destroy();
+ this.actorID = null;
+ }
+ this._isDestroyed = true;
+
+ this.targetFront = null;
+ this.parentFront = null;
+ this._frontCreationListeners = null;
+ this._frontDestructionListeners = null;
+ this._beforeListeners = null;
+ }
+
+ async manage(front, form, ctx) {
+ if (!front.actorID) {
+ throw new Error(
+ "Can't manage front without an actor ID.\n" +
+ "Ensure server supports " +
+ front.typeName +
+ "."
+ );
+ }
+
+ if (front.parentFront && front.parentFront !== this) {
+ throw new Error(
+ `${this.actorID} (${this.typeName}) can't manage ${front.actorID}
+ (${front.typeName}) since it has a different parentFront ${
+ front.parentFront
+ ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
+ : "<no parentFront>"
+ }`
+ );
+ }
+
+ super.manage(front);
+
+ if (typeof front.initialize == "function") {
+ await front.initialize();
+ }
+ front._initializeResolved = true;
+
+ // Ensure calling form() *before* notifying about this front being just created.
+ // We exprect the front to be fully initialized, especially via its form attributes.
+ // But do that *after* calling manage() so that the front is already registered
+ // in Pools and can be fetched by its ID, in case a child actor, created in form()
+ // tries to get a reference to its parent via the actor ID.
+ if (form) {
+ front.form(form, ctx);
+ }
+
+ // Call listeners registered via `watchFronts` method
+ // (ignore if this front has been destroyed)
+ if (this._frontCreationListeners) {
+ this._frontCreationListeners.emit(front.typeName, front);
+ }
+ }
+
+ async unmanage(front) {
+ super.unmanage(front);
+
+ // Call listeners registered via `watchFronts` method
+ if (this._frontDestructionListeners) {
+ this._frontDestructionListeners.emit(front.typeName, front);
+ }
+ }
+
+ /*
+ * Listen for the creation and/or destruction of fronts matching one of the provided types.
+ *
+ * @param {String} typeName
+ * Actor type to watch.
+ * @param {Function} onAvailable (optional)
+ * Callback fired when a front has been just created or was already available.
+ * The function is called with one arguments, the front.
+ * @param {Function} onDestroy (optional)
+ * Callback fired in case of front destruction.
+ * The function is called with the same argument than onAvailable.
+ */
+ watchFronts(typeName, onAvailable, onDestroy) {
+ if (this.isDestroyed()) {
+ // The front was already destroyed, bail out.
+ console.error(
+ `Tried to call watchFronts for the '${typeName}' type on an ` +
+ `already destroyed front '${this.typeName}'.`
+ );
+ return;
+ }
+
+ if (onAvailable) {
+ // First fire the callback on fronts with the correct type and which have
+ // been initialized. If initialize() is still in progress, the front will
+ // be emitted via _frontCreationListeners shortly after.
+ for (const front of this.poolChildren()) {
+ if (front.typeName == typeName && front._initializeResolved) {
+ onAvailable(front);
+ }
+ }
+
+ if (!this._frontCreationListeners) {
+ this._frontCreationListeners = new EventEmitter();
+ }
+ // Then register the callback for fronts instantiated in the future
+ this._frontCreationListeners.on(typeName, onAvailable);
+ }
+
+ if (onDestroy) {
+ if (!this._frontDestructionListeners) {
+ this._frontDestructionListeners = new EventEmitter();
+ }
+ this._frontDestructionListeners.on(typeName, onDestroy);
+ }
+ }
+
+ /**
+ * Stop listening for the creation and/or destruction of a given type of fronts.
+ * See `watchFronts()` for documentation of the arguments.
+ */
+ unwatchFronts(typeName, onAvailable, onDestroy) {
+ if (this.isDestroyed()) {
+ // The front was already destroyed, bail out.
+ console.error(
+ `Tried to call unwatchFronts for the '${typeName}' type on an ` +
+ `already destroyed front '${this.typeName}'.`
+ );
+ return;
+ }
+
+ if (onAvailable && this._frontCreationListeners) {
+ this._frontCreationListeners.off(typeName, onAvailable);
+ }
+ if (onDestroy && this._frontDestructionListeners) {
+ this._frontDestructionListeners.off(typeName, onDestroy);
+ }
+ }
+
+ /**
+ * Register an event listener that will be called immediately on packer receival.
+ * The given callback is going to be called before emitting the event via EventEmitter
+ * API on the Front. Event emitting will be delayed if the callback is async.
+ * Only one such listener can be registered per type of event.
+ *
+ * @param String type
+ * Event emitted by the actor to intercept.
+ * @param Function callback
+ * Function that will process the event.
+ */
+ before(type, callback) {
+ if (this._beforeListeners.has(type)) {
+ throw new Error(
+ `Can't register multiple before listeners for "${type}".`
+ );
+ }
+ this._beforeListeners.set(type, callback);
+ }
+
+ toString() {
+ return "[Front for " + this.typeName + "/" + this.actorID + "]";
+ }
+
+ /**
+ * Update the actor from its representation.
+ * Subclasses should override this.
+ */
+ form(form) {}
+
+ /**
+ * Send a packet on the connection.
+ */
+ send(packet) {
+ if (packet.to) {
+ this.conn._transport.send(packet);
+ } else {
+ packet.to = this.actorID;
+ // The connection might be closed during the promise resolution
+ if (this.conn && this.conn._transport) {
+ this.conn._transport.send(packet);
+ }
+ }
+ }
+
+ /**
+ * Send a two-way request on the connection.
+ */
+ request(packet) {
+ const deferred = Promise.withResolvers();
+ // Save packet basics for debugging
+ const { to, type } = packet;
+ this._requests.push({
+ deferred,
+ to: to || this.actorID,
+ type,
+ stack: getStack(),
+ });
+ this.send(packet);
+ return deferred.promise;
+ }
+
+ /**
+ * Handler for incoming packets from the client's actor.
+ */
+ onPacket(packet) {
+ if (this.isDestroyed()) {
+ // If the Front was already destroyed, all the requests have been purged
+ // and rejected with detailed error messages in baseFrontClassDestroy.
+ return;
+ }
+
+ // Pick off event packets
+ const type = packet.type || undefined;
+ if (this._clientSpec.events && this._clientSpec.events.has(type)) {
+ const event = this._clientSpec.events.get(packet.type);
+ let args;
+ try {
+ args = event.request.read(packet, this);
+ } catch (ex) {
+ console.error("Error reading event: " + packet.type);
+ console.exception(ex);
+ throw ex;
+ }
+ // Check for "pre event" callback to be processed before emitting events on fronts
+ // Use event.name instead of packet.type to use specific event name instead of RDP
+ // packet's type.
+ const beforeEvent = this._beforeListeners.get(event.name);
+ if (beforeEvent) {
+ const result = beforeEvent.apply(this, args);
+ // Check to see if the beforeEvent returned a promise -- if so,
+ // wait for their resolution before emitting. Otherwise, emit synchronously.
+ if (result && typeof result.then == "function") {
+ result.then(() => {
+ super.emit(event.name, ...args);
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Front",
+ null,
+ `${this.typeName}.${event.name}`
+ );
+ });
+ return;
+ }
+ }
+
+ super.emit(event.name, ...args);
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Front",
+ null,
+ `${this.typeName}.${event.name}`
+ );
+ return;
+ }
+
+ // Remaining packets must be responses.
+ if (this._requests.length === 0) {
+ const msg =
+ "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
+ const err = Error(msg);
+ console.error(err);
+ throw err;
+ }
+
+ const { deferred, stack } = this._requests.shift();
+ callFunctionWithAsyncStack(
+ () => {
+ if (packet.error) {
+ let message;
+ if (packet.error && packet.message) {
+ message =
+ "Protocol error (" + packet.error + "): " + packet.message;
+ } else {
+ message = packet.error;
+ }
+ message += " from: " + this.actorID;
+ if (packet.fileName) {
+ const { fileName, columnNumber, lineNumber } = packet;
+ message += ` (${fileName}:${lineNumber}:${columnNumber})`;
+ }
+ const packetError = new Error(message);
+ deferred.reject(packetError);
+ } else {
+ deferred.resolve(packet);
+ }
+ },
+ stack,
+ "DevTools RDP"
+ );
+ }
+
+ hasRequests() {
+ return !!this._requests.length;
+ }
+
+ /**
+ * Wait for all current requests from this front to settle. This is especially useful
+ * for tests and other utility environments that may not have events or mechanisms to
+ * await the completion of requests without this utility.
+ *
+ * @return Promise
+ * Resolved when all requests have settled.
+ */
+ waitForRequestsToSettle() {
+ return settleAll(this._requests.map(({ deferred }) => deferred.promise));
+ }
+}
+
+exports.Front = Front;
diff --git a/devtools/shared/protocol/Front/FrontClassWithSpec.js b/devtools/shared/protocol/Front/FrontClassWithSpec.js
new file mode 100644
index 0000000000..55091be3e4
--- /dev/null
+++ b/devtools/shared/protocol/Front/FrontClassWithSpec.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+var { Front } = require("resource://devtools/shared/protocol/Front.js");
+
+/**
+ * Generates request methods as described by the given actor specification on
+ * the given front prototype. Returns the front prototype.
+ */
+var generateRequestMethods = function (actorSpec, frontProto) {
+ if (frontProto._actorSpec) {
+ throw new Error("frontProto called twice on the same front prototype!");
+ }
+
+ frontProto.typeName = actorSpec.typeName;
+
+ // Generate request methods.
+ const methods = actorSpec.methods;
+ methods.forEach(spec => {
+ const name = spec.name;
+
+ frontProto[name] = function (...args) {
+ // If the front is destroyed, the request will not be able to complete.
+ if (this.isDestroyed()) {
+ throw new Error(
+ `Can not send request '${name}' because front '${this.typeName}' is already destroyed.`
+ );
+ }
+
+ const startTime = Cu.now();
+ let packet;
+ try {
+ packet = spec.request.write(args, this);
+ } catch (ex) {
+ console.error("Error writing request: " + name);
+ throw ex;
+ }
+ if (spec.oneway) {
+ // Fire-and-forget oneway packets.
+ this.send(packet);
+ return undefined;
+ }
+
+ return this.request(packet).then(response => {
+ let ret;
+ if (!this.conn) {
+ throw new Error("Missing conn on " + this);
+ }
+ if (this.isDestroyed()) {
+ throw new Error(
+ `Can not interpret '${name}' response because front '${this.typeName}' is already destroyed.`
+ );
+ }
+ try {
+ ret = spec.response.read(response, this);
+ } catch (ex) {
+ console.error("Error reading response to: " + name + "\n" + ex);
+ throw ex;
+ }
+ ChromeUtils.addProfilerMarker(
+ "RDP Front",
+ startTime,
+ `${this.typeName}:${name}()`
+ );
+ return ret;
+ });
+ };
+
+ // Release methods should call the destroy function on return.
+ if (spec.release) {
+ const fn = frontProto[name];
+ frontProto[name] = function (...args) {
+ return fn.apply(this, args).then(result => {
+ this.destroy();
+ return result;
+ });
+ };
+ }
+ });
+
+ // Process event specifications
+ frontProto._clientSpec = {};
+
+ const actorEvents = actorSpec.events;
+ if (actorEvents) {
+ frontProto._clientSpec.events = new Map();
+
+ for (const [name, request] of actorEvents) {
+ frontProto._clientSpec.events.set(request.type, {
+ name,
+ request,
+ });
+ }
+ }
+
+ frontProto._actorSpec = actorSpec;
+
+ return frontProto;
+};
+
+/**
+ * Create a front class for the given actor specification and front prototype.
+ *
+ * @param object actorSpec
+ * The actor specification you're creating a front for.
+ * @param object proto
+ * The object prototype. Must have a 'typeName' property,
+ * should have method definitions, can have event definitions.
+ */
+var FrontClassWithSpec = function (actorSpec) {
+ class OneFront extends Front {}
+ generateRequestMethods(actorSpec, OneFront.prototype);
+ return OneFront;
+};
+exports.FrontClassWithSpec = FrontClassWithSpec;
diff --git a/devtools/shared/protocol/Front/moz.build b/devtools/shared/protocol/Front/moz.build
new file mode 100644
index 0000000000..75b36c5eab
--- /dev/null
+++ b/devtools/shared/protocol/Front/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ "FrontClassWithSpec.js",
+)
diff --git a/devtools/shared/protocol/Pool.js b/devtools/shared/protocol/Pool.js
new file mode 100644
index 0000000000..b5cb4c3eb1
--- /dev/null
+++ b/devtools/shared/protocol/Pool.js
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+/**
+ * Actor and Front implementations
+ */
+
+/**
+ * A protocol object that can manage the lifetime of other protocol
+ * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
+ *
+ * @param {DevToolsServerConnection|DevToolsClient} [conn]
+ * Either a DevToolsServerConnection or a DevToolsClient. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @param {String} [label]
+ * An optional label for the Pool.
+ * @constructor
+ */
+class Pool extends EventEmitter {
+ constructor(conn, label) {
+ super();
+
+ if (conn) {
+ this.conn = conn;
+ }
+ this.label = label;
+
+ // Will be individually flipped to true by Actor/Front classes.
+ // Will also only be exposed via Actor/Front::isDestroyed().
+ this._isDestroyed = false;
+ }
+
+ __poolMap = null;
+ parentPool = null;
+
+ /**
+ * Return the parent pool for this client.
+ */
+ getParent() {
+ return this.parentPool;
+ }
+
+ /**
+ * A pool is at the top of its pool hierarchy if it has:
+ * - no parent
+ * - or it is its own parent
+ */
+ isTopPool() {
+ const parent = this.getParent();
+ return !parent || parent === this;
+ }
+
+ poolFor(actorID) {
+ return this.conn.poolFor(actorID);
+ }
+
+ /**
+ * Override this if you want actors returned by this actor
+ * to belong to a different actor by default.
+ */
+ marshallPool() {
+ return this;
+ }
+
+ /**
+ * Pool is the base class for all actors, even leaf nodes.
+ * If the child map is actually referenced, go ahead and create
+ * the stuff needed by the pool.
+ */
+ get _poolMap() {
+ if (this.__poolMap) {
+ return this.__poolMap;
+ }
+ this.__poolMap = new Map();
+ this.conn.addActorPool(this);
+ return this.__poolMap;
+ }
+
+ /**
+ * Add an actor as a child of this pool.
+ */
+ manage(actor) {
+ if (!actor.actorID) {
+ actor.actorID = this.conn.allocID(actor.typeName);
+ } else {
+ // If the actor is already registered in a pool, remove it without destroying it.
+ // This happens for example when an addon is reloaded. To see this behavior, take a
+ // look at devtools/server/tests/xpcshell/test_addon_reload.js
+
+ const parent = actor.getParent();
+ if (parent && parent !== this) {
+ parent.unmanage(actor);
+ }
+ }
+
+ this._poolMap.set(actor.actorID, actor);
+ actor.parentPool = this;
+ }
+
+ unmanageChildren(FrontType) {
+ for (const front of this.poolChildren()) {
+ if (!FrontType || front instanceof FrontType) {
+ this.unmanage(front);
+ }
+ }
+ }
+
+ /**
+ * Remove an actor as a child of this pool.
+ */
+ unmanage(actor) {
+ if (this.__poolMap) {
+ this.__poolMap.delete(actor.actorID);
+ }
+ actor.parentPool = null;
+ }
+
+ // true if the given actor ID exists in the pool.
+ has(actorID) {
+ return this.__poolMap && this._poolMap.has(actorID);
+ }
+
+ /**
+ * Search for an actor in this pool, given an actorID
+ * @param {String} actorID
+ * @returns {Actor/null} Returns null if the actor wasn't found
+ */
+ getActorByID(actorID) {
+ if (this.__poolMap) {
+ return this._poolMap.get(actorID);
+ }
+ return null;
+ }
+
+ // Generator that yields each non-self child of the pool.
+ *poolChildren() {
+ if (!this.__poolMap) {
+ return;
+ }
+ for (const actor of this.__poolMap.values()) {
+ // Self-owned actors are ok, but don't need visiting twice.
+ if (actor === this) {
+ continue;
+ }
+ yield actor;
+ }
+ }
+
+ isDestroyed() {
+ // Note: _isDestroyed is only flipped from Actor and Front subclasses for
+ // now, so this method should not be called on pure Pool instances.
+ // See Bug 1717811.
+ return this._isDestroyed;
+ }
+
+ /**
+ * Pools can override this method in order to opt-out of a destroy sequence.
+ *
+ * For instance, Fronts are destroyed during the toolbox destroy. However when
+ * the toolbox is destroyed, the document holding the toolbox is also
+ * destroyed. So it should not be necessary to cleanup Fronts during toolbox
+ * destroy.
+ *
+ * For the time being, Fronts (or Pools in general) which want to opt-out of
+ * toolbox destroy can override this method and check the value of
+ * `this.conn.isToolboxDestroy`.
+ */
+ skipDestroy() {
+ return false;
+ }
+
+ /**
+ * Destroy this item, removing it from a parent if it has one,
+ * and destroying all children if necessary.
+ */
+ destroy() {
+ const parent = this.getParent();
+ if (parent) {
+ parent.unmanage(this);
+ }
+ if (!this.__poolMap) {
+ return;
+ }
+ // Immediately clear the poolmap so that we bail out early if the code is reentrant.
+ const poolMap = this.__poolMap;
+ this.__poolMap = null;
+
+ for (const actor of poolMap.values()) {
+ // Self-owned actors are ok, but don't need destroying twice.
+ if (actor === this) {
+ continue;
+ }
+
+ // Some pool-managed values don't extend Pool and won't have skipDestroy
+ // defined. For instance, the root actor and the lazy actors.
+ if (typeof actor.skipDestroy === "function" && actor.skipDestroy()) {
+ continue;
+ }
+
+ const destroy = actor.destroy;
+ if (destroy) {
+ // Disconnect destroy while we're destroying in case of (misbehaving)
+ // circular ownership.
+ actor.destroy = null;
+ destroy.call(actor);
+ actor.destroy = destroy;
+ }
+ }
+ this.conn.removeActorPool(this);
+ this.conn = null;
+ }
+}
+
+exports.Pool = Pool;
diff --git a/devtools/shared/protocol/Request.js b/devtools/shared/protocol/Request.js
new file mode 100644
index 0000000000..20befd938a
--- /dev/null
+++ b/devtools/shared/protocol/Request.js
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { extend } = require("resource://devtools/shared/extend.js");
+var {
+ findPlaceholders,
+ getPath,
+} = require("resource://devtools/shared/protocol/utils.js");
+var { types } = require("resource://devtools/shared/protocol/types.js");
+
+/**
+ * Manages a request template.
+ *
+ * @param object template
+ * The request template.
+ * @construcor
+ */
+var Request = function (template = {}) {
+ this.type = template.type;
+ this.template = template;
+ this.args = findPlaceholders(template, Arg);
+};
+
+Request.prototype = {
+ /**
+ * Write a request.
+ *
+ * @param array fnArgs
+ * The function arguments to place in the request.
+ * @param object ctx
+ * The object making the request.
+ * @returns a request packet.
+ */
+ write(fnArgs, ctx) {
+ const ret = {};
+ for (const key in this.template) {
+ const value = this.template[key];
+ if (value instanceof Arg) {
+ ret[key] = value.write(
+ value.index in fnArgs ? fnArgs[value.index] : undefined,
+ ctx,
+ key
+ );
+ } else if (key == "type") {
+ ret[key] = value;
+ } else {
+ throw new Error(
+ "Request can only an object with `Arg` or `Option` properties"
+ );
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * Read a request.
+ *
+ * @param object packet
+ * The request packet.
+ * @param object ctx
+ * The object making the request.
+ * @returns an arguments array
+ */
+ read(packet, ctx) {
+ const fnArgs = [];
+ for (const templateArg of this.args) {
+ const arg = templateArg.placeholder;
+ const path = templateArg.path;
+ const name = path[path.length - 1];
+ arg.read(getPath(packet, path), ctx, fnArgs, name);
+ }
+ return fnArgs;
+ },
+};
+
+exports.Request = Request;
+
+/**
+ * Request/Response templates and generation
+ *
+ * Request packets are specified as json templates with
+ * Arg and Option placeholders where arguments should be
+ * placed.
+ *
+ * Reponse packets are also specified as json templates,
+ * with a RetVal placeholder where the return value should be
+ * placed.
+ */
+
+/**
+ * Placeholder for simple arguments.
+ *
+ * @param number index
+ * The argument index to place at this position.
+ * @param type type
+ * The argument should be marshalled as this type.
+ * @constructor
+ */
+var Arg = function (index, type) {
+ this.index = index;
+ // Prevent force loading all Arg types by accessing it only when needed
+ loader.lazyGetter(this, "type", function () {
+ return types.getType(type);
+ });
+};
+
+Arg.prototype = {
+ write(arg, ctx) {
+ return this.type.write(arg, ctx);
+ },
+
+ read(v, ctx, outArgs) {
+ outArgs[this.index] = this.type.read(v, ctx);
+ },
+};
+
+// Outside of protocol.js, Arg is called as factory method, without the new keyword.
+exports.Arg = function (index, type) {
+ return new Arg(index, type);
+};
+
+/**
+ * Placeholder for an options argument value that should be hoisted
+ * into the packet.
+ *
+ * If provided in a method specification:
+ *
+ * { optionArg: Option(1)}
+ *
+ * Then arguments[1].optionArg will be placed in the packet in this
+ * value's place.
+ *
+ * @param number index
+ * The argument index of the options value.
+ * @param type type
+ * The argument should be marshalled as this type.
+ * @constructor
+ */
+var Option = function (index, type) {
+ Arg.call(this, index, type);
+};
+
+Option.prototype = extend(Arg.prototype, {
+ write(arg, ctx, name) {
+ // Ignore if arg is undefined or null; allow other falsy values
+ if (arg == undefined || arg[name] == undefined) {
+ return undefined;
+ }
+ const v = arg[name];
+ return this.type.write(v, ctx);
+ },
+ read(v, ctx, outArgs, name) {
+ if (outArgs[this.index] === undefined) {
+ outArgs[this.index] = {};
+ }
+ if (v === undefined) {
+ return;
+ }
+ outArgs[this.index][name] = this.type.read(v, ctx);
+ },
+});
+
+// Outside of protocol.js, Option is called as factory method, without the new keyword.
+exports.Option = function (index, type) {
+ return new Option(index, type);
+};
diff --git a/devtools/shared/protocol/Response.js b/devtools/shared/protocol/Response.js
new file mode 100644
index 0000000000..b456fe6149
--- /dev/null
+++ b/devtools/shared/protocol/Response.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+var {
+ findPlaceholders,
+ getPath,
+} = require("resource://devtools/shared/protocol/utils.js");
+var { types } = require("resource://devtools/shared/protocol/types.js");
+
+/**
+ * Manages a response template.
+ *
+ * @param object template
+ * The response template.
+ * @construcor
+ */
+var Response = function (template = {}) {
+ this.template = template;
+ if (this.template instanceof RetVal && this.template.isArrayType()) {
+ throw Error("Arrays should be wrapped in objects");
+ }
+
+ const placeholders = findPlaceholders(template, RetVal);
+ if (placeholders.length > 1) {
+ throw Error("More than one RetVal specified in response");
+ }
+ const placeholder = placeholders.shift();
+ if (placeholder) {
+ this.retVal = placeholder.placeholder;
+ this.path = placeholder.path;
+ }
+};
+
+Response.prototype = {
+ /**
+ * Write a response for the given return value.
+ *
+ * @param val ret
+ * The return value.
+ * @param object ctx
+ * The object writing the response.
+ */
+ write(ret, ctx) {
+ // Consider that `template` is either directly a `RetVal`,
+ // or a dictionary with may be one `RetVal`.
+ if (this.template instanceof RetVal) {
+ return this.template.write(ret, ctx);
+ }
+ const result = {};
+ for (const key in this.template) {
+ const value = this.template[key];
+ if (value instanceof RetVal) {
+ result[key] = value.write(ret, ctx);
+ } else {
+ throw new Error(
+ "Response can only be a `RetVal` instance or an object " +
+ "with one property being a `RetVal` instance."
+ );
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Read a return value from the given response.
+ *
+ * @param object packet
+ * The response packet.
+ * @param object ctx
+ * The object reading the response.
+ */
+ read(packet, ctx) {
+ if (!this.retVal) {
+ return undefined;
+ }
+ const v = getPath(packet, this.path);
+ return this.retVal.read(v, ctx);
+ },
+};
+
+exports.Response = Response;
+
+/**
+ * Placeholder for return values in a response template.
+ *
+ * @param type type
+ * The return value should be marshalled as this type.
+ */
+var RetVal = function (type) {
+ this._type = type;
+ // Prevent force loading all RetVal types by accessing it only when needed
+ loader.lazyGetter(this, "type", function () {
+ return types.getType(type);
+ });
+};
+
+RetVal.prototype = {
+ write(v, ctx) {
+ return this.type.write(v, ctx);
+ },
+
+ read(v, ctx) {
+ return this.type.read(v, ctx);
+ },
+
+ isArrayType() {
+ // `_type` should always be a string, but a few incorrect RetVal calls
+ // pass `0`. See Bug 1677703.
+ return typeof this._type === "string" && this._type.startsWith("array:");
+ },
+};
+
+// Outside of protocol.js, RetVal is called as factory method, without the new keyword.
+exports.RetVal = function (type) {
+ return new RetVal(type);
+};
diff --git a/devtools/shared/protocol/lazy-pool.js b/devtools/shared/protocol/lazy-pool.js
new file mode 100644
index 0000000000..0829fef1e0
--- /dev/null
+++ b/devtools/shared/protocol/lazy-pool.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { extend } = require("devtools/shared/extend");
+const { Pool } = require("devtools/shared/protocol");
+
+/**
+ * A Special Pool for RootActor and WindowGlobalTargetActor, which allows lazy loaded
+ * actors to be added to the pool.
+ *
+ * Like the Pool, this is a protocol object that can manage the lifetime of other protocol
+ * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
+ *
+ * @param conn
+ * Is a DevToolsServerConnection. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * @constructor
+ */
+function LazyPool(conn) {
+ this.conn = conn;
+}
+
+LazyPool.prototype = extend(Pool.prototype, {
+ // The actor for a given actor id stored in this pool
+ getActorByID(actorID) {
+ if (this.__poolMap) {
+ const entry = this._poolMap.get(actorID);
+ if (entry instanceof LazyActor) {
+ return entry.createActor();
+ }
+ return entry;
+ }
+ return null;
+ },
+});
+
+exports.LazyPool = LazyPool;
+
+/**
+ * Populate |parent._extraActors| as specified by |registeredActors|, reusing whatever
+ * actors are already there. Add all actors in the final extra actors table to
+ * |pool|. _extraActors is treated as a cache for lazy actors
+ *
+ * The target actor uses this to instantiate actors that other
+ * parts of the browser have specified with ActorRegistry.addTargetScopedActor
+ *
+ * @param factories
+ * An object whose own property names are the names of properties to add to
+ * some reply packet (say, a target actor grip or the "listTabs" response
+ * form), and whose own property values are actor constructor functions, as
+ * documented for addTargetScopedActor
+ *
+ * @param parent
+ * The parent TargetActor with which the new actors
+ * will be associated. It should support whatever API the |factories|
+ * constructor functions might be interested in, as it is passed to them.
+ * For the sake of CommonCreateExtraActors itself, it should have at least
+ * the following properties:
+ *
+ * - _extraActors
+ * An object whose own property names are factory table (and packet)
+ * property names, and whose values are no-argument actor constructors,
+ * of the sort that one can add to a Pool.
+ *
+ * - conn
+ * The DevToolsServerConnection in which the new actors will participate.
+ *
+ * - actorID
+ * The actor's name, for use as the new actors' parentID.
+ * @param pool
+ * An object which implements the protocol.js Pool interface, and has the
+ * following properties
+ *
+ * - manage
+ * a function which adds a given actor to an actor pool
+ */
+function createExtraActors(registeredActors, pool, parent) {
+ // Walk over global actors added by extensions.
+ const nameMap = {};
+ for (const name in registeredActors) {
+ let actor = parent._extraActors[name];
+ if (!actor) {
+ // Register another factory, but this time specific to this connection.
+ // It creates a fake actor that looks like an regular actor in the pool,
+ // but without actually instantiating the actor.
+ // It will only be instantiated on the first request made to the actor.
+ actor = new LazyActor(registeredActors[name], parent, pool);
+ parent._extraActors[name] = actor;
+ }
+
+ // If the actor already exists in the pool, it may have been instantiated,
+ // so make sure not to overwrite it by a non-instantiated version.
+ if (!pool.has(actor.actorID)) {
+ pool.manage(actor);
+ }
+ nameMap[name] = actor.actorID;
+ }
+ return nameMap;
+}
+
+exports.createExtraActors = createExtraActors;
+
+/**
+ * Creates an "actor-like" object which responds in the same way as an ordinary actor
+ * but has fewer capabilities (ie, does not manage lifetimes or have it's own pool).
+ *
+ *
+ * @param factories
+ * An object whose own property names are the names of properties to add to
+ * some reply packet (say, a target actor grip or the "listTabs" response
+ * form), and whose own property values are actor constructor functions, as
+ * documented for addTargetScopedActor
+ *
+ * @param parent
+ * The parent TargetActor with which the new actors
+ * will be associated. It should support whatever API the |factories|
+ * constructor functions might be interested in, as it is passed to them.
+ * For the sake of CommonCreateExtraActors itself, it should have at least
+ * the following properties:
+ *
+ * - _extraActors
+ * An object whose own property names are factory table (and packet)
+ * property names, and whose values are no-argument actor constructors,
+ * of the sort that one can add to a Pool.
+ *
+ * - conn
+ * The DevToolsServerConnection in which the new actors will participate.
+ *
+ * - actorID
+ * The actor's name, for use as the new actors' parentID.
+ * @param pool
+ * An object which implements the protocol.js Pool interface, and has the
+ * following properties
+ *
+ * - manage
+ * a function which adds a given actor to an actor pool
+ */
+
+function LazyActor(factory, parent, pool) {
+ this._options = factory.options;
+ this._parentActor = parent;
+ this._name = factory.name;
+ this._pool = pool;
+
+ // needed for taking a place in a pool
+ this.typeName = factory.name;
+}
+
+LazyActor.prototype = {
+ loadModule(id) {
+ const options = this._options;
+ try {
+ return require(id);
+ // Fetch the actor constructor
+ } catch (e) {
+ throw new Error(
+ `Unable to load actor module '${options.id}'\n${e.message}\n${e.stack}\n`
+ );
+ }
+ },
+
+ getConstructor() {
+ const options = this._options;
+ if (options.constructorFun) {
+ // Actor definition registered by testing helpers
+ return options.constructorFun;
+ }
+ // Lazy actor definition, where options contains all the information
+ // required to load the actor lazily.
+ // Exposes `name` attribute in order to allow removeXXXActor to match
+ // the actor by its actor constructor name.
+ this.name = options.constructorName;
+ const module = this.loadModule(options.id);
+ const constructor = module[options.constructorName];
+ if (!constructor) {
+ throw new Error(
+ `Unable to find actor constructor named '${this.name}'. (Is it exported?)`
+ );
+ }
+ return constructor;
+ },
+
+ /**
+ * Return the parent pool for this lazy actor.
+ */
+ getParent() {
+ return this.conn && this.conn.poolFor(this.actorID);
+ },
+
+ /**
+ * This will only happen if the actor is destroyed before it is created
+ * We do not want to use the Pool destruction method, because this actor
+ * has no pool. However, it might have a parent that should unmange this
+ * actor
+ */
+ destroy() {
+ const parent = this.getParent();
+ if (parent) {
+ parent.unmanage(this);
+ }
+ },
+
+ createActor() {
+ // Fetch the actor constructor
+ const Constructor = this.getConstructor();
+ // Instantiate a new actor instance
+ const conn = this._parentActor.conn;
+ // this should be taken care of once all actors are moved to protocol.js
+ const instance = new Constructor(conn, this._parentActor);
+ instance.conn = conn;
+
+ // We want the newly-constructed actor to completely replace the factory
+ // actor. Reusing the existing actor ID will make sure Pool.manage
+ // replaces the old actor with the new actor.
+ instance.actorID = this.actorID;
+
+ this._pool.manage(instance);
+
+ return instance;
+ },
+};
diff --git a/devtools/shared/protocol/moz.build b/devtools/shared/protocol/moz.build
new file mode 100644
index 0000000000..a75a7ae3ae
--- /dev/null
+++ b/devtools/shared/protocol/moz.build
@@ -0,0 +1,22 @@
+# 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 += [
+ "Actor",
+ "Front",
+]
+
+DevToolsModules(
+ "Actor.js",
+ "Front.js",
+ "lazy-pool.js",
+ "Pool.js",
+ "Request.js",
+ "Response.js",
+ "types.js",
+ "utils.js",
+)
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
diff --git a/devtools/shared/protocol/tests/xpcshell/.eslintrc.js b/devtools/shared/protocol/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/protocol/tests/xpcshell/head.js b/devtools/shared/protocol/tests/xpcshell/head.js
new file mode 100644
index 0000000000..dd055ddb42
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/head.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+function dumpn(msg) {
+ dump("DBG-TEST: " + msg + "\n");
+}
+
+function connectPipeTracing() {
+ return new TracingTransport(DevToolsServer.connectPipe());
+}
+
+/**
+ * Mock the `Transport` class in order to intercept all the packet
+ * getting in and out and then being able to assert them and dump them.
+ */
+function TracingTransport(childTransport) {
+ this.hooks = null;
+ this.child = childTransport;
+ this.child.hooks = this;
+
+ this.expectations = [];
+ this.packets = [];
+ this.checkIndex = 0;
+}
+
+TracingTransport.prototype = {
+ // Remove actor names
+ normalize(packet) {
+ return JSON.parse(
+ JSON.stringify(packet, (key, value) => {
+ if (key === "to" || key === "from" || key === "actor") {
+ return "<actorid>";
+ }
+ return value;
+ })
+ );
+ },
+ send(packet) {
+ this.packets.push({
+ type: "sent",
+ packet: this.normalize(packet),
+ });
+ return this.child.send(packet);
+ },
+ close() {
+ return this.child.close();
+ },
+ ready() {
+ return this.child.ready();
+ },
+ onPacket(packet) {
+ this.packets.push({
+ type: "received",
+ packet: this.normalize(packet),
+ });
+ this.hooks.onPacket(packet);
+ },
+ onTransportClosed() {
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed();
+ }
+ },
+
+ expectSend(expected) {
+ const packet = this.packets[this.checkIndex++];
+ Assert.equal(packet.type, "sent");
+ deepEqual(packet.packet, this.normalize(expected));
+ },
+
+ expectReceive(expected) {
+ const packet = this.packets[this.checkIndex++];
+ Assert.equal(packet.type, "received");
+ deepEqual(packet.packet, this.normalize(expected));
+ },
+
+ // Write your tests, call dumpLog at the end, inspect the output,
+ // then sprinkle the calls through the right places in your test.
+ dumpLog() {
+ for (const entry of this.packets) {
+ if (entry.type === "sent") {
+ dumpn("trace.expectSend(" + entry.packet + ");");
+ } else {
+ dumpn("trace.expectReceive(" + entry.packet + ");");
+ }
+ }
+ },
+};
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js
new file mode 100644
index 0000000000..ce237e1c00
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Outstanding requests should be rejected when the connection aborts
+ * unexpectedly.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal } = protocol;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return this.sequence++;
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const onSimpleReturn = rootFront.simpleReturn();
+ trace.close();
+
+ try {
+ await onSimpleReturn;
+ ok(false, "Connection was aborted, request shouldn't resolve");
+ } catch (e) {
+ const error = e.toString();
+ ok(true, "Connection was aborted, request rejected correctly");
+ ok(error.includes("Request stack:"), "Error includes request stack");
+ ok(error.includes("test_protocol_abort.js"), "Stack includes this test");
+ }
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js
new file mode 100644
index 0000000000..dd7196710b
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure we get replies in the same order that we sent their
+ * requests even when earlier requests take several event ticks to
+ * complete.
+ */
+
+const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js");
+const protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal } = protocol;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ promiseReturn: {
+ request: { toWait: Arg(0, "number") },
+ response: { value: RetVal("number") },
+ },
+ simpleThrow: {
+ response: { value: RetVal("number") },
+ },
+ promiseThrow: {
+ request: { toWait: Arg(0, "number") },
+ response: { value: RetVal("number") },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return this.sequence++;
+ }
+
+ // Guarantee that this resolves after simpleReturn returns.
+ async promiseReturn(toWait) {
+ const sequence = this.sequence++;
+
+ // Wait until the number of requests specified by toWait have
+ // happened, to test queuing.
+ while (this.sequence - sequence < toWait) {
+ await waitForTick();
+ }
+
+ return sequence;
+ }
+
+ simpleThrow() {
+ throw new Error(this.sequence++);
+ }
+
+ // Guarantee that this resolves after simpleReturn returns.
+ promiseThrow(toWait) {
+ return this.promiseReturn(toWait).then(Promise.reject);
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const calls = [];
+ let sequence = 0;
+
+ // Execute a call that won't finish processing until 2
+ // more calls have happened
+ calls.push(
+ rootFront.promiseReturn(2).then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 0);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ // Put a few requests into the backlog
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 1);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 2);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleThrow().then(
+ () => {
+ Assert.ok(false, "simpleThrow shouldn't succeed!");
+ },
+ error => {
+ // Check right return order
+ Assert.equal(sequence++, 3);
+ }
+ )
+ );
+
+ calls.push(
+ rootFront.promiseThrow(2).then(
+ () => {
+ Assert.ok(false, "promiseThrow shouldn't succeed!");
+ },
+ error => {
+ // Check right return order
+ Assert.equal(sequence++, 4);
+ Assert.ok(true, "simple throw should throw");
+ }
+ )
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 5);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ // Break up the backlog with a long request that waits
+ // for another simpleReturn before completing
+ calls.push(
+ rootFront.promiseReturn(1).then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 6);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 7);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ await Promise.all(calls);
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js
new file mode 100644
index 0000000000..728e58c6b9
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js
@@ -0,0 +1,700 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+const protocol = require("resource://devtools/shared/protocol.js");
+const { types, Arg, RetVal } = protocol;
+
+// Predeclaring the actor type so that it can be used in the
+// implementation of the child actor.
+types.addActorType("childActor");
+types.addActorType("otherChildActor");
+types.addPolymorphicType("polytype", ["childActor", "otherChildActor"]);
+
+const childSpec = protocol.generateActorSpec({
+ typeName: "childActor",
+
+ events: {
+ event1: {
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ event2: {
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ "named-event": {
+ type: "namedEvent",
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ "object-event": {
+ type: "objectEvent",
+ detail: Arg(0, "childActor#actorid"),
+ },
+ "array-object-event": {
+ type: "arrayObjectEvent",
+ detail: Arg(0, "array:childActor#actorid"),
+ },
+ },
+
+ methods: {
+ echo: {
+ request: { str: Arg(0) },
+ response: { str: RetVal("string") },
+ },
+ getDetail1: {
+ response: {
+ child: RetVal("childActor#actorid"),
+ },
+ },
+ getDetail2: {
+ response: {
+ child: RetVal("childActor#actorid"),
+ },
+ },
+ getIDDetail: {
+ response: {
+ idDetail: RetVal("childActor#actorid"),
+ },
+ },
+ getIntArray: {
+ request: { inputArray: Arg(0, "array:number") },
+ response: {
+ intArray: RetVal("array:number"),
+ },
+ },
+ getSibling: {
+ request: { id: Arg(0) },
+ response: { sibling: RetVal("childActor") },
+ },
+ emitEvents: {
+ response: { value: RetVal("string") },
+ },
+ release: {
+ release: true,
+ },
+ },
+});
+
+class ChildActor extends protocol.Actor {
+ constructor(conn, id) {
+ super(conn, childSpec);
+ this.childID = id;
+ }
+
+ // Actors returned by this actor should be owned by the root actor.
+ marshallPool() {
+ return this.getParent();
+ }
+
+ toString() {
+ return "[ChildActor " + this.childID + "]";
+ }
+
+ destroy() {
+ super.destroy();
+ this.destroyed = true;
+ }
+
+ form() {
+ return {
+ actor: this.actorID,
+ childID: this.childID,
+ };
+ }
+
+ echo(str) {
+ return str;
+ }
+
+ getDetail1() {
+ return this;
+ }
+
+ getDetail2() {
+ return this;
+ }
+
+ getIDDetail() {
+ return this;
+ }
+
+ getIntArray(inputArray) {
+ // Test that protocol.js converts an iterator to an array.
+ const f = function* () {
+ for (const i of inputArray) {
+ yield 2 * i;
+ }
+ };
+ return f();
+ }
+
+ getSibling(id) {
+ return this.getParent().getChild(id);
+ }
+
+ emitEvents() {
+ this.emit("event1", 1, 2, 3);
+ this.emit("event2", 4, 5, 6);
+ this.emit("named-event", 1, 2, 3);
+ this.emit("object-event", this);
+ this.emit("array-object-event", [this]);
+ return "correct response";
+ }
+
+ release() {}
+}
+
+class ChildFront extends protocol.FrontClassWithSpec(childSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._parentFront = parentFront;
+
+ this.before("event1", this.onEvent1.bind(this));
+ this.before("event2", this.onEvent2a.bind(this));
+ this.on("event2", this.onEvent2b.bind(this));
+ }
+
+ destroy() {
+ this.destroyed = true;
+ // Call parent's destroy, which may be re-entrant and recall this function
+ this._parentFront.destroy();
+ super.destroy();
+ }
+
+ marshallPool() {
+ return this.getParent();
+ }
+
+ toString() {
+ return "[child front " + this.childID + "]";
+ }
+
+ form(form) {
+ this.childID = form.childID;
+ }
+
+ onEvent1(a, b, c) {
+ this.event1arg3 = c;
+ }
+
+ onEvent2a(a, b, c) {
+ return Promise.resolve().then(() => {
+ this.event2arg3 = c;
+ });
+ }
+
+ onEvent2b(a, b, c) {
+ this.event2arg2 = b;
+ }
+}
+protocol.registerFront(ChildFront);
+
+const otherChildSpec = protocol.generateActorSpec({
+ typeName: "otherChildActor",
+ methods: {
+ getOtherChild: {
+ request: {},
+ response: { sibling: RetVal("otherChildActor") },
+ },
+ },
+ events: {},
+});
+
+class OtherChildActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, otherChildSpec);
+ }
+
+ getOtherChild() {
+ return new OtherChildActor(this.conn);
+ }
+}
+
+class OtherChildFront extends protocol.FrontClassWithSpec(otherChildSpec) {}
+protocol.registerFront(OtherChildFront);
+
+types.addDictType("manyChildrenDict", {
+ child5: "childActor",
+ more: "array:childActor",
+});
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ getChild: {
+ request: { str: Arg(0) },
+ response: { actor: RetVal("childActor") },
+ },
+ getOtherChild: {
+ request: {},
+ response: { sibling: RetVal("otherChildActor") },
+ },
+ getChildren: {
+ request: { ids: Arg(0, "array:string") },
+ response: { children: RetVal("array:childActor") },
+ },
+ getChildren2: {
+ request: { ids: Arg(0, "array:childActor") },
+ response: { children: RetVal("array:childActor") },
+ },
+ getManyChildren: {
+ response: RetVal("manyChildrenDict"),
+ },
+ getPolymorphism: {
+ request: { id: Arg(0, "number") },
+ response: { child: RetVal("polytype") },
+ },
+ requestPolymorphism: {
+ request: {
+ id: Arg(0, "number"),
+ actor: Arg(1, "polytype"),
+ },
+ response: { child: RetVal("polytype") },
+ },
+ },
+});
+
+let rootActor = null;
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ rootActor = this;
+ this.actorID = "root";
+ this._children = {};
+ }
+
+ toString() {
+ return "[root actor]";
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ getChild(id) {
+ if (id in this._children) {
+ return this._children[id];
+ }
+ const child = new ChildActor(this.conn, id);
+ this._children[id] = child;
+ return child;
+ }
+
+ // Other child actor won't all be own by the root actor
+ // and can have their own children
+ getOtherChild() {
+ return new OtherChildActor(this.conn);
+ }
+
+ getChildren(ids) {
+ return ids.map(id => this.getChild(id));
+ }
+
+ getChildren2(ids) {
+ const f = function* () {
+ for (const c of ids) {
+ yield c;
+ }
+ };
+ return f();
+ }
+
+ getManyChildren() {
+ return {
+ // note that this isn't in the specialization array.
+ foo: "bar",
+ child5: this.getChild("child5"),
+ more: [this.getChild("child6"), this.getChild("child7")],
+ };
+ }
+
+ getPolymorphism(id) {
+ if (id == 0) {
+ return new ChildActor(this.conn, id);
+ } else if (id == 1) {
+ return new OtherChildActor(this.conn);
+ }
+ throw new Error("Unexpected id");
+ }
+
+ requestPolymorphism(id, actor) {
+ if (id == 0 && actor instanceof ChildActor) {
+ return actor;
+ } else if (id == 1 && actor instanceof OtherChildActor) {
+ return actor;
+ }
+ throw new Error("Unexpected id or actor");
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.actorID = "root";
+ // Root actor owns itself.
+ this.manage(this);
+ }
+
+ toString() {
+ return "[root front]";
+ }
+}
+
+let rootFront, childFront;
+function expectRootChildren(size) {
+ Assert.equal(rootActor._poolMap.size, size);
+ Assert.equal(rootFront._poolMap.size, size + 1);
+ if (childFront) {
+ Assert.equal(childFront._poolMap.size, 0);
+ }
+}
+protocol.registerFront(RootFront);
+
+function childrenOfType(pool, type) {
+ const children = [...rootFront.poolChildren()];
+ return children.filter(child => child instanceof type);
+}
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => {
+ return new RootActor(conn);
+ };
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ const [applicationType] = await client.connect();
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+
+ rootFront = client.mainRoot;
+
+ await testSimpleChildren(trace);
+ await testDetail(trace);
+ await testSibling(trace);
+ await testEvents(trace);
+ await testManyChildren(trace);
+ await testGenerator(trace);
+ await testPolymorphism(trace);
+ await testUnmanageChildren(trace);
+ // Execute that assertion very last as it destroy the root front and actor
+ await testDestroy(trace);
+
+ await client.close();
+});
+
+async function testSimpleChildren(trace) {
+ childFront = await rootFront.getChild("child1");
+ trace.expectSend({ type: "getChild", str: "child1", to: "<actorid>" });
+ trace.expectReceive({ actor: "<actorid>", from: "<actorid>" });
+
+ Assert.ok(childFront instanceof ChildFront);
+ Assert.equal(childFront.childID, "child1");
+ expectRootChildren(1);
+
+ // Request the child again, make sure the same is returned.
+ let ret = await rootFront.getChild("child1");
+ trace.expectSend({ type: "getChild", str: "child1", to: "<actorid>" });
+ trace.expectReceive({ actor: "<actorid>", from: "<actorid>" });
+
+ expectRootChildren(1);
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.echo("hello");
+ trace.expectSend({ type: "echo", str: "hello", to: "<actorid>" });
+ trace.expectReceive({ str: "hello", from: "<actorid>" });
+
+ Assert.equal(ret, "hello");
+}
+
+async function testDetail(trace) {
+ let ret = await childFront.getDetail1();
+ trace.expectSend({ type: "getDetail1", to: "<actorid>" });
+ trace.expectReceive({ child: childFront.actorID, from: "<actorid>" });
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.getDetail2();
+ trace.expectSend({ type: "getDetail2", to: "<actorid>" });
+ trace.expectReceive({ child: childFront.actorID, from: "<actorid>" });
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.getIDDetail();
+ trace.expectSend({ type: "getIDDetail", to: "<actorid>" });
+ trace.expectReceive({
+ idDetail: childFront.actorID,
+ from: "<actorid>",
+ });
+ Assert.ok(ret === childFront);
+}
+
+async function testSibling(trace) {
+ await childFront.getSibling("siblingID");
+ trace.expectSend({
+ type: "getSibling",
+ id: "siblingID",
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ sibling: { actor: "<actorid>", childID: "siblingID" },
+ from: "<actorid>",
+ });
+
+ expectRootChildren(2);
+}
+
+async function testEvents(trace) {
+ const ret = await rootFront.getChildren(["child1", "child2"]);
+ trace.expectSend({
+ type: "getChildren",
+ ids: ["child1", "child2"],
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ children: [
+ { actor: "<actorid>", childID: "child1" },
+ { actor: "<actorid>", childID: "child2" },
+ ],
+ from: "<actorid>",
+ });
+
+ expectRootChildren(3);
+ Assert.ok(ret[0] === childFront);
+ Assert.ok(ret[1] !== childFront);
+ Assert.ok(ret[1] instanceof ChildFront);
+
+ // On both children, listen to events. We're only
+ // going to trigger events on the first child, so an event
+ // triggered on the second should cause immediate failures.
+
+ const set = new Set([
+ "event1",
+ "event2",
+ "named-event",
+ "object-event",
+ "array-object-event",
+ ]);
+
+ childFront.on("event1", (a, b, c) => {
+ Assert.equal(a, 1);
+ Assert.equal(b, 2);
+ Assert.equal(c, 3);
+ // Verify that the pre-event handler was called.
+ Assert.equal(childFront.event1arg3, 3);
+ set.delete("event1");
+ });
+ childFront.on("event2", (a, b, c) => {
+ Assert.equal(a, 4);
+ Assert.equal(b, 5);
+ Assert.equal(c, 6);
+ // Verify that the async pre-event handler was called,
+ // setting the property before this handler was called.
+ Assert.equal(childFront.event2arg3, 6);
+ // And check that the sync preEvent with the same name is also
+ // executed
+ Assert.equal(childFront.event2arg2, 5);
+ set.delete("event2");
+ });
+ childFront.on("named-event", (a, b, c) => {
+ Assert.equal(a, 1);
+ Assert.equal(b, 2);
+ Assert.equal(c, 3);
+ set.delete("named-event");
+ });
+ childFront.on("object-event", obj => {
+ Assert.ok(obj === childFront);
+ set.delete("object-event");
+ });
+ childFront.on("array-object-event", array => {
+ Assert.ok(array[0] === childFront);
+ set.delete("array-object-event");
+ });
+
+ const fail = function () {
+ do_throw("Unexpected event");
+ };
+ ret[1].on("event1", fail);
+ ret[1].on("event2", fail);
+ ret[1].on("named-event", fail);
+ ret[1].on("object-event", fail);
+ ret[1].on("array-object-event", fail);
+
+ await childFront.emitEvents();
+ trace.expectSend({ type: "emitEvents", to: "<actorid>" });
+ trace.expectReceive({
+ type: "event1",
+ a: 1,
+ b: 2,
+ c: 3,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "event2",
+ a: 4,
+ b: 5,
+ c: 6,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "namedEvent",
+ a: 1,
+ b: 2,
+ c: 3,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "objectEvent",
+ detail: childFront.actorID,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "arrayObjectEvent",
+ detail: [childFront.actorID],
+ from: "<actorid>",
+ });
+ trace.expectReceive({ value: "correct response", from: "<actorid>" });
+
+ Assert.equal(set.size, 0);
+}
+
+async function testManyChildren(trace) {
+ const ret = await rootFront.getManyChildren();
+ trace.expectSend({ type: "getManyChildren", to: "<actorid>" });
+ trace.expectReceive({
+ foo: "bar",
+ child5: { actor: "<actorid>", childID: "child5" },
+ more: [
+ { actor: "<actorid>", childID: "child6" },
+ { actor: "<actorid>", childID: "child7" },
+ ],
+ from: "<actorid>",
+ });
+
+ // Check all the crazy stuff we did in getManyChildren
+ Assert.equal(ret.foo, "bar");
+ Assert.equal(ret.child5.childID, "child5");
+ Assert.equal(ret.more[0].childID, "child6");
+ Assert.equal(ret.more[1].childID, "child7");
+}
+
+async function testGenerator(trace) {
+ // Test accepting a generator.
+ const f = function* () {
+ for (const i of [1, 2, 3, 4, 5]) {
+ yield i;
+ }
+ };
+ let ret = await childFront.getIntArray(f());
+ Assert.equal(ret.length, 5);
+ const expected = [2, 4, 6, 8, 10];
+ for (let i = 0; i < 5; ++i) {
+ Assert.equal(ret[i], expected[i]);
+ }
+
+ const ids = await rootFront.getChildren(["child1", "child2"]);
+ const f2 = function* () {
+ for (const id of ids) {
+ yield id;
+ }
+ };
+ ret = await rootFront.getChildren2(f2());
+ Assert.equal(ret.length, 2);
+ Assert.ok(ret[0] === childFront);
+ Assert.ok(ret[1] !== childFront);
+ Assert.ok(ret[1] instanceof ChildFront);
+}
+
+async function testPolymorphism(trace) {
+ // Check polymorphic types returned by an actor
+ const firstChild = await rootFront.getPolymorphism(0);
+ Assert.ok(firstChild instanceof ChildFront);
+
+ // Check polymorphic types passed to a front
+ const sameFirstChild = await rootFront.requestPolymorphism(0, firstChild);
+ Assert.ok(sameFirstChild instanceof ChildFront);
+ Assert.equal(sameFirstChild, firstChild);
+
+ // Same with the second possible type
+ const secondChild = await rootFront.getPolymorphism(1);
+ Assert.ok(secondChild instanceof OtherChildFront);
+
+ const sameSecondChild = await rootFront.requestPolymorphism(1, secondChild);
+ Assert.ok(sameSecondChild instanceof OtherChildFront);
+ Assert.equal(sameSecondChild, secondChild);
+
+ // Check that any other type is rejected
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, null);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got an empty value/);
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, 42);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got value: '42'/);
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, rootFront);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got an actor of type: 'root'/);
+}
+
+async function testUnmanageChildren(trace) {
+ // There is already one front of type OtherChildFront
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 1);
+
+ // Create another front of type OtherChildFront
+ const front = await rootFront.getPolymorphism(1);
+ Assert.ok(front instanceof OtherChildFront);
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 2);
+
+ // Remove all fronts of type OtherChildFront
+ rootFront.unmanageChildren(OtherChildFront);
+ Assert.ok(
+ !front.isDestroyed(),
+ "Unmanaged front is not considered as destroyed"
+ );
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 0);
+}
+
+async function testDestroy(trace) {
+ const front = await rootFront.getOtherChild();
+ const otherChildFront = await front.getOtherChild();
+ Assert.equal(
+ otherChildFront.getParent(),
+ front,
+ "the child is a children of first front"
+ );
+
+ front.destroy();
+ Assert.ok(front.isDestroyed(), "sibling is correctly reported as destroyed");
+ Assert.ok(!front.getParent(), "sibling has no more parent declared");
+ Assert.ok(otherChildFront.isDestroyed(), "the child is also destroyed");
+ Assert.ok(
+ !otherChildFront.getParent(),
+ "the child also has no more parent declared"
+ );
+ Assert.ok(
+ !otherChildFront.parentPool,
+ "the child also has its parentPool attribute nullified"
+ );
+
+ // Verify that re-entrant Front.destroy doesn't throw, nor loop
+ // Execute that very last as it will destroy the root actor and front
+ const sibling = await childFront.getSibling("siblingID");
+ sibling.destroy();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
new file mode 100644
index 0000000000..ef566d6b97
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { lazyLoadFront } = require("resource://devtools/shared/specs/index.js");
+const Types =
+ require("resource://devtools/shared/specs/index.js").__TypesForTests;
+const { getType } = require("resource://devtools/shared/protocol.js").types;
+
+function run_test() {
+ test_index_is_alphabetically_sorted();
+ test_specs();
+ test_fronts();
+}
+
+// Check alphabetic order of specs defined in devtools/shared/specs/index.js,
+// in order to ease its maintenance and readability.
+function test_index_is_alphabetically_sorted() {
+ let lastSpec = "";
+ for (const type of Types) {
+ const spec = type.spec;
+ if (lastSpec && spec < lastSpec) {
+ ok(false, `Spec definition for "${spec}" should be before "${lastSpec}"`);
+ }
+ lastSpec = spec;
+ }
+ ok(true, "Specs index is alphabetically sorted");
+}
+
+function test_specs() {
+ for (const type of Types) {
+ for (const typeName of type.types) {
+ ok(!!getType(typeName), `${typeName} spec is defined`);
+ }
+ }
+ ok(true, "Specs are all accessible");
+}
+
+function test_fronts() {
+ for (const item of Types) {
+ if (!item.front) {
+ continue;
+ }
+ for (const typeName of item.types) {
+ lazyLoadFront(typeName);
+ const type = getType(typeName);
+ ok(!!type, `Front for ${typeName} has a spec`);
+ ok(type.frontClass, `${typeName} has a front correctly defined`);
+ }
+ }
+ ok(true, "Front are all accessible");
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js
new file mode 100644
index 0000000000..6b530f0a61
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { RetVal } = protocol;
+
+// Test invalid response specs throw when generating the Actor specification.
+
+// Test top level array response
+add_task(async function () {
+ Assert.throws(() => {
+ protocol.generateActorSpec({
+ typeName: "invalidArrayResponse",
+ methods: {
+ invalidMethod: {
+ response: RetVal("array:string"),
+ },
+ },
+ });
+ }, /Arrays should be wrapped in objects/);
+
+ protocol.generateActorSpec({
+ typeName: "validArrayResponse",
+ methods: {
+ validMethod: {
+ response: {
+ someArray: RetVal("array:string"),
+ },
+ },
+ },
+ });
+ ok(true, "Arrays wrapped in object are valid response packets");
+});
+
+// Test response with several placeholders
+add_task(async function () {
+ Assert.throws(() => {
+ protocol.generateActorSpec({
+ typeName: "tooManyPlaceholdersResponse",
+ methods: {
+ invalidMethod: {
+ response: {
+ prop1: RetVal("json"),
+ prop2: RetVal("json"),
+ },
+ },
+ },
+ });
+ }, /More than one RetVal specified in response/);
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js
new file mode 100644
index 0000000000..bd887ba88a
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+const { Front } = require("resource://devtools/shared/protocol/Front.js");
+
+add_task(async function () {
+ // Front constructor expect to be provided a client object
+ const client = {};
+ const front = new Front(client);
+ ok(
+ !front.isDestroyed(),
+ "Blank front with no actor ID is not considered as destroyed"
+ );
+ front.destroy();
+ ok(front.isDestroyed(), "Front is destroyed");
+
+ const actor = new Actor(null, { typeName: "actor", methods: [] });
+ ok(
+ !actor.isDestroyed(),
+ "Blank actor with no actor ID is not considered as destroyed"
+ );
+ actor.destroy();
+ ok(actor.isDestroyed(), "Actor is destroyed");
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js
new file mode 100644
index 0000000000..cda1708520
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal, Arg } = protocol;
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+var {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+// The test implicitly relies on this.
+require("resource://devtools/client/fronts/string.js");
+
+DevToolsServer.LONG_STRING_LENGTH =
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH =
+ DevToolsServer.LONG_STRING_READ_LENGTH =
+ 5;
+
+var SHORT_STR = "abc";
+var LONG_STR = "abcdefghijklmnop";
+
+var rootActor = null;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ events: {
+ "string-event": {
+ str: Arg(0, "longstring"),
+ },
+ },
+
+ methods: {
+ shortString: {
+ response: { value: RetVal("longstring") },
+ },
+ longString: {
+ response: { value: RetVal("longstring") },
+ },
+ emitShortString: {
+ oneway: true,
+ },
+ emitLongString: {
+ oneway: true,
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ rootActor = this;
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ shortString() {
+ return new LongStringActor(this.conn, SHORT_STR);
+ }
+
+ longString() {
+ return new LongStringActor(this.conn, LONG_STR);
+ }
+
+ emitShortString() {
+ EventEmitter.emit(
+ this,
+ "string-event",
+ new LongStringActor(this.conn, SHORT_STR)
+ );
+ }
+
+ emitLongString() {
+ EventEmitter.emit(
+ this,
+ "string-event",
+ new LongStringActor(this.conn, LONG_STR)
+ );
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+function run_test() {
+ DevToolsServer.createRootActor = conn => {
+ return new RootActor(conn);
+ };
+
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ let rootFront;
+
+ let strfront = null;
+
+ const expectRootChildren = function (size) {
+ Assert.equal(rootActor.__poolMap.size, size + 1);
+ Assert.equal(rootFront.__poolMap.size, size + 1);
+ };
+
+ client.connect().then(([applicationType, traits]) => {
+ rootFront = client.mainRoot;
+
+ // Root actor has no children yet.
+ expectRootChildren(0);
+
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+ rootFront
+ .shortString()
+ .then(ret => {
+ trace.expectSend({ type: "shortString", to: "<actorid>" });
+ trace.expectReceive({ value: "abc", from: "<actorid>" });
+
+ // Should only own the one reference (itself) at this point.
+ expectRootChildren(0);
+ strfront = ret;
+ })
+ .then(() => {
+ return strfront.string();
+ })
+ .then(ret => {
+ Assert.equal(ret, SHORT_STR);
+ })
+ .then(() => {
+ return rootFront.longString();
+ })
+ .then(ret => {
+ trace.expectSend({ type: "longString", to: "<actorid>" });
+ trace.expectReceive({
+ value: {
+ type: "longString",
+ actor: "<actorid>",
+ length: 16,
+ initial: "abcde",
+ },
+ from: "<actorid>",
+ });
+
+ strfront = ret;
+ // Should own a reference to itself and an extra string now.
+ expectRootChildren(1);
+ })
+ .then(() => {
+ return strfront.string();
+ })
+ .then(ret => {
+ trace.expectSend({
+ type: "substring",
+ start: 5,
+ end: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "fghij", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 10,
+ end: 15,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "klmno", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 15,
+ end: 20,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "p", from: "<actorid>" });
+
+ Assert.equal(ret, LONG_STR);
+ })
+ .then(() => {
+ return strfront.release();
+ })
+ .then(() => {
+ trace.expectSend({ type: "release", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+
+ // That reference should be removed now.
+ expectRootChildren(0);
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ rootFront.once("string-event", str => {
+ trace.expectSend({ type: "emitShortString", to: "<actorid>" });
+ trace.expectReceive({
+ type: "string-event",
+ str: "abc",
+ from: "<actorid>",
+ });
+
+ Assert.ok(!!str);
+ strfront = str;
+ // Shouldn't generate any new references
+ expectRootChildren(0);
+ // will generate no packets.
+ strfront.string().then(value => {
+ resolve(value);
+ });
+ });
+ rootFront.emitShortString();
+ });
+ })
+ .then(value => {
+ Assert.equal(value, SHORT_STR);
+ })
+ .then(() => {
+ // Will generate no packets
+ return strfront.release();
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ rootFront.once("string-event", str => {
+ trace.expectSend({ type: "emitLongString", to: "<actorid>" });
+ trace.expectReceive({
+ type: "string-event",
+ str: {
+ type: "longString",
+ actor: "<actorid>",
+ length: 16,
+ initial: "abcde",
+ },
+ from: "<actorid>",
+ });
+
+ Assert.ok(!!str);
+ // Should generate one new reference
+ expectRootChildren(1);
+ strfront = str;
+ strfront.string().then(value => {
+ trace.expectSend({
+ type: "substring",
+ start: 5,
+ end: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "fghij", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 10,
+ end: 15,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "klmno", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 15,
+ end: 20,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "p", from: "<actorid>" });
+
+ resolve(value);
+ });
+ });
+ rootFront.emitLongString();
+ });
+ })
+ .then(value => {
+ Assert.equal(value, LONG_STR);
+ })
+ .then(() => {
+ return strfront.release();
+ })
+ .then(() => {
+ trace.expectSend({ type: "release", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+ expectRootChildren(0);
+ })
+ .then(() => {
+ client.close().then(() => {
+ do_test_finished();
+ });
+ })
+ .catch(err => {
+ do_report_unexpected_exception(err, "Failure executing test");
+ });
+ });
+ do_test_pending();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js
new file mode 100644
index 0000000000..523d147f6c
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js
@@ -0,0 +1,316 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { Arg, Option, RetVal } = protocol;
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ events: {
+ oneway: { a: Arg(0) },
+ falsyOptions: {
+ zero: Option(0),
+ farce: Option(0),
+ },
+ },
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ promiseReturn: {
+ response: { value: RetVal("number") },
+ },
+ simpleArgs: {
+ request: {
+ firstArg: Arg(0),
+ secondArg: Arg(1),
+ },
+ response: RetVal(),
+ },
+ optionArgs: {
+ request: {
+ option1: Option(0),
+ option2: Option(0),
+ },
+ response: RetVal(),
+ },
+ optionalArgs: {
+ request: {
+ a: Arg(0),
+ b: Arg(1, "nullable:number"),
+ },
+ response: {
+ value: RetVal("number"),
+ },
+ },
+ arrayArgs: {
+ request: {
+ a: Arg(0, "array:number"),
+ },
+ response: {
+ arrayReturn: RetVal("array:number"),
+ },
+ },
+ nestedArrayArgs: {
+ request: { a: Arg(0, "array:array:number") },
+ response: { value: RetVal("array:array:number") },
+ },
+ renamedEcho: {
+ request: {
+ type: "echo",
+ a: Arg(0),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ testOneWay: {
+ request: { a: Arg(0) },
+ oneway: true,
+ },
+ emitFalsyOptions: {
+ oneway: true,
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return 1;
+ }
+
+ promiseReturn() {
+ return Promise.resolve(1);
+ }
+
+ simpleArgs(a, b) {
+ return { firstResponse: a + 1, secondResponse: b + 1 };
+ }
+
+ optionArgs(options) {
+ return { option1: options.option1, option2: options.option2 };
+ }
+
+ optionalArgs(a, b = 200) {
+ return b;
+ }
+
+ arrayArgs(a) {
+ return a;
+ }
+
+ nestedArrayArgs(a) {
+ return a;
+ }
+
+ /**
+ * Test that the 'type' part of the request packet works
+ * correctly when the type isn't the same as the method name
+ */
+ renamedEcho(a) {
+ if (this.conn.currentPacket.type != "echo") {
+ return "goodbye";
+ }
+ return a;
+ }
+
+ testOneWay(a) {
+ // Emit to show that we got this message, because there won't be a response.
+ EventEmitter.emit(this, "oneway", a);
+ }
+
+ emitFalsyOptions() {
+ EventEmitter.emit(this, "falsyOptions", { zero: 0, farce: false });
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => {
+ return new RootActor(conn);
+ };
+ DevToolsServer.init();
+
+ protocol.types.getType("array:array:array:number");
+ protocol.types.getType("array:array:array:number");
+
+ Assert.throws(
+ () => protocol.types.getType("unknown"),
+ /Unknown type:/,
+ "Should throw for unknown type"
+ );
+ Assert.throws(
+ () => protocol.types.getType("array:unknown"),
+ /Unknown type:/,
+ "Should throw for unknown type"
+ );
+ Assert.throws(
+ () => protocol.types.getType("unknown:number"),
+ /Unknown collection type:/,
+ "Should throw for unknown collection type"
+ );
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+
+ const [applicationType] = await client.connect();
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+
+ const rootFront = client.mainRoot;
+
+ let ret = await rootFront.simpleReturn();
+ trace.expectSend({ type: "simpleReturn", to: "<actorid>" });
+ trace.expectReceive({ value: 1, from: "<actorid>" });
+ Assert.equal(ret, 1);
+
+ ret = await rootFront.promiseReturn();
+ trace.expectSend({ type: "promiseReturn", to: "<actorid>" });
+ trace.expectReceive({ value: 1, from: "<actorid>" });
+ Assert.equal(ret, 1);
+
+ Assert.throws(
+ () => rootFront.simpleArgs(5),
+ /undefined passed where a value is required/,
+ "Should throw if simpleArgs is missing an argument."
+ );
+
+ ret = await rootFront.simpleArgs(5, 10);
+ trace.expectSend({
+ type: "simpleArgs",
+ firstArg: 5,
+ secondArg: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ firstResponse: 6,
+ secondResponse: 11,
+ from: "<actorid>",
+ });
+ Assert.equal(ret.firstResponse, 6);
+ Assert.equal(ret.secondResponse, 11);
+
+ ret = await rootFront.optionArgs({
+ option1: 5,
+ option2: 10,
+ });
+ trace.expectSend({
+ type: "optionArgs",
+ option1: 5,
+ option2: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ option1: 5, option2: 10, from: "<actorid>" });
+ Assert.equal(ret.option1, 5);
+ Assert.equal(ret.option2, 10);
+
+ ret = await rootFront.optionArgs({});
+ trace.expectSend({ type: "optionArgs", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+ Assert.ok(typeof ret.option1 === "undefined");
+ Assert.ok(typeof ret.option2 === "undefined");
+
+ // Explicitly call an optional argument...
+ ret = await rootFront.optionalArgs(5, 10);
+ trace.expectSend({
+ type: "optionalArgs",
+ a: 5,
+ b: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ value: 10, from: "<actorid>" });
+ Assert.equal(ret, 10);
+
+ // Now don't pass the optional argument, expect the default.
+ ret = await rootFront.optionalArgs(5);
+ trace.expectSend({ type: "optionalArgs", a: 5, to: "<actorid>" });
+ trace.expectReceive({ value: 200, from: "<actorid>" });
+ Assert.equal(ret, 200);
+
+ ret = await rootFront.arrayArgs([0, 1, 2, 3, 4, 5]);
+ trace.expectSend({
+ type: "arrayArgs",
+ a: [0, 1, 2, 3, 4, 5],
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ arrayReturn: [0, 1, 2, 3, 4, 5],
+ from: "<actorid>",
+ });
+ Assert.equal(ret[0], 0);
+ Assert.equal(ret[5], 5);
+
+ ret = await rootFront.arrayArgs([[5]]);
+ trace.expectSend({ type: "arrayArgs", a: [[5]], to: "<actorid>" });
+ trace.expectReceive({ arrayReturn: [[5]], from: "<actorid>" });
+ Assert.equal(ret[0][0], 5);
+
+ const str = await rootFront.renamedEcho("hello");
+ trace.expectSend({ type: "echo", a: "hello", to: "<actorid>" });
+ trace.expectReceive({ value: "hello", from: "<actorid>" });
+ Assert.equal(str, "hello");
+
+ const onOneWay = rootFront.once("oneway");
+ Assert.ok(typeof rootFront.testOneWay("hello") === "undefined");
+ const response = await onOneWay;
+ trace.expectSend({ type: "testOneWay", a: "hello", to: "<actorid>" });
+ trace.expectReceive({
+ type: "oneway",
+ a: "hello",
+ from: "<actorid>",
+ });
+ Assert.equal(response, "hello");
+
+ const onFalsyOptions = rootFront.once("falsyOptions");
+ rootFront.emitFalsyOptions();
+ const res = await onFalsyOptions;
+ trace.expectSend({ type: "emitFalsyOptions", to: "<actorid>" });
+ trace.expectReceive({
+ type: "falsyOptions",
+ farce: false,
+ zero: 0,
+ from: "<actorid>",
+ });
+
+ Assert.ok(res.zero === 0);
+ Assert.ok(res.farce === false);
+
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js
new file mode 100644
index 0000000000..faf8402ea6
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Client request stacks should span the entire process from before making the
+ * request to handling the reply from the server. The server frames are not
+ * included, nor can they be in most cases, since the server can be a remote
+ * device.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal } = protocol;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return this.sequence++;
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+function run_test() {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ let rootFront;
+
+ client.connect().then(function onConnect() {
+ rootFront = client.mainRoot;
+
+ rootFront
+ .simpleReturn()
+ .then(
+ () => {
+ let stack = Components.stack;
+ while (stack) {
+ info(stack.name);
+ if (stack.name.includes("onConnect")) {
+ // Reached back to outer function before request
+ ok(true, "Complete stack");
+ return;
+ }
+ stack = stack.asyncCaller || stack.caller;
+ }
+ ok(false, "Incomplete stack");
+ },
+ () => {
+ ok(false, "Request failed unexpectedly");
+ }
+ )
+ .then(() => {
+ client.close().then(() => {
+ do_test_finished();
+ });
+ });
+ });
+
+ do_test_pending();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js
new file mode 100644
index 0000000000..4a62c5e073
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const { types } = require("resource://devtools/shared/protocol.js");
+
+function run_test() {
+ types.addActorType("myActor1");
+ types.addActorType("myActor2");
+ types.addActorType("myActor3");
+
+ types.addPolymorphicType("ptype1", ["myActor1", "myActor2"]);
+ const ptype1 = types.getType("ptype1");
+ Assert.equal(ptype1.name, "ptype1");
+ Assert.equal(ptype1.category, "polymorphic");
+
+ types.addPolymorphicType("ptype2", ["myActor1", "myActor2", "myActor3"]);
+ const ptype2 = types.getType("ptype2");
+ Assert.equal(ptype2.name, "ptype2");
+ Assert.equal(ptype2.category, "polymorphic");
+
+ // Polymorphic types only accept actor types
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "myActor4"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Unknown type: myActor4");
+ }
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "string"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'myActor1,string', the type 'string' isn't an actor"
+ );
+ }
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "boolean"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'myActor1,boolean', the type 'boolean' isn't an actor"
+ );
+ }
+
+ // Polymorphic types are not compatible with array or nullables
+ try {
+ types.addPolymorphicType("ptype", ["array:myActor1", "myActor2"]);
+ Assert.ok(false, "addType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'array:myActor1,myActor2', the type 'array:myActor1' isn't an actor"
+ );
+ }
+ try {
+ types.addPolymorphicType("ptype", ["nullable:myActor1", "myActor2"]);
+ Assert.ok(false, "addType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'nullable:myActor1,myActor2', the type 'nullable:myActor1' isn't an actor"
+ );
+ }
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js
new file mode 100644
index 0000000000..060a1743b1
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const { types } = require("resource://devtools/shared/protocol.js");
+
+function run_test() {
+ types.addType("test", {
+ read: v => "successful read: " + v,
+ write: v => "successful write: " + v,
+ });
+
+ // Verify the type registered correctly.
+
+ const type = types.getType("test");
+ const arrayType = types.getType("array:test");
+ Assert.equal(type.read("foo"), "successful read: foo");
+ Assert.equal(arrayType.read(["foo"])[0], "successful read: foo");
+
+ types.removeType("test");
+
+ Assert.equal(type.name, "DEFUNCT:test");
+ try {
+ types.getType("test");
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Unknown type: test");
+ }
+
+ try {
+ type.read("foo");
+ Assert.ok(false, "type.read should have thrown an exception.");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Using defunct type: test");
+ }
+
+ try {
+ arrayType.read(["foo"]);
+ Assert.ok(false, "array:test.read should have thrown an exception.");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Using defunct type: test");
+ }
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js
new file mode 100644
index 0000000000..16f98f176b
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test Front.watchFronts method.
+ */
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { RetVal } = protocol;
+
+const childSpec = protocol.generateActorSpec({
+ typeName: "childActor",
+
+ methods: {
+ release: {
+ release: true,
+ },
+ },
+});
+
+class ChildActor extends protocol.Actor {
+ constructor(conn, id) {
+ super(conn, childSpec);
+ this.childID = id;
+ }
+
+ release() {}
+
+ form() {
+ return {
+ actor: this.actorID,
+ childID: this.childID,
+ foo: "bar",
+ };
+ }
+}
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ createChild: {
+ request: {},
+ response: { actor: RetVal("childActor") },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ this.actorID = "root";
+
+ // Root actor owns itself.
+ this.manage(this);
+
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ createChild() {
+ return new ChildActor(this.conn, this.sequence++);
+ }
+}
+
+class ChildFront extends protocol.FrontClassWithSpec(childSpec) {
+ form(form) {
+ this.childID = form.childID;
+ this.foo = form.foo;
+ }
+}
+protocol.registerFront(ChildFront);
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function run_test() {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const fronts = [];
+ const listener = front => {
+ equal(
+ front.foo,
+ "bar",
+ "Front's form is set before watchFronts listeners are called"
+ );
+ fronts.push(front);
+ };
+ rootFront.watchFronts("childActor", listener);
+
+ const firstChild = await rootFront.createChild();
+ ok(
+ firstChild instanceof ChildFront,
+ "createChild returns a ChildFront instance"
+ );
+ equal(firstChild.childID, 0, "First child has ID=0");
+
+ equal(
+ fronts.length,
+ 1,
+ "watchFronts fires the callback, even if the front is created in the future"
+ );
+ equal(
+ fronts[0],
+ firstChild,
+ "watchFronts fires the callback with the right front instance"
+ );
+
+ const watchFrontsAfter = await new Promise(resolve => {
+ rootFront.watchFronts("childActor", resolve);
+ });
+ equal(
+ watchFrontsAfter,
+ firstChild,
+ "watchFronts fires the callback, even if the front is already created, " +
+ " with the same front instance"
+ );
+
+ equal(
+ fronts.length,
+ 1,
+ "There is still only one front reported from the first listener"
+ );
+
+ const secondChild = await rootFront.createChild();
+
+ equal(
+ fronts.length,
+ 2,
+ "After a second call to createChild, two fronts are reported"
+ );
+ equal(fronts[1], secondChild, "And the new front is the right instance");
+
+ // Test unregistering a front listener
+ rootFront.unwatchFronts("childActor", listener);
+
+ const thirdChild = await rootFront.createChild();
+ equal(
+ fronts.length,
+ 2,
+ "After calling unwatchFronts, the listener is no longer called"
+ );
+
+ // Test front destruction
+ const destroyed = [];
+ rootFront.watchFronts("childActor", null, front => {
+ destroyed.push(front);
+ });
+ await thirdChild.release();
+ equal(
+ destroyed.length,
+ 1,
+ "After the destruction of the front, one destruction is reported"
+ );
+ equal(destroyed[0], thirdChild, "And the destroyed front is the right one");
+
+ trace.close();
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/xpcshell.toml b/devtools/shared/protocol/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..f316485cb3
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,30 @@
+[DEFAULT]
+tags = "devtools"
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = ""
+
+["test_protocol_abort.js"]
+
+["test_protocol_async.js"]
+
+["test_protocol_children.js"]
+
+["test_protocol_index.js"]
+
+["test_protocol_invalid_response.js"]
+
+["test_protocol_lifecycle.js"]
+
+["test_protocol_longstring.js"]
+
+["test_protocol_simple.js"]
+
+["test_protocol_stack.js"]
+
+["test_protocol_types.js"]
+
+["test_protocol_unregister.js"]
+
+["test_protocol_watchFronts.js"]
diff --git a/devtools/shared/protocol/types.js b/devtools/shared/protocol/types.js
new file mode 100644
index 0000000000..41764fbd79
--- /dev/null
+++ b/devtools/shared/protocol/types.js
@@ -0,0 +1,587 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+var {
+ lazyLoadSpec,
+ lazyLoadFront,
+} = require("resource://devtools/shared/specs/index.js");
+
+/**
+ * Types: named marshallers/demarshallers.
+ *
+ * Types provide a 'write' function that takes a js representation and
+ * returns a protocol representation, and a "read" function that
+ * takes a protocol representation and returns a js representation.
+ *
+ * The read and write methods are also passed a context object that
+ * represent the actor or front requesting the translation.
+ *
+ * Types are referred to with a typestring. Basic types are
+ * registered by name using addType, and more complex types can
+ * be generated by adding detail to the type name.
+ */
+
+var types = Object.create(null);
+exports.types = types;
+
+var registeredTypes = (types.registeredTypes = new Map());
+
+exports.registeredTypes = registeredTypes;
+
+/**
+ * Return the type object associated with a given typestring.
+ * If passed a type object, it will be returned unchanged.
+ *
+ * Types can be registered with addType, or can be created on
+ * the fly with typestrings. Examples:
+ *
+ * boolean
+ * threadActor
+ * threadActor#detail
+ * array:threadActor
+ * array:array:threadActor#detail
+ *
+ * @param [typestring|type] type
+ * Either a typestring naming a type or a type object.
+ *
+ * @returns a type object.
+ */
+types.getType = function (type) {
+ if (!type) {
+ return types.Primitive;
+ }
+
+ if (typeof type !== "string") {
+ return type;
+ }
+
+ // If already registered, we're done here.
+ let reg = registeredTypes.get(type);
+ if (reg) {
+ return reg;
+ }
+
+ // Try to lazy load the spec, if not already loaded.
+ if (lazyLoadSpec(type)) {
+ // If a spec module was lazy loaded, it will synchronously call
+ // generateActorSpec, and set the type in `registeredTypes`.
+ reg = registeredTypes.get(type);
+ if (reg) {
+ return reg;
+ }
+ }
+
+ // New type, see if it's a collection type:
+ const sep = type.indexOf(":");
+ if (sep >= 0) {
+ const collection = type.substring(0, sep);
+ const subtype = types.getType(type.substring(sep + 1));
+
+ if (collection === "array") {
+ return types.addArrayType(subtype);
+ } else if (collection === "nullable") {
+ return types.addNullableType(subtype);
+ }
+
+ throw Error("Unknown collection type: " + collection);
+ }
+
+ // Not a collection, might be actor detail
+ const pieces = type.split("#", 2);
+ if (pieces.length > 1) {
+ if (pieces[1] != "actorid") {
+ throw new Error(
+ "Unsupported detail, only support 'actorid', got: " + pieces[1]
+ );
+ }
+ return types.addActorDetail(type, pieces[0], pieces[1]);
+ }
+
+ throw Error("Unknown type: " + type);
+};
+
+/**
+ * Don't allow undefined when writing primitive types to packets. If
+ * you want to allow undefined, use a nullable type.
+ */
+function identityWrite(v) {
+ if (v === undefined) {
+ throw Error("undefined passed where a value is required");
+ }
+ // This has to handle iterator->array conversion because arrays of
+ // primitive types pass through here.
+ if (v && typeof v.next === "function") {
+ return [...v];
+ }
+ return v;
+}
+
+/**
+ * Add a type to the type system.
+ *
+ * When registering a type, you can provide `read` and `write` methods.
+ *
+ * The `read` method will be passed a JS object value from the JSON
+ * packet and must return a native representation. The `write` method will
+ * be passed a native representation and should provide a JSONable value.
+ *
+ * These methods will both be passed a context. The context is the object
+ * performing or servicing the request - on the server side it will be
+ * an Actor, on the client side it will be a Front.
+ *
+ * @param typestring name
+ * Name to register
+ * @param object typeObject
+ * An object whose properties will be stored in the type, including
+ * the `read` and `write` methods.
+ * @param object options
+ * Can specify `thawed` to prevent the type from being frozen.
+ *
+ * @returns a type object that can be used in protocol definitions.
+ */
+types.addType = function (name, typeObject = {}, options = {}) {
+ if (registeredTypes.has(name)) {
+ throw Error("Type '" + name + "' already exists.");
+ }
+
+ const type = Object.assign(
+ {
+ toString() {
+ return "[protocol type:" + name + "]";
+ },
+ name,
+ primitive: !(typeObject.read || typeObject.write),
+ read: identityWrite,
+ write: identityWrite,
+ },
+ typeObject
+ );
+
+ registeredTypes.set(name, type);
+
+ return type;
+};
+
+/**
+ * Remove a type previously registered with the system.
+ * Primarily useful for types registered by addons.
+ */
+types.removeType = function (name) {
+ // This type may still be referenced by other types, make sure
+ // those references don't work.
+ const type = registeredTypes.get(name);
+
+ type.name = "DEFUNCT:" + name;
+ type.category = "defunct";
+ type.primitive = false;
+ type.read = type.write = function () {
+ throw new Error("Using defunct type: " + name);
+ };
+
+ registeredTypes.delete(name);
+};
+
+/**
+ * Add an array type to the type system.
+ *
+ * getType() will call this function if provided an "array:<type>"
+ * typestring.
+ *
+ * @param type subtype
+ * The subtype to be held by the array.
+ */
+types.addArrayType = function (subtype) {
+ subtype = types.getType(subtype);
+
+ const name = "array:" + subtype.name;
+
+ // Arrays of primitive types are primitive types themselves.
+ if (subtype.primitive) {
+ return types.addType(name);
+ }
+ return types.addType(name, {
+ category: "array",
+ read: (v, ctx) => {
+ if (v && typeof v.next === "function") {
+ v = [...v];
+ }
+ return v.map(i => subtype.read(i, ctx));
+ },
+ write: (v, ctx) => {
+ if (v && typeof v.next === "function") {
+ v = [...v];
+ }
+ return v.map(i => subtype.write(i, ctx));
+ },
+ });
+};
+
+/**
+ * Add a dict type to the type system. This allows you to serialize
+ * a JS object that contains non-primitive subtypes.
+ *
+ * Properties of the value that aren't included in the specializations
+ * will be serialized as primitive values.
+ *
+ * @param object specializations
+ * A dict of property names => type
+ */
+types.addDictType = function (name, specializations) {
+ const specTypes = {};
+ for (const prop in specializations) {
+ try {
+ specTypes[prop] = types.getType(specializations[prop]);
+ } catch (e) {
+ // Types may not be defined yet. Sometimes, we define the type *after* using it, but
+ // also, we have cyclic definitions on types. So lazily load them when they are not
+ // immediately available.
+ loader.lazyGetter(specTypes, prop, () => {
+ return types.getType(specializations[prop]);
+ });
+ }
+ }
+ return types.addType(name, {
+ category: "dict",
+ specializations,
+ read: (v, ctx) => {
+ const ret = {};
+ for (const prop in v) {
+ if (prop in specTypes) {
+ ret[prop] = specTypes[prop].read(v[prop], ctx);
+ } else {
+ ret[prop] = v[prop];
+ }
+ }
+ return ret;
+ },
+
+ write: (v, ctx) => {
+ const ret = {};
+ for (const prop in v) {
+ if (prop in specTypes) {
+ ret[prop] = specTypes[prop].write(v[prop], ctx);
+ } else {
+ ret[prop] = v[prop];
+ }
+ }
+ return ret;
+ },
+ });
+};
+
+/**
+ * Register an actor type with the type system.
+ *
+ * Types are marshalled differently when communicating server->client
+ * than they are when communicating client->server. The server needs
+ * to provide useful information to the client, so uses the actor's
+ * `form` method to get a json representation of the actor. When
+ * making a request from the client we only need the actor ID string.
+ *
+ * This function can be called before the associated actor has been
+ * constructed, but the read and write methods won't work until
+ * the associated addActorImpl or addActorFront methods have been
+ * called during actor/front construction.
+ *
+ * @param string name
+ * The typestring to register.
+ */
+types.addActorType = function (name) {
+ // We call addActorType from:
+ // FrontClassWithSpec when registering front synchronously,
+ // generateActorSpec when defining specs,
+ // specs modules to register actor type early to use them in other types
+ if (registeredTypes.has(name)) {
+ return registeredTypes.get(name);
+ }
+ const type = types.addType(name, {
+ _actor: true,
+ category: "actor",
+ read: (v, ctx, detail) => {
+ // If we're reading a request on the server side, just
+ // find the actor registered with this actorID.
+ if (ctx instanceof Actor) {
+ return ctx.conn.getActor(v);
+ }
+
+ // Reading a response on the client side, check for an
+ // existing front on the connection, and create the front
+ // if it isn't found.
+ const actorID = typeof v === "string" ? v : v.actor;
+ // `ctx.conn` is a DevToolsClient
+ let front = ctx.conn.getFrontByID(actorID);
+
+ // When the type `${name}#actorid` is used, `v` is a string refering to the
+ // actor ID. We cannot read form information in this case and the actorID was
+ // already set when creating the front, so no need to do anything.
+ let form = null;
+ if (detail != "actorid") {
+ form = identityWrite(v);
+ }
+
+ if (!front) {
+ // If front isn't instantiated yet, create one.
+ // Try lazy loading front if not already loaded.
+ // The front module will synchronously call `FrontClassWithSpec` and
+ // augment `type` with the `frontClass` attribute.
+ if (!type.frontClass) {
+ lazyLoadFront(name);
+ }
+
+ const parentFront = ctx.marshallPool();
+ const targetFront = parentFront.isTargetFront
+ ? parentFront
+ : parentFront.targetFront;
+
+ // Use intermediate Class variable to please eslint requiring
+ // a capital letter for all constructors.
+ const Class = type.frontClass;
+ front = new Class(ctx.conn, targetFront, parentFront);
+ front.actorID = actorID;
+
+ parentFront.manage(front, form, ctx);
+ } else if (form) {
+ front.form(form, ctx);
+ }
+
+ return front;
+ },
+ write: (v, ctx, detail) => {
+ // If returning a response from the server side, make sure
+ // the actor is added to a parent object and return its form.
+ if (v instanceof Actor) {
+ if (v.isDestroyed()) {
+ throw new Error(
+ `Attempted to write a response containing a destroyed actor`
+ );
+ }
+ if (!v.actorID) {
+ ctx.marshallPool().manage(v);
+ }
+ if (detail == "actorid") {
+ return v.actorID;
+ }
+ return identityWrite(v.form(detail));
+ }
+
+ // Writing a request from the client side, just send the actor id.
+ return v.actorID;
+ },
+ });
+ return type;
+};
+
+types.addPolymorphicType = function (name, subtypes) {
+ // Assert that all subtypes are actors, as the marshalling implementation depends on that.
+ for (const subTypeName of subtypes) {
+ const subtype = types.getType(subTypeName);
+ if (subtype.category != "actor") {
+ throw new Error(
+ `In polymorphic type '${subtypes.join(
+ ","
+ )}', the type '${subTypeName}' isn't an actor`
+ );
+ }
+ }
+
+ return types.addType(name, {
+ category: "polymorphic",
+ read: (value, ctx) => {
+ // `value` is either a string which is an Actor ID or a form object
+ // where `actor` is an actor ID
+ const actorID = typeof value === "string" ? value : value.actor;
+ if (!actorID) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
+ );
+ }
+
+ // Extract the typeName out of the actor ID, which should be composed like this
+ // ${DevToolsServerConnectionPrefix}.${typeName}${Number}
+ const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1];
+ if (!subtypes.includes(typeName)) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
+ );
+ }
+
+ const subtype = types.getType(typeName);
+ return subtype.read(value, ctx);
+ },
+ write: (value, ctx) => {
+ if (!value) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
+ );
+ }
+ // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
+ const typeName = value.typeName;
+ if (!typeName) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
+ );
+ }
+
+ if (!subtypes.includes(typeName)) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
+ );
+ }
+
+ const subtype = types.getType(typeName);
+ return subtype.write(value, ctx);
+ },
+ });
+};
+types.addNullableType = function (subtype) {
+ subtype = types.getType(subtype);
+ return types.addType("nullable:" + subtype.name, {
+ category: "nullable",
+ read: (value, ctx) => {
+ if (value == null) {
+ return value;
+ }
+ return subtype.read(value, ctx);
+ },
+ write: (value, ctx) => {
+ if (value == null) {
+ return value;
+ }
+ return subtype.write(value, ctx);
+ },
+ });
+};
+
+/**
+ * Register an actor detail type. This is just like an actor type, but
+ * will pass a detail hint to the actor's form method during serialization/
+ * deserialization.
+ *
+ * This is called by getType() when passed an 'actorType#detail' string.
+ *
+ * @param string name
+ * The typestring to register this type as.
+ * @param type actorType
+ * The actor type you'll be detailing.
+ * @param string detail
+ * The detail to pass.
+ */
+types.addActorDetail = function (name, actorType, detail) {
+ actorType = types.getType(actorType);
+ if (!actorType._actor) {
+ throw Error(
+ `Details only apply to actor types, tried to add detail '${detail}' ` +
+ `to ${actorType.name}`
+ );
+ }
+ return types.addType(name, {
+ _actor: true,
+ category: "detail",
+ read: (v, ctx) => actorType.read(v, ctx, detail),
+ write: (v, ctx) => actorType.write(v, ctx, detail),
+ });
+};
+
+// Add a few named primitive types.
+types.Primitive = types.addType("primitive");
+types.String = types.addType("string");
+types.Number = types.addType("number");
+types.Boolean = types.addType("boolean");
+types.JSON = types.addType("json");
+
+exports.registerFront = function (cls) {
+ const { typeName } = cls.prototype;
+ if (!registeredTypes.has(typeName)) {
+ types.addActorType(typeName);
+ }
+ registeredTypes.get(typeName).frontClass = cls;
+};
+
+/**
+ * Instantiate a front of the given type.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param string typeName
+ * The type name of the front to instantiate. This is defined in its specifiation.
+ * @returns Front
+ * The created front.
+ */
+function createFront(client, typeName, target = null) {
+ const type = types.getType(typeName);
+ if (!type) {
+ throw new Error(`No spec for front type '${typeName}'.`);
+ } else if (!type.frontClass) {
+ lazyLoadFront(typeName);
+ }
+
+ // Use intermediate Class variable to please eslint requiring
+ // a capital letter for all constructors.
+ const Class = type.frontClass;
+ return new Class(client, target, target);
+}
+
+/**
+ * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
+ * front of the given type by picking its actor ID out of either the target or root
+ * front's form.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param string typeName
+ * The type name of the front to instantiate. This is defined in its specifiation.
+ * @param json form
+ * If we want to instantiate a global actor's front, this is the root front's form,
+ * otherwise we are instantiating a target-scoped front from the target front's form.
+ * @param [Target|null] target
+ * If we are instantiating a target-scoped front, this is a reference to the front's
+ * Target instance, otherwise this is null.
+ */
+async function getFront(client, typeName, form, target = null) {
+ const front = createFront(client, typeName, target);
+ const { formAttributeName } = front;
+ if (!formAttributeName) {
+ throw new Error(`Can't find the form attribute name for ${typeName}`);
+ }
+ // Retrieve the actor ID from root or target actor's form
+ front.actorID = form[formAttributeName];
+ if (!front.actorID) {
+ throw new Error(
+ `Can't find the actor ID for ${typeName} from root or target` +
+ ` actor's form.`
+ );
+ }
+
+ if (!target) {
+ await front.manage(front);
+ } else {
+ await target.manage(front);
+ }
+
+ return front;
+}
+exports.getFront = getFront;
+
+/**
+ * Create a RootFront.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param Object packet
+ * @returns RootFront
+ */
+function createRootFront(client, packet) {
+ const rootFront = createFront(client, "root");
+ rootFront.form(packet);
+
+ // Root Front is a special case, managing itself as it doesn't have any parent.
+ // It will register itself to DevToolsClient as a Pool via Front._poolMap.
+ rootFront.manage(rootFront);
+
+ return rootFront;
+}
+exports.createRootFront = createRootFront;
diff --git a/devtools/shared/protocol/utils.js b/devtools/shared/protocol/utils.js
new file mode 100644
index 0000000000..3433c2f446
--- /dev/null
+++ b/devtools/shared/protocol/utils.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";
+
+/**
+ * Find Placeholders in the template and save them along with their
+ * paths.
+ */
+function findPlaceholders(template, constructor, path = [], placeholders = []) {
+ if (!template || typeof template != "object") {
+ return placeholders;
+ }
+
+ if (template instanceof constructor) {
+ placeholders.push({ placeholder: template, path: [...path] });
+ return placeholders;
+ }
+
+ for (const name in template) {
+ path.push(name);
+ findPlaceholders(template[name], constructor, path, placeholders);
+ path.pop();
+ }
+
+ return placeholders;
+}
+
+exports.findPlaceholders = findPlaceholders;
+
+/**
+ * Get the value at a given path, or undefined if not found.
+ */
+function getPath(obj, path) {
+ for (const name of path) {
+ if (!(name in obj)) {
+ return undefined;
+ }
+ obj = obj[name];
+ }
+ return obj;
+}
+exports.getPath = getPath;
diff --git a/devtools/shared/qrcode/decoder/LICENSE b/devtools/shared/qrcode/decoder/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/devtools/shared/qrcode/decoder/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/devtools/shared/qrcode/decoder/index.js b/devtools/shared/qrcode/decoder/index.js
new file mode 100644
index 0000000000..f99a947cf4
--- /dev/null
+++ b/devtools/shared/qrcode/decoder/index.js
@@ -0,0 +1,2374 @@
+/*
+ Ported to JavaScript by Lazar Laszlo 2011
+
+ lazarsoft@gmail.com, www.lazarsoft.info
+*/
+/*
+*
+* Copyright 2007 ZXing authors
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+var imgU8 = null;
+
+var imgU32 = null;
+
+var imgWidth = 0;
+
+var imgHeight = 0;
+
+var maxImgSize = 1024 * 1024;
+
+var sizeOfDataLengthInfo = [ [ 10, 9, 8, 8 ], [ 12, 11, 16, 10 ], [ 14, 13, 16, 12 ] ];
+
+var GridSampler = {};
+
+GridSampler.checkAndNudgePoints = function(image, points) {
+ let width = imgWidth;
+ let height = imgHeight;
+ let nudged = true;
+ for (let offset = 0; offset < points.length && nudged; offset += 2) {
+ let x = Math.floor(points[offset]);
+ let y = Math.floor(points[offset + 1]);
+ if (x < -1 || x > width || y < -1 || y > height) {
+ throw "Error.checkAndNudgePoints ";
+ }
+ nudged = false;
+ if (x == -1) {
+ points[offset] = 0;
+ nudged = true;
+ } else if (x == width) {
+ points[offset] = width - 1;
+ nudged = true;
+ }
+ if (y == -1) {
+ points[offset + 1] = 0;
+ nudged = true;
+ } else if (y == height) {
+ points[offset + 1] = height - 1;
+ nudged = true;
+ }
+ }
+ nudged = true;
+ for (let offset = points.length - 2; offset >= 0 && nudged; offset -= 2) {
+ let x = Math.floor(points[offset]);
+ let y = Math.floor(points[offset + 1]);
+ if (x < -1 || x > width || y < -1 || y > height) {
+ throw "Error.checkAndNudgePoints ";
+ }
+ nudged = false;
+ if (x == -1) {
+ points[offset] = 0;
+ nudged = true;
+ } else if (x == width) {
+ points[offset] = width - 1;
+ nudged = true;
+ }
+ if (y == -1) {
+ points[offset + 1] = 0;
+ nudged = true;
+ } else if (y == height) {
+ points[offset + 1] = height - 1;
+ nudged = true;
+ }
+ }
+};
+
+GridSampler.sampleGrid3 = function(image, dimension, transform) {
+ let bits = new BitMatrix(dimension);
+ let points = new Array(dimension << 1);
+ for (let y = 0; y < dimension; y++) {
+ let max = points.length;
+ let iValue = y + 0.5;
+ for (let x = 0; x < max; x += 2) {
+ points[x] = (x >> 1) + 0.5;
+ points[x + 1] = iValue;
+ }
+ transform.transformPoints1(points);
+ GridSampler.checkAndNudgePoints(image, points);
+ try {
+ for (let x = 0; x < max; x += 2) {
+ let xpoint = Math.floor(points[x]) * 4 + Math.floor(points[x + 1]) * imgWidth * 4;
+ let bit = image[Math.floor(points[x]) + imgWidth * Math.floor(points[x + 1])];
+ imgU8[xpoint] = bit ? 255 : 0;
+ imgU8[xpoint + 1] = bit ? 255 : 0;
+ imgU8[xpoint + 2] = 0;
+ imgU8[xpoint + 3] = 255;
+ if (bit) bits.set_Renamed(x >> 1, y);
+ }
+ } catch (aioobe) {
+ throw "Error.checkAndNudgePoints";
+ }
+ }
+ return bits;
+};
+
+GridSampler.sampleGridx = function(image, dimension, p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY) {
+ let transform = PerspectiveTransform.quadrilateralToQuadrilateral(p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY);
+ return GridSampler.sampleGrid3(image, dimension, transform);
+};
+
+function ECB(count, dataCodewords) {
+ this.count = count;
+ this.dataCodewords = dataCodewords;
+ this.__defineGetter__("Count", function() {
+ return this.count;
+ });
+ this.__defineGetter__("DataCodewords", function() {
+ return this.dataCodewords;
+ });
+}
+
+function ECBlocks(ecCodewordsPerBlock, ecBlocks1, ecBlocks2) {
+ this.ecCodewordsPerBlock = ecCodewordsPerBlock;
+ if (ecBlocks2) this.ecBlocks = new Array(ecBlocks1, ecBlocks2); else this.ecBlocks = new Array(ecBlocks1);
+ this.__defineGetter__("ECCodewordsPerBlock", function() {
+ return this.ecCodewordsPerBlock;
+ });
+ this.__defineGetter__("TotalECCodewords", function() {
+ return this.ecCodewordsPerBlock * this.NumBlocks;
+ });
+ this.__defineGetter__("NumBlocks", function() {
+ let total = 0;
+ for (let i = 0; i < this.ecBlocks.length; i++) {
+ total += this.ecBlocks[i].length;
+ }
+ return total;
+ });
+ this.getECBlocks = function() {
+ return this.ecBlocks;
+ };
+}
+
+function Version(versionNumber, alignmentPatternCenters, ecBlocks1, ecBlocks2, ecBlocks3, ecBlocks4) {
+ this.versionNumber = versionNumber;
+ this.alignmentPatternCenters = alignmentPatternCenters;
+ this.ecBlocks = new Array(ecBlocks1, ecBlocks2, ecBlocks3, ecBlocks4);
+ let total = 0;
+ let ecCodewords = ecBlocks1.ECCodewordsPerBlock;
+ let ecbArray = ecBlocks1.getECBlocks();
+ for (let i = 0; i < ecbArray.length; i++) {
+ let ecBlock = ecbArray[i];
+ total += ecBlock.Count * (ecBlock.DataCodewords + ecCodewords);
+ }
+ this.totalCodewords = total;
+ this.__defineGetter__("VersionNumber", function() {
+ return this.versionNumber;
+ });
+ this.__defineGetter__("AlignmentPatternCenters", function() {
+ return this.alignmentPatternCenters;
+ });
+ this.__defineGetter__("TotalCodewords", function() {
+ return this.totalCodewords;
+ });
+ this.__defineGetter__("DimensionForVersion", function() {
+ return 17 + 4 * this.versionNumber;
+ });
+ this.buildFunctionPattern = function() {
+ let dimension = this.DimensionForVersion;
+ let bitMatrix = new BitMatrix(dimension);
+ bitMatrix.setRegion(0, 0, 9, 9);
+ bitMatrix.setRegion(dimension - 8, 0, 8, 9);
+ bitMatrix.setRegion(0, dimension - 8, 9, 8);
+ let max = this.alignmentPatternCenters.length;
+ for (let x = 0; x < max; x++) {
+ let i = this.alignmentPatternCenters[x] - 2;
+ for (let y = 0; y < max; y++) {
+ if (x === 0 && (y === 0 || y === max - 1) || x === max - 1 && y === 0) {
+ continue;
+ }
+ bitMatrix.setRegion(this.alignmentPatternCenters[y] - 2, i, 5, 5);
+ }
+ }
+ bitMatrix.setRegion(6, 9, 1, dimension - 17);
+ bitMatrix.setRegion(9, 6, dimension - 17, 1);
+ if (this.versionNumber > 6) {
+ bitMatrix.setRegion(dimension - 11, 0, 3, 6);
+ bitMatrix.setRegion(0, dimension - 11, 6, 3);
+ }
+ return bitMatrix;
+ };
+ this.getECBlocksForLevel = function(ecLevel) {
+ return this.ecBlocks[ecLevel.ordinal()];
+ };
+}
+
+Version.VERSION_DECODE_INFO = new Array(31892, 34236, 39577, 42195, 48118, 51042, 55367, 58893, 63784, 68472, 70749, 76311, 79154, 84390, 87683, 92361, 96236, 102084, 102881, 110507, 110734, 117786, 119615, 126325, 127568, 133589, 136944, 141498, 145311, 150283, 152622, 158308, 161089, 167017);
+
+Version.VERSIONS = buildVersions();
+
+Version.getVersionForNumber = function(versionNumber) {
+ if (versionNumber < 1 || versionNumber > 40) {
+ throw "ArgumentException";
+ }
+ return Version.VERSIONS[versionNumber - 1];
+};
+
+Version.getProvisionalVersionForDimension = function(dimension) {
+ if (dimension % 4 != 1) {
+ throw "Error getProvisionalVersionForDimension";
+ }
+ try {
+ return Version.getVersionForNumber(dimension - 17 >> 2);
+ } catch (iae) {
+ throw "Error getVersionForNumber";
+ }
+};
+
+Version.decodeVersionInformation = function(versionBits) {
+ let bestDifference = 4294967295;
+ let bestVersion = 0;
+ for (let i = 0; i < Version.VERSION_DECODE_INFO.length; i++) {
+ let targetVersion = Version.VERSION_DECODE_INFO[i];
+ if (targetVersion == versionBits) {
+ return this.getVersionForNumber(i + 7);
+ }
+ let bitsDifference = FormatInformation.numBitsDiffering(versionBits, targetVersion);
+ if (bitsDifference < bestDifference) {
+ bestVersion = i + 7;
+ bestDifference = bitsDifference;
+ }
+ }
+ if (bestDifference <= 3) {
+ return this.getVersionForNumber(bestVersion);
+ }
+ return null;
+};
+
+function buildVersions() {
+ return new Array(new Version(1, new Array(), new ECBlocks(7, new ECB(1, 19)), new ECBlocks(10, new ECB(1, 16)), new ECBlocks(13, new ECB(1, 13)), new ECBlocks(17, new ECB(1, 9))), new Version(2, new Array(6, 18), new ECBlocks(10, new ECB(1, 34)), new ECBlocks(16, new ECB(1, 28)), new ECBlocks(22, new ECB(1, 22)), new ECBlocks(28, new ECB(1, 16))), new Version(3, new Array(6, 22), new ECBlocks(15, new ECB(1, 55)), new ECBlocks(26, new ECB(1, 44)), new ECBlocks(18, new ECB(2, 17)), new ECBlocks(22, new ECB(2, 13))), new Version(4, new Array(6, 26), new ECBlocks(20, new ECB(1, 80)), new ECBlocks(18, new ECB(2, 32)), new ECBlocks(26, new ECB(2, 24)), new ECBlocks(16, new ECB(4, 9))), new Version(5, new Array(6, 30), new ECBlocks(26, new ECB(1, 108)), new ECBlocks(24, new ECB(2, 43)), new ECBlocks(18, new ECB(2, 15), new ECB(2, 16)), new ECBlocks(22, new ECB(2, 11), new ECB(2, 12))), new Version(6, new Array(6, 34), new ECBlocks(18, new ECB(2, 68)), new ECBlocks(16, new ECB(4, 27)), new ECBlocks(24, new ECB(4, 19)), new ECBlocks(28, new ECB(4, 15))), new Version(7, new Array(6, 22, 38), new ECBlocks(20, new ECB(2, 78)), new ECBlocks(18, new ECB(4, 31)), new ECBlocks(18, new ECB(2, 14), new ECB(4, 15)), new ECBlocks(26, new ECB(4, 13), new ECB(1, 14))), new Version(8, new Array(6, 24, 42), new ECBlocks(24, new ECB(2, 97)), new ECBlocks(22, new ECB(2, 38), new ECB(2, 39)), new ECBlocks(22, new ECB(4, 18), new ECB(2, 19)), new ECBlocks(26, new ECB(4, 14), new ECB(2, 15))), new Version(9, new Array(6, 26, 46), new ECBlocks(30, new ECB(2, 116)), new ECBlocks(22, new ECB(3, 36), new ECB(2, 37)), new ECBlocks(20, new ECB(4, 16), new ECB(4, 17)), new ECBlocks(24, new ECB(4, 12), new ECB(4, 13))), new Version(10, new Array(6, 28, 50), new ECBlocks(18, new ECB(2, 68), new ECB(2, 69)), new ECBlocks(26, new ECB(4, 43), new ECB(1, 44)), new ECBlocks(24, new ECB(6, 19), new ECB(2, 20)), new ECBlocks(28, new ECB(6, 15), new ECB(2, 16))), new Version(11, new Array(6, 30, 54), new ECBlocks(20, new ECB(4, 81)), new ECBlocks(30, new ECB(1, 50), new ECB(4, 51)), new ECBlocks(28, new ECB(4, 22), new ECB(4, 23)), new ECBlocks(24, new ECB(3, 12), new ECB(8, 13))), new Version(12, new Array(6, 32, 58), new ECBlocks(24, new ECB(2, 92), new ECB(2, 93)), new ECBlocks(22, new ECB(6, 36), new ECB(2, 37)), new ECBlocks(26, new ECB(4, 20), new ECB(6, 21)), new ECBlocks(28, new ECB(7, 14), new ECB(4, 15))), new Version(13, new Array(6, 34, 62), new ECBlocks(26, new ECB(4, 107)), new ECBlocks(22, new ECB(8, 37), new ECB(1, 38)), new ECBlocks(24, new ECB(8, 20), new ECB(4, 21)), new ECBlocks(22, new ECB(12, 11), new ECB(4, 12))), new Version(14, new Array(6, 26, 46, 66), new ECBlocks(30, new ECB(3, 115), new ECB(1, 116)), new ECBlocks(24, new ECB(4, 40), new ECB(5, 41)), new ECBlocks(20, new ECB(11, 16), new ECB(5, 17)), new ECBlocks(24, new ECB(11, 12), new ECB(5, 13))), new Version(15, new Array(6, 26, 48, 70), new ECBlocks(22, new ECB(5, 87), new ECB(1, 88)), new ECBlocks(24, new ECB(5, 41), new ECB(5, 42)), new ECBlocks(30, new ECB(5, 24), new ECB(7, 25)), new ECBlocks(24, new ECB(11, 12), new ECB(7, 13))), new Version(16, new Array(6, 26, 50, 74), new ECBlocks(24, new ECB(5, 98), new ECB(1, 99)), new ECBlocks(28, new ECB(7, 45), new ECB(3, 46)), new ECBlocks(24, new ECB(15, 19), new ECB(2, 20)), new ECBlocks(30, new ECB(3, 15), new ECB(13, 16))), new Version(17, new Array(6, 30, 54, 78), new ECBlocks(28, new ECB(1, 107), new ECB(5, 108)), new ECBlocks(28, new ECB(10, 46), new ECB(1, 47)), new ECBlocks(28, new ECB(1, 22), new ECB(15, 23)), new ECBlocks(28, new ECB(2, 14), new ECB(17, 15))), new Version(18, new Array(6, 30, 56, 82), new ECBlocks(30, new ECB(5, 120), new ECB(1, 121)), new ECBlocks(26, new ECB(9, 43), new ECB(4, 44)), new ECBlocks(28, new ECB(17, 22), new ECB(1, 23)), new ECBlocks(28, new ECB(2, 14), new ECB(19, 15))), new Version(19, new Array(6, 30, 58, 86), new ECBlocks(28, new ECB(3, 113), new ECB(4, 114)), new ECBlocks(26, new ECB(3, 44), new ECB(11, 45)), new ECBlocks(26, new ECB(17, 21), new ECB(4, 22)), new ECBlocks(26, new ECB(9, 13), new ECB(16, 14))), new Version(20, new Array(6, 34, 62, 90), new ECBlocks(28, new ECB(3, 107), new ECB(5, 108)), new ECBlocks(26, new ECB(3, 41), new ECB(13, 42)), new ECBlocks(30, new ECB(15, 24), new ECB(5, 25)), new ECBlocks(28, new ECB(15, 15), new ECB(10, 16))), new Version(21, new Array(6, 28, 50, 72, 94), new ECBlocks(28, new ECB(4, 116), new ECB(4, 117)), new ECBlocks(26, new ECB(17, 42)), new ECBlocks(28, new ECB(17, 22), new ECB(6, 23)), new ECBlocks(30, new ECB(19, 16), new ECB(6, 17))), new Version(22, new Array(6, 26, 50, 74, 98), new ECBlocks(28, new ECB(2, 111), new ECB(7, 112)), new ECBlocks(28, new ECB(17, 46)), new ECBlocks(30, new ECB(7, 24), new ECB(16, 25)), new ECBlocks(24, new ECB(34, 13))), new Version(23, new Array(6, 30, 54, 74, 102), new ECBlocks(30, new ECB(4, 121), new ECB(5, 122)), new ECBlocks(28, new ECB(4, 47), new ECB(14, 48)), new ECBlocks(30, new ECB(11, 24), new ECB(14, 25)), new ECBlocks(30, new ECB(16, 15), new ECB(14, 16))), new Version(24, new Array(6, 28, 54, 80, 106), new ECBlocks(30, new ECB(6, 117), new ECB(4, 118)), new ECBlocks(28, new ECB(6, 45), new ECB(14, 46)), new ECBlocks(30, new ECB(11, 24), new ECB(16, 25)), new ECBlocks(30, new ECB(30, 16), new ECB(2, 17))), new Version(25, new Array(6, 32, 58, 84, 110), new ECBlocks(26, new ECB(8, 106), new ECB(4, 107)), new ECBlocks(28, new ECB(8, 47), new ECB(13, 48)), new ECBlocks(30, new ECB(7, 24), new ECB(22, 25)), new ECBlocks(30, new ECB(22, 15), new ECB(13, 16))), new Version(26, new Array(6, 30, 58, 86, 114), new ECBlocks(28, new ECB(10, 114), new ECB(2, 115)), new ECBlocks(28, new ECB(19, 46), new ECB(4, 47)), new ECBlocks(28, new ECB(28, 22), new ECB(6, 23)), new ECBlocks(30, new ECB(33, 16), new ECB(4, 17))), new Version(27, new Array(6, 34, 62, 90, 118), new ECBlocks(30, new ECB(8, 122), new ECB(4, 123)), new ECBlocks(28, new ECB(22, 45), new ECB(3, 46)), new ECBlocks(30, new ECB(8, 23), new ECB(26, 24)), new ECBlocks(30, new ECB(12, 15), new ECB(28, 16))), new Version(28, new Array(6, 26, 50, 74, 98, 122), new ECBlocks(30, new ECB(3, 117), new ECB(10, 118)), new ECBlocks(28, new ECB(3, 45), new ECB(23, 46)), new ECBlocks(30, new ECB(4, 24), new ECB(31, 25)), new ECBlocks(30, new ECB(11, 15), new ECB(31, 16))), new Version(29, new Array(6, 30, 54, 78, 102, 126), new ECBlocks(30, new ECB(7, 116), new ECB(7, 117)), new ECBlocks(28, new ECB(21, 45), new ECB(7, 46)), new ECBlocks(30, new ECB(1, 23), new ECB(37, 24)), new ECBlocks(30, new ECB(19, 15), new ECB(26, 16))), new Version(30, new Array(6, 26, 52, 78, 104, 130), new ECBlocks(30, new ECB(5, 115), new ECB(10, 116)), new ECBlocks(28, new ECB(19, 47), new ECB(10, 48)), new ECBlocks(30, new ECB(15, 24), new ECB(25, 25)), new ECBlocks(30, new ECB(23, 15), new ECB(25, 16))), new Version(31, new Array(6, 30, 56, 82, 108, 134), new ECBlocks(30, new ECB(13, 115), new ECB(3, 116)), new ECBlocks(28, new ECB(2, 46), new ECB(29, 47)), new ECBlocks(30, new ECB(42, 24), new ECB(1, 25)), new ECBlocks(30, new ECB(23, 15), new ECB(28, 16))), new Version(32, new Array(6, 34, 60, 86, 112, 138), new ECBlocks(30, new ECB(17, 115)), new ECBlocks(28, new ECB(10, 46), new ECB(23, 47)), new ECBlocks(30, new ECB(10, 24), new ECB(35, 25)), new ECBlocks(30, new ECB(19, 15), new ECB(35, 16))), new Version(33, new Array(6, 30, 58, 86, 114, 142), new ECBlocks(30, new ECB(17, 115), new ECB(1, 116)), new ECBlocks(28, new ECB(14, 46), new ECB(21, 47)), new ECBlocks(30, new ECB(29, 24), new ECB(19, 25)), new ECBlocks(30, new ECB(11, 15), new ECB(46, 16))), new Version(34, new Array(6, 34, 62, 90, 118, 146), new ECBlocks(30, new ECB(13, 115), new ECB(6, 116)), new ECBlocks(28, new ECB(14, 46), new ECB(23, 47)), new ECBlocks(30, new ECB(44, 24), new ECB(7, 25)), new ECBlocks(30, new ECB(59, 16), new ECB(1, 17))), new Version(35, new Array(6, 30, 54, 78, 102, 126, 150), new ECBlocks(30, new ECB(12, 121), new ECB(7, 122)), new ECBlocks(28, new ECB(12, 47), new ECB(26, 48)), new ECBlocks(30, new ECB(39, 24), new ECB(14, 25)), new ECBlocks(30, new ECB(22, 15), new ECB(41, 16))), new Version(36, new Array(6, 24, 50, 76, 102, 128, 154), new ECBlocks(30, new ECB(6, 121), new ECB(14, 122)), new ECBlocks(28, new ECB(6, 47), new ECB(34, 48)), new ECBlocks(30, new ECB(46, 24), new ECB(10, 25)), new ECBlocks(30, new ECB(2, 15), new ECB(64, 16))), new Version(37, new Array(6, 28, 54, 80, 106, 132, 158), new ECBlocks(30, new ECB(17, 122), new ECB(4, 123)), new ECBlocks(28, new ECB(29, 46), new ECB(14, 47)), new ECBlocks(30, new ECB(49, 24), new ECB(10, 25)), new ECBlocks(30, new ECB(24, 15), new ECB(46, 16))), new Version(38, new Array(6, 32, 58, 84, 110, 136, 162), new ECBlocks(30, new ECB(4, 122), new ECB(18, 123)), new ECBlocks(28, new ECB(13, 46), new ECB(32, 47)), new ECBlocks(30, new ECB(48, 24), new ECB(14, 25)), new ECBlocks(30, new ECB(42, 15), new ECB(32, 16))), new Version(39, new Array(6, 26, 54, 82, 110, 138, 166), new ECBlocks(30, new ECB(20, 117), new ECB(4, 118)), new ECBlocks(28, new ECB(40, 47), new ECB(7, 48)), new ECBlocks(30, new ECB(43, 24), new ECB(22, 25)), new ECBlocks(30, new ECB(10, 15), new ECB(67, 16))), new Version(40, new Array(6, 30, 58, 86, 114, 142, 170), new ECBlocks(30, new ECB(19, 118), new ECB(6, 119)), new ECBlocks(28, new ECB(18, 47), new ECB(31, 48)), new ECBlocks(30, new ECB(34, 24), new ECB(34, 25)), new ECBlocks(30, new ECB(20, 15), new ECB(61, 16))));
+}
+
+function PerspectiveTransform(a11, a21, a31, a12, a22, a32, a13, a23, a33) {
+ this.a11 = a11;
+ this.a12 = a12;
+ this.a13 = a13;
+ this.a21 = a21;
+ this.a22 = a22;
+ this.a23 = a23;
+ this.a31 = a31;
+ this.a32 = a32;
+ this.a33 = a33;
+ this.transformPoints1 = function(points) {
+ let max = points.length;
+ let a11 = this.a11;
+ let a12 = this.a12;
+ let a13 = this.a13;
+ let a21 = this.a21;
+ let a22 = this.a22;
+ let a23 = this.a23;
+ let a31 = this.a31;
+ let a32 = this.a32;
+ let a33 = this.a33;
+ for (let i = 0; i < max; i += 2) {
+ let x = points[i];
+ let y = points[i + 1];
+ let denominator = a13 * x + a23 * y + a33;
+ points[i] = (a11 * x + a21 * y + a31) / denominator;
+ points[i + 1] = (a12 * x + a22 * y + a32) / denominator;
+ }
+ };
+ this.transformPoints2 = function(xValues, yValues) {
+ let n = xValues.length;
+ for (let i = 0; i < n; i++) {
+ let x = xValues[i];
+ let y = yValues[i];
+ let denominator = this.a13 * x + this.a23 * y + this.a33;
+ xValues[i] = (this.a11 * x + this.a21 * y + this.a31) / denominator;
+ yValues[i] = (this.a12 * x + this.a22 * y + this.a32) / denominator;
+ }
+ };
+ this.buildAdjoint = function() {
+ return new PerspectiveTransform(this.a22 * this.a33 - this.a23 * this.a32, this.a23 * this.a31 - this.a21 * this.a33, this.a21 * this.a32 - this.a22 * this.a31, this.a13 * this.a32 - this.a12 * this.a33, this.a11 * this.a33 - this.a13 * this.a31, this.a12 * this.a31 - this.a11 * this.a32, this.a12 * this.a23 - this.a13 * this.a22, this.a13 * this.a21 - this.a11 * this.a23, this.a11 * this.a22 - this.a12 * this.a21);
+ };
+ this.times = function(other) {
+ return new PerspectiveTransform(this.a11 * other.a11 + this.a21 * other.a12 + this.a31 * other.a13, this.a11 * other.a21 + this.a21 * other.a22 + this.a31 * other.a23, this.a11 * other.a31 + this.a21 * other.a32 + this.a31 * other.a33, this.a12 * other.a11 + this.a22 * other.a12 + this.a32 * other.a13, this.a12 * other.a21 + this.a22 * other.a22 + this.a32 * other.a23, this.a12 * other.a31 + this.a22 * other.a32 + this.a32 * other.a33, this.a13 * other.a11 + this.a23 * other.a12 + this.a33 * other.a13, this.a13 * other.a21 + this.a23 * other.a22 + this.a33 * other.a23, this.a13 * other.a31 + this.a23 * other.a32 + this.a33 * other.a33);
+ };
+}
+
+PerspectiveTransform.quadrilateralToQuadrilateral = function(x0, y0, x1, y1, x2, y2, x3, y3, x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p) {
+ let qToS = this.quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3);
+ let sToQ = this.squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p);
+ return sToQ.times(qToS);
+};
+
+PerspectiveTransform.squareToQuadrilateral = function(x0, y0, x1, y1, x2, y2, x3, y3) {
+ let dy2 = y3 - y2;
+ let dy3 = y0 - y1 + y2 - y3;
+ if (dy2 === 0 && dy3 === 0) {
+ return new PerspectiveTransform(x1 - x0, x2 - x1, x0, y1 - y0, y2 - y1, y0, 0, 0, 1);
+ } else {
+ let dx1 = x1 - x2;
+ let dx2 = x3 - x2;
+ let dx3 = x0 - x1 + x2 - x3;
+ let dy1 = y1 - y2;
+ let denominator = dx1 * dy2 - dx2 * dy1;
+ let a13 = (dx3 * dy2 - dx2 * dy3) / denominator;
+ let a23 = (dx1 * dy3 - dx3 * dy1) / denominator;
+ return new PerspectiveTransform(x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0, y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0, a13, a23, 1);
+ }
+};
+
+PerspectiveTransform.quadrilateralToSquare = function(x0, y0, x1, y1, x2, y2, x3, y3) {
+ return this.squareToQuadrilateral(x0, y0, x1, y1, x2, y2, x3, y3).buildAdjoint();
+};
+
+function DetectorResult(bits, points) {
+ this.bits = bits;
+ this.points = points;
+}
+
+function Detector(image) {
+ this.image = image;
+ this.resultPointCallback = null;
+ this.sizeOfBlackWhiteBlackRun = function(fromX, fromY, toX, toY) {
+ let steep = Math.abs(toY - fromY) > Math.abs(toX - fromX);
+ if (steep) {
+ let temp = fromX;
+ fromX = fromY;
+ fromY = temp;
+ temp = toX;
+ toX = toY;
+ toY = temp;
+ }
+ let dx = Math.abs(toX - fromX);
+ let dy = Math.abs(toY - fromY);
+ let error = -dx >> 1;
+ let ystep = fromY < toY ? 1 : -1;
+ let xstep = fromX < toX ? 1 : -1;
+ let state = 0;
+ for (let x = fromX, y = fromY; x != toX; x += xstep) {
+ let realX = steep ? y : x;
+ let realY = steep ? x : y;
+ if (state == 1) {
+ if (this.image[realX + realY * imgWidth]) {
+ state++;
+ }
+ } else {
+ if (!this.image[realX + realY * imgWidth]) {
+ state++;
+ }
+ }
+ if (state == 3) {
+ let diffX = x - fromX;
+ let diffY = y - fromY;
+ return Math.sqrt(diffX * diffX + diffY * diffY);
+ }
+ error += dy;
+ if (error > 0) {
+ if (y == toY) {
+ break;
+ }
+ y += ystep;
+ error -= dx;
+ }
+ }
+ let diffX2 = toX - fromX;
+ let diffY2 = toY - fromY;
+ return Math.sqrt(diffX2 * diffX2 + diffY2 * diffY2);
+ };
+ this.sizeOfBlackWhiteBlackRunBothWays = function(fromX, fromY, toX, toY) {
+ let result = this.sizeOfBlackWhiteBlackRun(fromX, fromY, toX, toY);
+ let scale = 1;
+ let otherToX = fromX - (toX - fromX);
+ if (otherToX < 0) {
+ scale = fromX / (fromX - otherToX);
+ otherToX = 0;
+ } else if (otherToX >= imgWidth) {
+ scale = (imgWidth - 1 - fromX) / (otherToX - fromX);
+ otherToX = imgWidth - 1;
+ }
+ let otherToY = Math.floor(fromY - (toY - fromY) * scale);
+ scale = 1;
+ if (otherToY < 0) {
+ scale = fromY / (fromY - otherToY);
+ otherToY = 0;
+ } else if (otherToY >= imgHeight) {
+ scale = (imgHeight - 1 - fromY) / (otherToY - fromY);
+ otherToY = imgHeight - 1;
+ }
+ otherToX = Math.floor(fromX + (otherToX - fromX) * scale);
+ result += this.sizeOfBlackWhiteBlackRun(fromX, fromY, otherToX, otherToY);
+ return result - 1;
+ };
+ this.calculateModuleSizeOneWay = function(pattern, otherPattern) {
+ let moduleSizeEst1 = this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(pattern.X), Math.floor(pattern.Y), Math.floor(otherPattern.X), Math.floor(otherPattern.Y));
+ let moduleSizeEst2 = this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(otherPattern.X), Math.floor(otherPattern.Y), Math.floor(pattern.X), Math.floor(pattern.Y));
+ if (isNaN(moduleSizeEst1)) {
+ return moduleSizeEst2 / 7;
+ }
+ if (isNaN(moduleSizeEst2)) {
+ return moduleSizeEst1 / 7;
+ }
+ return (moduleSizeEst1 + moduleSizeEst2) / 14;
+ };
+ this.calculateModuleSize = function(topLeft, topRight, bottomLeft) {
+ return (this.calculateModuleSizeOneWay(topLeft, topRight) + this.calculateModuleSizeOneWay(topLeft, bottomLeft)) / 2;
+ };
+ this.distance = function(pattern1, pattern2) {
+ let xDiff = pattern1.X - pattern2.X;
+ let yDiff = pattern1.Y - pattern2.Y;
+ return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
+ };
+ this.computeDimension = function(topLeft, topRight, bottomLeft, moduleSize) {
+ let tltrCentersDimension = Math.round(this.distance(topLeft, topRight) / moduleSize);
+ let tlblCentersDimension = Math.round(this.distance(topLeft, bottomLeft) / moduleSize);
+ let dimension = (tltrCentersDimension + tlblCentersDimension >> 1) + 7;
+ switch (dimension & 3) {
+ case 0:
+ dimension++;
+ break;
+
+ case 2:
+ dimension--;
+ break;
+
+ case 3:
+ throw "Error";
+ }
+ return dimension;
+ };
+ this.findAlignmentInRegion = function(overallEstModuleSize, estAlignmentX, estAlignmentY, allowanceFactor) {
+ let allowance = Math.floor(allowanceFactor * overallEstModuleSize);
+ let alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance);
+ let alignmentAreaRightX = Math.min(imgWidth - 1, estAlignmentX + allowance);
+ if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize * 3) {
+ throw "Error";
+ }
+ let alignmentAreaTopY = Math.max(0, estAlignmentY - allowance);
+ let alignmentAreaBottomY = Math.min(imgHeight - 1, estAlignmentY + allowance);
+ let alignmentFinder = new AlignmentPatternFinder(this.image, alignmentAreaLeftX, alignmentAreaTopY, alignmentAreaRightX - alignmentAreaLeftX, alignmentAreaBottomY - alignmentAreaTopY, overallEstModuleSize, this.resultPointCallback);
+ return alignmentFinder.find();
+ };
+ this.createTransform = function(topLeft, topRight, bottomLeft, alignmentPattern, dimension) {
+ let dimMinusThree = dimension - 3.5;
+ let bottomRightX;
+ let bottomRightY;
+ let sourceBottomRightX;
+ let sourceBottomRightY;
+ if (alignmentPattern !== null) {
+ bottomRightX = alignmentPattern.X;
+ bottomRightY = alignmentPattern.Y;
+ sourceBottomRightX = sourceBottomRightY = dimMinusThree - 3;
+ } else {
+ bottomRightX = topRight.X - topLeft.X + bottomLeft.X;
+ bottomRightY = topRight.Y - topLeft.Y + bottomLeft.Y;
+ sourceBottomRightX = sourceBottomRightY = dimMinusThree;
+ }
+ let transform = PerspectiveTransform.quadrilateralToQuadrilateral(3.5, 3.5, dimMinusThree, 3.5, sourceBottomRightX, sourceBottomRightY, 3.5, dimMinusThree, topLeft.X, topLeft.Y, topRight.X, topRight.Y, bottomRightX, bottomRightY, bottomLeft.X, bottomLeft.Y);
+ return transform;
+ };
+ this.sampleGrid = function(image, transform, dimension) {
+ let sampler = GridSampler;
+ return sampler.sampleGrid3(image, dimension, transform);
+ };
+ this.processFinderPatternInfo = function(info) {
+ let topLeft = info.TopLeft;
+ let topRight = info.TopRight;
+ let bottomLeft = info.BottomLeft;
+ let moduleSize = this.calculateModuleSize(topLeft, topRight, bottomLeft);
+ if (moduleSize < 1) {
+ throw "Error";
+ }
+ let dimension = this.computeDimension(topLeft, topRight, bottomLeft, moduleSize);
+ let provisionalVersion = Version.getProvisionalVersionForDimension(dimension);
+ let modulesBetweenFPCenters = provisionalVersion.DimensionForVersion - 7;
+ let alignmentPattern = null;
+ if (provisionalVersion.AlignmentPatternCenters.length > 0) {
+ let bottomRightX = topRight.X - topLeft.X + bottomLeft.X;
+ let bottomRightY = topRight.Y - topLeft.Y + bottomLeft.Y;
+ let correctionToTopLeft = 1 - 3 / modulesBetweenFPCenters;
+ let estAlignmentX = Math.floor(topLeft.X + correctionToTopLeft * (bottomRightX - topLeft.X));
+ let estAlignmentY = Math.floor(topLeft.Y + correctionToTopLeft * (bottomRightY - topLeft.Y));
+ for (let i = 4; i <= 16; i <<= 1) {
+ alignmentPattern = this.findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i);
+ break;
+ }
+ }
+ let transform = this.createTransform(topLeft, topRight, bottomLeft, alignmentPattern, dimension);
+ let bits = this.sampleGrid(this.image, transform, dimension);
+ let points;
+ if (alignmentPattern === null) {
+ points = new Array(bottomLeft, topLeft, topRight);
+ } else {
+ points = new Array(bottomLeft, topLeft, topRight, alignmentPattern);
+ }
+ return new DetectorResult(bits, points);
+ };
+ this.detect = function() {
+ let info = new FinderPatternFinder().findFinderPattern(this.image);
+ return this.processFinderPatternInfo(info);
+ };
+}
+
+var FORMAT_INFO_MASK_QR = 21522;
+
+var FORMAT_INFO_DECODE_LOOKUP = new Array(new Array(21522, 0), new Array(20773, 1), new Array(24188, 2), new Array(23371, 3), new Array(17913, 4), new Array(16590, 5), new Array(20375, 6), new Array(19104, 7), new Array(30660, 8), new Array(29427, 9), new Array(32170, 10), new Array(30877, 11), new Array(26159, 12), new Array(25368, 13), new Array(27713, 14), new Array(26998, 15), new Array(5769, 16), new Array(5054, 17), new Array(7399, 18), new Array(6608, 19), new Array(1890, 20), new Array(597, 21), new Array(3340, 22), new Array(2107, 23), new Array(13663, 24), new Array(12392, 25), new Array(16177, 26), new Array(14854, 27), new Array(9396, 28), new Array(8579, 29), new Array(11994, 30), new Array(11245, 31));
+
+var BITS_SET_IN_HALF_BYTE = new Array(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);
+
+function FormatInformation(formatInfo) {
+ this.errorCorrectionLevel = ErrorCorrectionLevel.forBits(formatInfo >> 3 & 3);
+ this.dataMask = formatInfo & 7;
+ this.__defineGetter__("ErrorCorrectionLevel", function() {
+ return this.errorCorrectionLevel;
+ });
+ this.__defineGetter__("DataMask", function() {
+ return this.dataMask;
+ });
+ this.GetHashCode = function() {
+ return this.errorCorrectionLevel.ordinal() << 3 | this.dataMask;
+ };
+ this.Equals = function(o) {
+ let other = o;
+ return this.errorCorrectionLevel == other.errorCorrectionLevel && this.dataMask == other.dataMask;
+ };
+}
+
+FormatInformation.numBitsDiffering = function(a, b) {
+ a ^= b;
+ return BITS_SET_IN_HALF_BYTE[a & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 4) & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 8) & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 12) & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 16) & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 20) & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 24) & 15] + BITS_SET_IN_HALF_BYTE[URShift(a, 28) & 15];
+};
+
+FormatInformation.decodeFormatInformation = function(maskedFormatInfo) {
+ let formatInfo = FormatInformation.doDecodeFormatInformation(maskedFormatInfo);
+ if (formatInfo !== null) {
+ return formatInfo;
+ }
+ return FormatInformation.doDecodeFormatInformation(maskedFormatInfo ^ FORMAT_INFO_MASK_QR);
+};
+
+FormatInformation.doDecodeFormatInformation = function(maskedFormatInfo) {
+ let bestDifference = 4294967295;
+ let bestFormatInfo = 0;
+ for (let i = 0; i < FORMAT_INFO_DECODE_LOOKUP.length; i++) {
+ let decodeInfo = FORMAT_INFO_DECODE_LOOKUP[i];
+ let targetInfo = decodeInfo[0];
+ if (targetInfo == maskedFormatInfo) {
+ return new FormatInformation(decodeInfo[1]);
+ }
+ let bitsDifference = this.numBitsDiffering(maskedFormatInfo, targetInfo);
+ if (bitsDifference < bestDifference) {
+ bestFormatInfo = decodeInfo[1];
+ bestDifference = bitsDifference;
+ }
+ }
+ if (bestDifference <= 3) {
+ return new FormatInformation(bestFormatInfo);
+ }
+ return null;
+};
+
+function ErrorCorrectionLevel(ordinal, bits, name) {
+ this.ordinal_Renamed_Field = ordinal;
+ this.bits = bits;
+ this.name = name;
+ this.__defineGetter__("Bits", function() {
+ return this.bits;
+ });
+ this.__defineGetter__("Name", function() {
+ return this.name;
+ });
+ this.ordinal = function() {
+ return this.ordinal_Renamed_Field;
+ };
+}
+
+var L = new ErrorCorrectionLevel(0, 1, "L");
+
+var M = new ErrorCorrectionLevel(1, 0, "M");
+
+var Q = new ErrorCorrectionLevel(2, 3, "Q");
+
+var H = new ErrorCorrectionLevel(3, 2, "H");
+
+var FOR_BITS = new Array(M, L, H, Q);
+
+ErrorCorrectionLevel.forBits = function(bits) {
+ if (bits < 0 || bits >= FOR_BITS.length) {
+ throw "ArgumentException";
+ }
+ return FOR_BITS[bits];
+};
+
+function BitMatrix(width, height) {
+ if (!height) height = width;
+ if (width < 1 || height < 1) {
+ throw "Both dimensions must be greater than 0";
+ }
+ this.width = width;
+ this.height = height;
+ let rowSize = width >> 5;
+ if ((width & 31) !== 0) {
+ rowSize++;
+ }
+ this.rowSize = rowSize;
+ this.bits = new Array(rowSize * height);
+ for (let i = 0; i < this.bits.length; i++) this.bits[i] = 0;
+ this.__defineGetter__("Width", function() {
+ return this.width;
+ });
+ this.__defineGetter__("Height", function() {
+ return this.height;
+ });
+ this.__defineGetter__("Dimension", function() {
+ if (this.width != this.height) {
+ throw "Can't call getDimension() on a non-square matrix";
+ }
+ return this.width;
+ });
+ this.get_Renamed = function(x, y) {
+ let offset = y * this.rowSize + (x >> 5);
+ return (URShift(this.bits[offset], x & 31) & 1) !== 0;
+ };
+ this.set_Renamed = function(x, y) {
+ let offset = y * this.rowSize + (x >> 5);
+ this.bits[offset] |= 1 << (x & 31);
+ };
+ this.flip = function(x, y) {
+ let offset = y * this.rowSize + (x >> 5);
+ this.bits[offset] ^= 1 << (x & 31);
+ };
+ this.clear = function() {
+ let max = this.bits.length;
+ for (let i = 0; i < max; i++) {
+ this.bits[i] = 0;
+ }
+ };
+ this.setRegion = function(left, top, width, height) {
+ if (top < 0 || left < 0) {
+ throw "Left and top must be nonnegative";
+ }
+ if (height < 1 || width < 1) {
+ throw "Height and width must be at least 1";
+ }
+ let right = left + width;
+ let bottom = top + height;
+ if (bottom > this.height || right > this.width) {
+ throw "The region must fit inside the matrix";
+ }
+ for (let y = top; y < bottom; y++) {
+ let offset = y * this.rowSize;
+ for (let x = left; x < right; x++) {
+ this.bits[offset + (x >> 5)] |= 1 << (x & 31);
+ }
+ }
+ };
+}
+
+function DataBlock(numDataCodewords, codewords) {
+ this.numDataCodewords = numDataCodewords;
+ this.codewords = codewords;
+ this.__defineGetter__("NumDataCodewords", function() {
+ return this.numDataCodewords;
+ });
+ this.__defineGetter__("Codewords", function() {
+ return this.codewords;
+ });
+}
+
+DataBlock.getDataBlocks = function(rawCodewords, version, ecLevel) {
+ if (rawCodewords.length != version.TotalCodewords) {
+ throw "ArgumentException";
+ }
+ let ecBlocks = version.getECBlocksForLevel(ecLevel);
+ let totalBlocks = 0;
+ let ecBlockArray = ecBlocks.getECBlocks();
+ for (let i = 0; i < ecBlockArray.length; i++) {
+ totalBlocks += ecBlockArray[i].Count;
+ }
+ let result = new Array(totalBlocks);
+ let numResultBlocks = 0;
+ for (let j = 0; j < ecBlockArray.length; j++) {
+ let ecBlock = ecBlockArray[j];
+ for (let i = 0; i < ecBlock.Count; i++) {
+ let numDataCodewords = ecBlock.DataCodewords;
+ let numBlockCodewords = ecBlocks.ECCodewordsPerBlock + numDataCodewords;
+ result[numResultBlocks++] = new DataBlock(numDataCodewords, new Array(numBlockCodewords));
+ }
+ }
+ let shorterBlocksTotalCodewords = result[0].codewords.length;
+ let longerBlocksStartAt = result.length - 1;
+ while (longerBlocksStartAt >= 0) {
+ let numCodewords = result[longerBlocksStartAt].codewords.length;
+ if (numCodewords == shorterBlocksTotalCodewords) {
+ break;
+ }
+ longerBlocksStartAt--;
+ }
+ longerBlocksStartAt++;
+ let shorterBlocksNumDataCodewords = shorterBlocksTotalCodewords - ecBlocks.ECCodewordsPerBlock;
+ let rawCodewordsOffset = 0;
+ for (let i = 0; i < shorterBlocksNumDataCodewords; i++) {
+ for (let j = 0; j < numResultBlocks; j++) {
+ result[j].codewords[i] = rawCodewords[rawCodewordsOffset++];
+ }
+ }
+ for (let j = longerBlocksStartAt; j < numResultBlocks; j++) {
+ result[j].codewords[shorterBlocksNumDataCodewords] = rawCodewords[rawCodewordsOffset++];
+ }
+ let max = result[0].codewords.length;
+ for (let i = shorterBlocksNumDataCodewords; i < max; i++) {
+ for (let j = 0; j < numResultBlocks; j++) {
+ let iOffset = j < longerBlocksStartAt ? i : i + 1;
+ result[j].codewords[iOffset] = rawCodewords[rawCodewordsOffset++];
+ }
+ }
+ return result;
+};
+
+var DataMask = {};
+
+function BitMatrixParser(bitMatrix) {
+ let dimension = bitMatrix.Dimension;
+ if (dimension < 21 || (dimension & 3) != 1) {
+ throw "Error BitMatrixParser";
+ }
+ this.bitMatrix = bitMatrix;
+ this.parsedVersion = null;
+ this.parsedFormatInfo = null;
+ this.copyBit = function(i, j, versionBits) {
+ return this.bitMatrix.get_Renamed(i, j) ? versionBits << 1 | 1 : versionBits << 1;
+ };
+ this.readFormatInformation = function() {
+ if (this.parsedFormatInfo !== null) {
+ return this.parsedFormatInfo;
+ }
+ let formatInfoBits = 0;
+ for (let i = 0; i < 6; i++) {
+ formatInfoBits = this.copyBit(i, 8, formatInfoBits);
+ }
+ formatInfoBits = this.copyBit(7, 8, formatInfoBits);
+ formatInfoBits = this.copyBit(8, 8, formatInfoBits);
+ formatInfoBits = this.copyBit(8, 7, formatInfoBits);
+ for (let j = 5; j >= 0; j--) {
+ formatInfoBits = this.copyBit(8, j, formatInfoBits);
+ }
+ this.parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits);
+ if (this.parsedFormatInfo !== null) {
+ return this.parsedFormatInfo;
+ }
+ let dimension = this.bitMatrix.Dimension;
+ formatInfoBits = 0;
+ let iMin = dimension - 8;
+ for (let i = dimension - 1; i >= iMin; i--) {
+ formatInfoBits = this.copyBit(i, 8, formatInfoBits);
+ }
+ for (let j = dimension - 7; j < dimension; j++) {
+ formatInfoBits = this.copyBit(8, j, formatInfoBits);
+ }
+ this.parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits);
+ if (this.parsedFormatInfo !== null) {
+ return this.parsedFormatInfo;
+ }
+ throw "Error readFormatInformation";
+ };
+ this.readVersion = function() {
+ if (this.parsedVersion !== null) {
+ return this.parsedVersion;
+ }
+ let dimension = this.bitMatrix.Dimension;
+ let provisionalVersion = dimension - 17 >> 2;
+ if (provisionalVersion <= 6) {
+ return Version.getVersionForNumber(provisionalVersion);
+ }
+ let versionBits = 0;
+ let ijMin = dimension - 11;
+ for (let j = 5; j >= 0; j--) {
+ for (let i = dimension - 9; i >= ijMin; i--) {
+ versionBits = this.copyBit(i, j, versionBits);
+ }
+ }
+ this.parsedVersion = Version.decodeVersionInformation(versionBits);
+ if (this.parsedVersion !== null && this.parsedVersion.DimensionForVersion == dimension) {
+ return this.parsedVersion;
+ }
+ versionBits = 0;
+ for (let i = 5; i >= 0; i--) {
+ for (let j = dimension - 9; j >= ijMin; j--) {
+ versionBits = this.copyBit(i, j, versionBits);
+ }
+ }
+ this.parsedVersion = Version.decodeVersionInformation(versionBits);
+ if (this.parsedVersion !== null && this.parsedVersion.DimensionForVersion == dimension) {
+ return this.parsedVersion;
+ }
+ throw "Error readVersion";
+ };
+ this.readCodewords = function() {
+ let formatInfo = this.readFormatInformation();
+ let version = this.readVersion();
+ let dataMask = DataMask.forReference(formatInfo.DataMask);
+ let dimension = this.bitMatrix.Dimension;
+ dataMask.unmaskBitMatrix(this.bitMatrix, dimension);
+ let functionPattern = version.buildFunctionPattern();
+ let readingUp = true;
+ let result = new Array(version.TotalCodewords);
+ let resultOffset = 0;
+ let currentByte = 0;
+ let bitsRead = 0;
+ for (let j = dimension - 1; j > 0; j -= 2) {
+ if (j == 6) {
+ j--;
+ }
+ for (let count = 0; count < dimension; count++) {
+ let i = readingUp ? dimension - 1 - count : count;
+ for (let col = 0; col < 2; col++) {
+ if (!functionPattern.get_Renamed(j - col, i)) {
+ bitsRead++;
+ currentByte <<= 1;
+ if (this.bitMatrix.get_Renamed(j - col, i)) {
+ currentByte |= 1;
+ }
+ if (bitsRead == 8) {
+ result[resultOffset++] = currentByte;
+ bitsRead = 0;
+ currentByte = 0;
+ }
+ }
+ }
+ }
+ readingUp ^= true;
+ }
+ if (resultOffset != version.TotalCodewords) {
+ throw "Error readCodewords";
+ }
+ return result;
+ };
+}
+
+DataMask.forReference = function(reference) {
+ if (reference < 0 || reference > 7) {
+ throw "System.ArgumentException";
+ }
+ return DataMask.DATA_MASKS[reference];
+};
+
+function DataMask000() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ return (i + j & 1) === 0;
+ };
+}
+
+function DataMask001() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ return (i & 1) === 0;
+ };
+}
+
+function DataMask010() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ return j % 3 === 0;
+ };
+}
+
+function DataMask011() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ return (i + j) % 3 === 0;
+ };
+}
+
+function DataMask100() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ return (URShift(i, 1) + j / 3 & 1) === 0;
+ };
+}
+
+function DataMask101() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ let temp = i * j;
+ return (temp & 1) + temp % 3 === 0;
+ };
+}
+
+function DataMask110() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ let temp = i * j;
+ return ((temp & 1) + temp % 3 & 1) === 0;
+ };
+}
+
+function DataMask111() {
+ this.unmaskBitMatrix = function(bits, dimension) {
+ for (let i = 0; i < dimension; i++) {
+ for (let j = 0; j < dimension; j++) {
+ if (this.isMasked(i, j)) {
+ bits.flip(j, i);
+ }
+ }
+ }
+ };
+ this.isMasked = function(i, j) {
+ return ((i + j & 1) + i * j % 3 & 1) === 0;
+ };
+}
+
+DataMask.DATA_MASKS = new Array(new DataMask000(), new DataMask001(), new DataMask010(), new DataMask011(), new DataMask100(), new DataMask101(), new DataMask110(), new DataMask111());
+
+function ReedSolomonDecoder(field) {
+ this.field = field;
+ this.decode = function(received, twoS) {
+ let poly = new GF256Poly(this.field, received);
+ let syndromeCoefficients = new Array(twoS);
+ for (let i = 0; i < syndromeCoefficients.length; i++) syndromeCoefficients[i] = 0;
+ let dataMatrix = false;
+ let noError = true;
+ for (let i = 0; i < twoS; i++) {
+ let value = poly.evaluateAt(this.field.exp(dataMatrix ? i + 1 : i));
+ syndromeCoefficients[syndromeCoefficients.length - 1 - i] = value;
+ if (value !== 0) {
+ noError = false;
+ }
+ }
+ if (noError) {
+ return;
+ }
+ let syndrome = new GF256Poly(this.field, syndromeCoefficients);
+ let sigmaOmega = this.runEuclideanAlgorithm(this.field.buildMonomial(twoS, 1), syndrome, twoS);
+ let sigma = sigmaOmega[0];
+ let omega = sigmaOmega[1];
+ let errorLocations = this.findErrorLocations(sigma);
+ let errorMagnitudes = this.findErrorMagnitudes(omega, errorLocations, dataMatrix);
+ for (let i = 0; i < errorLocations.length; i++) {
+ let position = received.length - 1 - this.field.log(errorLocations[i]);
+ if (position < 0) {
+ throw "ReedSolomonException Bad error location";
+ }
+ received[position] = GF256.addOrSubtract(received[position], errorMagnitudes[i]);
+ }
+ };
+ this.runEuclideanAlgorithm = function(a, b, R) {
+ if (a.Degree < b.Degree) {
+ let temp = a;
+ a = b;
+ b = temp;
+ }
+ let rLast = a;
+ let r = b;
+ let sLast = this.field.One;
+ let s = this.field.Zero;
+ let tLast = this.field.Zero;
+ let t = this.field.One;
+ while (r.Degree >= Math.floor(R / 2)) {
+ let rLastLast = rLast;
+ let sLastLast = sLast;
+ let tLastLast = tLast;
+ rLast = r;
+ sLast = s;
+ tLast = t;
+ if (rLast.Zero) {
+ throw "r_{i-1} was zero";
+ }
+ r = rLastLast;
+ let q = this.field.Zero;
+ let denominatorLeadingTerm = rLast.getCoefficient(rLast.Degree);
+ let dltInverse = this.field.inverse(denominatorLeadingTerm);
+ while (r.Degree >= rLast.Degree && !r.Zero) {
+ let degreeDiff = r.Degree - rLast.Degree;
+ let scale = this.field.multiply(r.getCoefficient(r.Degree), dltInverse);
+ q = q.addOrSubtract(this.field.buildMonomial(degreeDiff, scale));
+ r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale));
+ }
+ s = q.multiply1(sLast).addOrSubtract(sLastLast);
+ t = q.multiply1(tLast).addOrSubtract(tLastLast);
+ }
+ let sigmaTildeAtZero = t.getCoefficient(0);
+ if (sigmaTildeAtZero === 0) {
+ throw "ReedSolomonException sigmaTilde(0) was zero";
+ }
+ let inverse = this.field.inverse(sigmaTildeAtZero);
+ let sigma = t.multiply2(inverse);
+ let omega = r.multiply2(inverse);
+ return new Array(sigma, omega);
+ };
+ this.findErrorLocations = function(errorLocator) {
+ let numErrors = errorLocator.Degree;
+ if (numErrors == 1) {
+ return new Array(errorLocator.getCoefficient(1));
+ }
+ let result = new Array(numErrors);
+ let e = 0;
+ for (let i = 1; i < 256 && e < numErrors; i++) {
+ if (errorLocator.evaluateAt(i) === 0) {
+ result[e] = this.field.inverse(i);
+ e++;
+ }
+ }
+ if (e != numErrors) {
+ throw "Error locator degree does not match number of roots";
+ }
+ return result;
+ };
+ this.findErrorMagnitudes = function(errorEvaluator, errorLocations, dataMatrix) {
+ let s = errorLocations.length;
+ let result = new Array(s);
+ for (let i = 0; i < s; i++) {
+ let xiInverse = this.field.inverse(errorLocations[i]);
+ let denominator = 1;
+ for (let j = 0; j < s; j++) {
+ if (i != j) {
+ denominator = this.field.multiply(denominator, GF256.addOrSubtract(1, this.field.multiply(errorLocations[j], xiInverse)));
+ }
+ }
+ result[i] = this.field.multiply(errorEvaluator.evaluateAt(xiInverse), this.field.inverse(denominator));
+ if (dataMatrix) {
+ result[i] = this.field.multiply(result[i], xiInverse);
+ }
+ }
+ return result;
+ };
+}
+
+function GF256Poly(field, coefficients) {
+ if (coefficients === null || coefficients.length === 0) {
+ throw "System.ArgumentException";
+ }
+ this.field = field;
+ let coefficientsLength = coefficients.length;
+ if (coefficientsLength > 1 && coefficients[0] === 0) {
+ let firstNonZero = 1;
+ while (firstNonZero < coefficientsLength && coefficients[firstNonZero] === 0) {
+ firstNonZero++;
+ }
+ if (firstNonZero == coefficientsLength) {
+ this.coefficients = field.Zero.coefficients;
+ } else {
+ this.coefficients = new Array(coefficientsLength - firstNonZero);
+ for (let i = 0; i < this.coefficients.length; i++) this.coefficients[i] = 0;
+ for (let ci = 0; ci < this.coefficients.length; ci++) this.coefficients[ci] = coefficients[firstNonZero + ci];
+ }
+ } else {
+ this.coefficients = coefficients;
+ }
+ this.__defineGetter__("Zero", function() {
+ return this.coefficients[0] === 0;
+ });
+ this.__defineGetter__("Degree", function() {
+ return this.coefficients.length - 1;
+ });
+ this.__defineGetter__("Coefficients", function() {
+ return this.coefficients;
+ });
+ this.getCoefficient = function(degree) {
+ return this.coefficients[this.coefficients.length - 1 - degree];
+ };
+ this.evaluateAt = function(a) {
+ if (a === 0) {
+ return this.getCoefficient(0);
+ }
+ let size = this.coefficients.length;
+ if (a == 1) {
+ let result = 0;
+ for (let i = 0; i < size; i++) {
+ result = GF256.addOrSubtract(result, this.coefficients[i]);
+ }
+ return result;
+ }
+ let result2 = this.coefficients[0];
+ for (let i = 1; i < size; i++) {
+ result2 = GF256.addOrSubtract(this.field.multiply(a, result2), this.coefficients[i]);
+ }
+ return result2;
+ };
+ this.addOrSubtract = function(other) {
+ if (this.field != other.field) {
+ throw "GF256Polys do not have same GF256 field";
+ }
+ if (this.Zero) {
+ return other;
+ }
+ if (other.Zero) {
+ return this;
+ }
+ let smallerCoefficients = this.coefficients;
+ let largerCoefficients = other.coefficients;
+ if (smallerCoefficients.length > largerCoefficients.length) {
+ let temp = smallerCoefficients;
+ smallerCoefficients = largerCoefficients;
+ largerCoefficients = temp;
+ }
+ let sumDiff = new Array(largerCoefficients.length);
+ let lengthDiff = largerCoefficients.length - smallerCoefficients.length;
+ for (let ci = 0; ci < lengthDiff; ci++) sumDiff[ci] = largerCoefficients[ci];
+ for (let i = lengthDiff; i < largerCoefficients.length; i++) {
+ sumDiff[i] = GF256.addOrSubtract(smallerCoefficients[i - lengthDiff], largerCoefficients[i]);
+ }
+ return new GF256Poly(field, sumDiff);
+ };
+ this.multiply1 = function(other) {
+ if (this.field != other.field) {
+ throw "GF256Polys do not have same GF256 field";
+ }
+ if (this.Zero || other.Zero) {
+ return this.field.Zero;
+ }
+ let aCoefficients = this.coefficients;
+ let aLength = aCoefficients.length;
+ let bCoefficients = other.coefficients;
+ let bLength = bCoefficients.length;
+ let product = new Array(aLength + bLength - 1);
+ for (let i = 0; i < aLength; i++) {
+ let aCoeff = aCoefficients[i];
+ for (let j = 0; j < bLength; j++) {
+ product[i + j] = GF256.addOrSubtract(product[i + j], this.field.multiply(aCoeff, bCoefficients[j]));
+ }
+ }
+ return new GF256Poly(this.field, product);
+ };
+ this.multiply2 = function(scalar) {
+ if (scalar === 0) {
+ return this.field.Zero;
+ }
+ if (scalar == 1) {
+ return this;
+ }
+ let size = this.coefficients.length;
+ let product = new Array(size);
+ for (let i = 0; i < size; i++) {
+ product[i] = this.field.multiply(this.coefficients[i], scalar);
+ }
+ return new GF256Poly(this.field, product);
+ };
+ this.multiplyByMonomial = function(degree, coefficient) {
+ if (degree < 0) {
+ throw "System.ArgumentException";
+ }
+ if (coefficient === 0) {
+ return this.field.Zero;
+ }
+ let size = this.coefficients.length;
+ let product = new Array(size + degree);
+ for (let i = 0; i < product.length; i++) product[i] = 0;
+ for (let i = 0; i < size; i++) {
+ product[i] = this.field.multiply(this.coefficients[i], coefficient);
+ }
+ return new GF256Poly(this.field, product);
+ };
+ this.divide = function(other) {
+ if (this.field != other.field) {
+ throw "GF256Polys do not have same GF256 field";
+ }
+ if (other.Zero) {
+ throw "Divide by 0";
+ }
+ let quotient = this.field.Zero;
+ let remainder = this;
+ let denominatorLeadingTerm = other.getCoefficient(other.Degree);
+ let inverseDenominatorLeadingTerm = this.field.inverse(denominatorLeadingTerm);
+ while (remainder.Degree >= other.Degree && !remainder.Zero) {
+ let degreeDifference = remainder.Degree - other.Degree;
+ let scale = this.field.multiply(remainder.getCoefficient(remainder.Degree), inverseDenominatorLeadingTerm);
+ let term = other.multiplyByMonomial(degreeDifference, scale);
+ let iterationQuotient = this.field.buildMonomial(degreeDifference, scale);
+ quotient = quotient.addOrSubtract(iterationQuotient);
+ remainder = remainder.addOrSubtract(term);
+ }
+ return new Array(quotient, remainder);
+ };
+}
+
+function GF256(primitive) {
+ this.expTable = new Array(256);
+ this.logTable = new Array(256);
+ let x = 1;
+ for (let i = 0; i < 256; i++) {
+ this.expTable[i] = x;
+ x <<= 1;
+ if (x >= 256) {
+ x ^= primitive;
+ }
+ }
+ for (let i = 0; i < 255; i++) {
+ this.logTable[this.expTable[i]] = i;
+ }
+ let at0 = new Array(1);
+ at0[0] = 0;
+ this.zero = new GF256Poly(this, new Array(at0));
+ let at1 = new Array(1);
+ at1[0] = 1;
+ this.one = new GF256Poly(this, new Array(at1));
+ this.__defineGetter__("Zero", function() {
+ return this.zero;
+ });
+ this.__defineGetter__("One", function() {
+ return this.one;
+ });
+ this.buildMonomial = function(degree, coefficient) {
+ if (degree < 0) {
+ throw "System.ArgumentException";
+ }
+ if (coefficient === 0) {
+ return this.zero;
+ }
+ let coefficients = new Array(degree + 1);
+ for (let i = 0; i < coefficients.length; i++) coefficients[i] = 0;
+ coefficients[0] = coefficient;
+ return new GF256Poly(this, coefficients);
+ };
+ this.exp = function(a) {
+ return this.expTable[a];
+ };
+ this.log = function(a) {
+ if (a === 0) {
+ throw "System.ArgumentException";
+ }
+ return this.logTable[a];
+ };
+ this.inverse = function(a) {
+ if (a === 0) {
+ throw "System.ArithmeticException";
+ }
+ return this.expTable[255 - this.logTable[a]];
+ };
+ this.multiply = function(a, b) {
+ if (a === 0 || b === 0) {
+ return 0;
+ }
+ if (a == 1) {
+ return b;
+ }
+ if (b == 1) {
+ return a;
+ }
+ return this.expTable[(this.logTable[a] + this.logTable[b]) % 255];
+ };
+}
+
+GF256.QR_CODE_FIELD = new GF256(285);
+
+GF256.DATA_MATRIX_FIELD = new GF256(301);
+
+GF256.addOrSubtract = function(a, b) {
+ return a ^ b;
+};
+
+var Decoder = {};
+
+Decoder.rsDecoder = new ReedSolomonDecoder(GF256.QR_CODE_FIELD);
+
+Decoder.correctErrors = function(codewordBytes, numDataCodewords) {
+ let numCodewords = codewordBytes.length;
+ let codewordsInts = new Array(numCodewords);
+ for (let i = 0; i < numCodewords; i++) {
+ codewordsInts[i] = codewordBytes[i] & 255;
+ }
+ let numECCodewords = codewordBytes.length - numDataCodewords;
+ try {
+ Decoder.rsDecoder.decode(codewordsInts, numECCodewords);
+ } catch (rse) {
+ throw rse;
+ }
+ for (let i = 0; i < numDataCodewords; i++) {
+ codewordBytes[i] = codewordsInts[i];
+ }
+};
+
+Decoder.decode = function(bits) {
+ let parser = new BitMatrixParser(bits);
+ let version = parser.readVersion();
+ let ecLevel = parser.readFormatInformation().ErrorCorrectionLevel;
+ let codewords = parser.readCodewords();
+ let dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel);
+ let totalBytes = 0;
+ for (let i = 0; i < dataBlocks.length; i++) {
+ totalBytes += dataBlocks[i].NumDataCodewords;
+ }
+ let resultBytes = new Array(totalBytes);
+ let resultOffset = 0;
+ for (let j = 0; j < dataBlocks.length; j++) {
+ let dataBlock = dataBlocks[j];
+ let codewordBytes = dataBlock.Codewords;
+ let numDataCodewords = dataBlock.NumDataCodewords;
+ Decoder.correctErrors(codewordBytes, numDataCodewords);
+ for (let i = 0; i < numDataCodewords; i++) {
+ resultBytes[resultOffset++] = codewordBytes[i];
+ }
+ }
+ let reader = new QRCodeDataBlockReader(resultBytes, version.VersionNumber, ecLevel.Bits);
+ return reader;
+};
+
+// mozilla: Get access to a window
+
+var DevToolsServer = require("resource://devtools/server/devtools-server.js").DevToolsServer;
+
+var window = Services.wm.getMostRecentWindow(DevToolsServer.chromeWindowType);
+
+var document = window.document;
+
+var Image = window.Image;
+
+var HTML_NS = "http://www.w3.org/1999/xhtml";
+
+var qrcode = {};
+
+qrcode.callback = null;
+
+qrcode.errback = null;
+
+qrcode.decode = function(src) {
+ if (arguments.length === 0) {
+ let canvas_qr = document.getElementById("qr-canvas");
+ let context = canvas_qr.getContext("2d");
+ imgWidth = canvas_qr.width;
+ imgHeight = canvas_qr.height;
+ imgU8 = context.getImageData(0, 0, imgWidth, imgHeight).data;
+ imgU32 = new Uint32Array(imgU8.buffer);
+ qrcode.result = qrcode.process(context);
+ if (qrcode.callback !== null) {
+ qrcode.callback(qrcode.result);
+ }
+ return qrcode.result;
+ } else {
+ let image = new Image();
+ image.onload = function() {
+ // mozilla: Use HTML namespace explicitly
+ let canvas_qr = document.createElementNS(HTML_NS, "canvas");
+ let context = canvas_qr.getContext("2d");
+ let nheight = image.height;
+ let nwidth = image.width;
+ if (image.width * image.height > maxImgSize) {
+ let ir = image.width / image.height;
+ nheight = Math.sqrt(maxImgSize / ir);
+ nwidth = ir * nheight;
+ }
+ canvas_qr.width = nwidth;
+ canvas_qr.height = nheight;
+ context.drawImage(image, 0, 0, canvas_qr.width, canvas_qr.height);
+ imgWidth = canvas_qr.width;
+ imgHeight = canvas_qr.height;
+ try {
+ imgU8 = context.getImageData(0, 0, canvas_qr.width, canvas_qr.height).data;
+ imgU32 = new Uint32Array(imgU8.buffer);
+ } catch (e) {
+ qrcode.result = "Cross domain image reading not supported in your browser! Save it to your computer then drag and drop the file!";
+ if (qrcode.callback !== null) {
+ qrcode.callback(qrcode.result);
+ }
+ return;
+ }
+ try {
+ qrcode.result = qrcode.process(context);
+ if (qrcode.callback !== null) {
+ qrcode.callback(qrcode.result);
+ }
+ } catch (e) {
+ if (qrcode.errback !== null) {
+ qrcode.errback(e);
+ } else {
+ console.error(e);
+ }
+ qrcode.result = "error decoding QR Code";
+ }
+ };
+ image.src = src;
+ }
+};
+
+qrcode.isUrl = function(s) {
+ let regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
+ return regexp.test(s);
+};
+
+qrcode.decode_url = function(s) {
+ let escaped = "";
+ try {
+ escaped = escape(s);
+ } catch (e) {
+ console.log(e);
+ escaped = s;
+ }
+ let ret = "";
+ try {
+ ret = decodeURIComponent(escaped);
+ } catch (e) {
+ console.log(e);
+ ret = escaped;
+ }
+ return ret;
+};
+
+qrcode.decode_utf8 = function(s) {
+ if (qrcode.isUrl(s)) return qrcode.decode_url(s); else return s;
+};
+
+qrcode.process = function(ctx) {
+ let image = qrcode.grayScaleToBitmap(qrcode.grayscale());
+ let detector = new Detector(image);
+ let qRCodeMatrix = detector.detect();
+ let reader = Decoder.decode(qRCodeMatrix.bits);
+ let data = reader.DataByte;
+ let str = "";
+ for (let i = 0; i < data.length; i++) {
+ for (let j = 0; j < data[i].length; j++) str += String.fromCharCode(data[i][j]);
+ }
+ return qrcode.decode_utf8(str);
+};
+
+qrcode.getMiddleBrightnessPerArea = function(image) {
+ let numSqrtArea = 4;
+ let areaWidth = Math.floor(imgWidth / numSqrtArea);
+ let areaHeight = Math.floor(imgHeight / numSqrtArea);
+ let minmax = new Array(numSqrtArea);
+ for (let i = 0; i < numSqrtArea; i++) {
+ minmax[i] = new Array(numSqrtArea);
+ for (let i2 = 0; i2 < numSqrtArea; i2++) {
+ minmax[i][i2] = new Array(0, 0);
+ }
+ }
+ for (let ay = 0; ay < numSqrtArea; ay++) {
+ for (let ax = 0; ax < numSqrtArea; ax++) {
+ minmax[ax][ay][0] = 255;
+ for (let dy = 0; dy < areaHeight; dy++) {
+ for (let dx = 0; dx < areaWidth; dx++) {
+ let target = image[areaWidth * ax + dx + (areaHeight * ay + dy) * imgWidth];
+ if (target < minmax[ax][ay][0]) minmax[ax][ay][0] = target;
+ if (target > minmax[ax][ay][1]) minmax[ax][ay][1] = target;
+ }
+ }
+ }
+ }
+ let middle = new Array(numSqrtArea);
+ for (let i3 = 0; i3 < numSqrtArea; i3++) {
+ middle[i3] = new Array(numSqrtArea);
+ }
+ for (let ay = 0; ay < numSqrtArea; ay++) {
+ for (let ax = 0; ax < numSqrtArea; ax++) {
+ middle[ax][ay] = Math.floor((minmax[ax][ay][0] + minmax[ax][ay][1]) / 2);
+ }
+ }
+ return middle;
+};
+
+qrcode.grayScaleToBitmap = function(grayScale) {
+ let middle = qrcode.getMiddleBrightnessPerArea(grayScale);
+ let sqrtNumArea = middle.length;
+ let areaWidth = Math.floor(imgWidth / sqrtNumArea);
+ let areaHeight = Math.floor(imgHeight / sqrtNumArea);
+ let bitmap = new Array(imgHeight * imgWidth);
+ for (let ay = 0; ay < sqrtNumArea; ay++) {
+ for (let ax = 0; ax < sqrtNumArea; ax++) {
+ for (let dy = 0; dy < areaHeight; dy++) {
+ for (let dx = 0; dx < areaWidth; dx++) {
+ bitmap[areaWidth * ax + dx + (areaHeight * ay + dy) * imgWidth] = grayScale[areaWidth * ax + dx + (areaHeight * ay + dy) * imgWidth] < middle[ax][ay] ? true : false;
+ }
+ }
+ }
+ }
+ return bitmap;
+};
+
+qrcode.grayscale = function() {
+ let ret = new Uint8ClampedArray(imgWidth * imgHeight);
+ for (let y = 0; y < imgHeight; y++) {
+ for (let x = 0; x < imgWidth; x++) {
+ let point = x + y * imgWidth;
+ let rgba = imgU32[point];
+ let p = (rgba & 0xFF) + ((rgba >> 8) & 0xFF) + ((rgba >> 16) & 0xFF);
+ ret[x + y * imgWidth] = p / 3;
+ }
+ }
+ return ret;
+};
+
+function URShift(number, bits) {
+ if (number >= 0) return number >> bits; else return (number >> bits) + (2 << ~bits);
+}
+
+// mozilla: Add module support
+module.exports = {
+ decodeFromURI: function(src, cb, errcb) {
+ if (cb) {
+ qrcode.callback = cb;
+ }
+ if (errcb) {
+ qrcode.errback = errcb;
+ }
+ return qrcode.decode(src);
+ },
+ decodeFromCanvas: function(canvas, cb) {
+ let context = canvas.getContext("2d");
+ imgWidth = canvas.width;
+ imgHeight = canvas.height;
+ imgU8 = context.getImageData(0, 0, imgWidth, imgHeight).data;
+ imgU32 = new Uint32Array(imgU8.buffer);
+ let result = qrcode.process(context);
+ if (cb) {
+ cb(result);
+ }
+ return result;
+ }
+};
+
+var MIN_SKIP = 3;
+
+var MAX_MODULES = 57;
+
+var INTEGER_MATH_SHIFT = 8;
+
+var CENTER_QUORUM = 2;
+
+qrcode.orderBestPatterns = function(patterns) {
+ function distance(pattern1, pattern2) {
+ let xDiff = pattern1.X - pattern2.X;
+ let yDiff = pattern1.Y - pattern2.Y;
+ return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
+ }
+ function crossProductZ(pointA, pointB, pointC) {
+ let bX = pointB.x;
+ let bY = pointB.y;
+ return (pointC.x - bX) * (pointA.y - bY) - (pointC.y - bY) * (pointA.x - bX);
+ }
+ let zeroOneDistance = distance(patterns[0], patterns[1]);
+ let oneTwoDistance = distance(patterns[1], patterns[2]);
+ let zeroTwoDistance = distance(patterns[0], patterns[2]);
+ let pointA, pointB, pointC;
+ if (oneTwoDistance >= zeroOneDistance && oneTwoDistance >= zeroTwoDistance) {
+ pointB = patterns[0];
+ pointA = patterns[1];
+ pointC = patterns[2];
+ } else if (zeroTwoDistance >= oneTwoDistance && zeroTwoDistance >= zeroOneDistance) {
+ pointB = patterns[1];
+ pointA = patterns[0];
+ pointC = patterns[2];
+ } else {
+ pointB = patterns[2];
+ pointA = patterns[0];
+ pointC = patterns[1];
+ }
+ if (crossProductZ(pointA, pointB, pointC) < 0) {
+ let temp = pointA;
+ pointA = pointC;
+ pointC = temp;
+ }
+ patterns[0] = pointA;
+ patterns[1] = pointB;
+ patterns[2] = pointC;
+};
+
+function FinderPattern(posX, posY, estimatedModuleSize) {
+ this.x = posX;
+ this.y = posY;
+ this.count = 1;
+ this.estimatedModuleSize = estimatedModuleSize;
+ this.__defineGetter__("EstimatedModuleSize", function() {
+ return this.estimatedModuleSize;
+ });
+ this.__defineGetter__("Count", function() {
+ return this.count;
+ });
+ this.__defineGetter__("X", function() {
+ return this.x;
+ });
+ this.__defineGetter__("Y", function() {
+ return this.y;
+ });
+ this.incrementCount = function() {
+ this.count++;
+ };
+ this.aboutEquals = function(moduleSize, i, j) {
+ if (Math.abs(i - this.y) <= moduleSize && Math.abs(j - this.x) <= moduleSize) {
+ let moduleSizeDiff = Math.abs(moduleSize - this.estimatedModuleSize);
+ return moduleSizeDiff <= 1 || moduleSizeDiff / this.estimatedModuleSize <= 1;
+ }
+ return false;
+ };
+}
+
+function FinderPatternInfo(patternCenters) {
+ this.bottomLeft = patternCenters[0];
+ this.topLeft = patternCenters[1];
+ this.topRight = patternCenters[2];
+ this.__defineGetter__("BottomLeft", function() {
+ return this.bottomLeft;
+ });
+ this.__defineGetter__("TopLeft", function() {
+ return this.topLeft;
+ });
+ this.__defineGetter__("TopRight", function() {
+ return this.topRight;
+ });
+}
+
+function FinderPatternFinder() {
+ this.image = null;
+ this.possibleCenters = [];
+ this.hasSkipped = false;
+ this.crossCheckStateCount = new Array(0, 0, 0, 0, 0);
+ this.resultPointCallback = null;
+ this.__defineGetter__("CrossCheckStateCount", function() {
+ this.crossCheckStateCount[0] = 0;
+ this.crossCheckStateCount[1] = 0;
+ this.crossCheckStateCount[2] = 0;
+ this.crossCheckStateCount[3] = 0;
+ this.crossCheckStateCount[4] = 0;
+ return this.crossCheckStateCount;
+ });
+ this.foundPatternCross = function(stateCount) {
+ let totalModuleSize = 0;
+ for (let i = 0; i < 5; i++) {
+ let count = stateCount[i];
+ if (count === 0) {
+ return false;
+ }
+ totalModuleSize += count;
+ }
+ if (totalModuleSize < 7) {
+ return false;
+ }
+ let moduleSize = Math.floor((totalModuleSize << INTEGER_MATH_SHIFT) / 7);
+ let maxVariance = Math.floor(moduleSize / 2);
+ return Math.abs(moduleSize - (stateCount[0] << INTEGER_MATH_SHIFT)) < maxVariance && Math.abs(moduleSize - (stateCount[1] << INTEGER_MATH_SHIFT)) < maxVariance && Math.abs(3 * moduleSize - (stateCount[2] << INTEGER_MATH_SHIFT)) < 3 * maxVariance && Math.abs(moduleSize - (stateCount[3] << INTEGER_MATH_SHIFT)) < maxVariance && Math.abs(moduleSize - (stateCount[4] << INTEGER_MATH_SHIFT)) < maxVariance;
+ };
+ this.centerFromEnd = function(stateCount, end) {
+ return end - stateCount[4] - stateCount[3] - stateCount[2] / 2;
+ };
+ this.crossCheckVertical = function(startI, centerJ, maxCount, originalStateCountTotal) {
+ let image = this.image;
+ let maxI = imgHeight;
+ let stateCount = this.CrossCheckStateCount;
+ let i = startI;
+ while (i >= 0 && image[centerJ + i * imgWidth]) {
+ stateCount[2]++;
+ i--;
+ }
+ if (i < 0) {
+ return NaN;
+ }
+ while (i >= 0 && !image[centerJ + i * imgWidth] && stateCount[1] <= maxCount) {
+ stateCount[1]++;
+ i--;
+ }
+ if (i < 0 || stateCount[1] > maxCount) {
+ return NaN;
+ }
+ while (i >= 0 && image[centerJ + i * imgWidth] && stateCount[0] <= maxCount) {
+ stateCount[0]++;
+ i--;
+ }
+ if (stateCount[0] > maxCount) {
+ return NaN;
+ }
+ i = startI + 1;
+ while (i < maxI && image[centerJ + i * imgWidth]) {
+ stateCount[2]++;
+ i++;
+ }
+ if (i == maxI) {
+ return NaN;
+ }
+ while (i < maxI && !image[centerJ + i * imgWidth] && stateCount[3] < maxCount) {
+ stateCount[3]++;
+ i++;
+ }
+ if (i == maxI || stateCount[3] >= maxCount) {
+ return NaN;
+ }
+ while (i < maxI && image[centerJ + i * imgWidth] && stateCount[4] < maxCount) {
+ stateCount[4]++;
+ i++;
+ }
+ if (stateCount[4] >= maxCount) {
+ return NaN;
+ }
+ let stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
+ if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) {
+ return NaN;
+ }
+ return this.foundPatternCross(stateCount) ? this.centerFromEnd(stateCount, i) : NaN;
+ };
+ this.crossCheckHorizontal = function(startJ, centerI, maxCount, originalStateCountTotal) {
+ let image = this.image;
+ let maxJ = imgWidth;
+ let stateCount = this.CrossCheckStateCount;
+ let j = startJ;
+ while (j >= 0 && image[j + centerI * imgWidth]) {
+ stateCount[2]++;
+ j--;
+ }
+ if (j < 0) {
+ return NaN;
+ }
+ while (j >= 0 && !image[j + centerI * imgWidth] && stateCount[1] <= maxCount) {
+ stateCount[1]++;
+ j--;
+ }
+ if (j < 0 || stateCount[1] > maxCount) {
+ return NaN;
+ }
+ while (j >= 0 && image[j + centerI * imgWidth] && stateCount[0] <= maxCount) {
+ stateCount[0]++;
+ j--;
+ }
+ if (stateCount[0] > maxCount) {
+ return NaN;
+ }
+ j = startJ + 1;
+ while (j < maxJ && image[j + centerI * imgWidth]) {
+ stateCount[2]++;
+ j++;
+ }
+ if (j == maxJ) {
+ return NaN;
+ }
+ while (j < maxJ && !image[j + centerI * imgWidth] && stateCount[3] < maxCount) {
+ stateCount[3]++;
+ j++;
+ }
+ if (j == maxJ || stateCount[3] >= maxCount) {
+ return NaN;
+ }
+ while (j < maxJ && image[j + centerI * imgWidth] && stateCount[4] < maxCount) {
+ stateCount[4]++;
+ j++;
+ }
+ if (stateCount[4] >= maxCount) {
+ return NaN;
+ }
+ let stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
+ if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= originalStateCountTotal) {
+ return NaN;
+ }
+ return this.foundPatternCross(stateCount) ? this.centerFromEnd(stateCount, j) : NaN;
+ };
+ this.handlePossibleCenter = function(stateCount, i, j) {
+ let stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
+ let centerJ = this.centerFromEnd(stateCount, j);
+ let centerI = this.crossCheckVertical(i, Math.floor(centerJ), stateCount[2], stateCountTotal);
+ if (!isNaN(centerI)) {
+ centerJ = this.crossCheckHorizontal(Math.floor(centerJ), Math.floor(centerI), stateCount[2], stateCountTotal);
+ if (!isNaN(centerJ)) {
+ let estimatedModuleSize = stateCountTotal / 7;
+ let found = false;
+ let max = this.possibleCenters.length;
+ for (let index = 0; index < max; index++) {
+ let center = this.possibleCenters[index];
+ if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) {
+ center.incrementCount();
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ let point = new FinderPattern(centerJ, centerI, estimatedModuleSize);
+ this.possibleCenters.push(point);
+ if (this.resultPointCallback !== null) {
+ this.resultPointCallback.foundPossibleResultPoint(point);
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ };
+ this.selectBestPatterns = function() {
+ let startSize = this.possibleCenters.length;
+ if (startSize < 3) {
+ throw Error("Couldn't find enough finder patterns");
+ }
+ if (startSize > 3) {
+ let totalModuleSize = 0;
+ let square = 0;
+ for (let i = 0; i < startSize; i++) {
+ let centerValue = this.possibleCenters[i].EstimatedModuleSize;
+ totalModuleSize += centerValue;
+ square += centerValue * centerValue;
+ }
+ let average = totalModuleSize / startSize;
+ this.possibleCenters.sort(function(center1, center2) {
+ let dA = Math.abs(center2.EstimatedModuleSize - average);
+ let dB = Math.abs(center1.EstimatedModuleSize - average);
+ if (dA < dB) {
+ return -1;
+ } else if (dA == dB) {
+ return 0;
+ } else {
+ return 1;
+ }
+ });
+ let stdDev = Math.sqrt(square / startSize - average * average);
+ let limit = Math.max(0.2 * average, stdDev);
+ for (let i = 0; i < this.possibleCenters.length && this.possibleCenters.length > 3; i++) {
+ let pattern = this.possibleCenters[i];
+ if (Math.abs(pattern.EstimatedModuleSize - average) > limit) {
+ // mozilla: use splice instead
+ this.possibleCenters.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ if (this.possibleCenters.length > 3) {
+ this.possibleCenters.sort(function(a, b) {
+ if (a.count > b.count) {
+ return -1;
+ }
+ if (a.count < b.count) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+ return new Array(this.possibleCenters[0], this.possibleCenters[1], this.possibleCenters[2]);
+ };
+ this.findRowSkip = function() {
+ let max = this.possibleCenters.length;
+ if (max <= 1) {
+ return 0;
+ }
+ let firstConfirmedCenter = null;
+ for (let i = 0; i < max; i++) {
+ let center = this.possibleCenters[i];
+ if (center.Count >= CENTER_QUORUM) {
+ if (firstConfirmedCenter === null) {
+ firstConfirmedCenter = center;
+ } else {
+ this.hasSkipped = true;
+ return Math.floor((Math.abs(firstConfirmedCenter.X - center.X) - Math.abs(firstConfirmedCenter.Y - center.Y)) / 2);
+ }
+ }
+ }
+ return 0;
+ };
+ this.haveMultiplyConfirmedCenters = function() {
+ let confirmedCount = 0;
+ let totalModuleSize = 0;
+ let max = this.possibleCenters.length;
+ for (let i = 0; i < max; i++) {
+ let pattern = this.possibleCenters[i];
+ if (pattern.Count >= CENTER_QUORUM) {
+ confirmedCount++;
+ totalModuleSize += pattern.EstimatedModuleSize;
+ }
+ }
+ if (confirmedCount < 3) {
+ return false;
+ }
+ let average = totalModuleSize / max;
+ let totalDeviation = 0;
+ for (let i = 0; i < max; i++) {
+ let pattern = this.possibleCenters[i];
+ totalDeviation += Math.abs(pattern.EstimatedModuleSize - average);
+ }
+ return totalDeviation <= 0.05 * totalModuleSize;
+ };
+ this.findFinderPattern = function(image) {
+ let tryHarder = false;
+ this.image = image;
+ let maxI = imgHeight;
+ let maxJ = imgWidth;
+ let iSkip = Math.floor(3 * maxI / (4 * MAX_MODULES));
+ if (iSkip < MIN_SKIP || tryHarder) {
+ iSkip = MIN_SKIP;
+ }
+ let done = false;
+ let stateCount = new Array(5);
+ for (let i = iSkip - 1; i < maxI && !done; i += iSkip) {
+ stateCount[0] = 0;
+ stateCount[1] = 0;
+ stateCount[2] = 0;
+ stateCount[3] = 0;
+ stateCount[4] = 0;
+ let currentState = 0;
+ for (let j = 0; j < maxJ; j++) {
+ if (image[j + i * imgWidth]) {
+ if ((currentState & 1) == 1) {
+ currentState++;
+ }
+ stateCount[currentState]++;
+ } else {
+ if ((currentState & 1) === 0) {
+ if (currentState == 4) {
+ if (this.foundPatternCross(stateCount)) {
+ let confirmed = this.handlePossibleCenter(stateCount, i, j);
+ if (confirmed) {
+ iSkip = 2;
+ if (this.hasSkipped) {
+ done = this.haveMultiplyConfirmedCenters();
+ } else {
+ let rowSkip = this.findRowSkip();
+ if (rowSkip > stateCount[2]) {
+ i += rowSkip - stateCount[2] - iSkip;
+ j = maxJ - 1;
+ }
+ }
+ } else {
+ do {
+ j++;
+ } while (j < maxJ && !image[j + i * imgWidth]);
+ j--;
+ }
+ currentState = 0;
+ stateCount[0] = 0;
+ stateCount[1] = 0;
+ stateCount[2] = 0;
+ stateCount[3] = 0;
+ stateCount[4] = 0;
+ } else {
+ stateCount[0] = stateCount[2];
+ stateCount[1] = stateCount[3];
+ stateCount[2] = stateCount[4];
+ stateCount[3] = 1;
+ stateCount[4] = 0;
+ currentState = 3;
+ }
+ } else {
+ stateCount[++currentState]++;
+ }
+ } else {
+ stateCount[currentState]++;
+ }
+ }
+ }
+ if (this.foundPatternCross(stateCount)) {
+ let confirmed = this.handlePossibleCenter(stateCount, i, maxJ);
+ if (confirmed) {
+ iSkip = stateCount[0];
+ if (this.hasSkipped) {
+ done = this.haveMultiplyConfirmedCenters();
+ }
+ }
+ }
+ }
+ let patternInfo = this.selectBestPatterns();
+ qrcode.orderBestPatterns(patternInfo);
+ return new FinderPatternInfo(patternInfo);
+ };
+}
+
+function AlignmentPattern(posX, posY, estimatedModuleSize) {
+ this.x = posX;
+ this.y = posY;
+ this.count = 1;
+ this.estimatedModuleSize = estimatedModuleSize;
+ this.__defineGetter__("EstimatedModuleSize", function() {
+ return this.estimatedModuleSize;
+ });
+ this.__defineGetter__("Count", function() {
+ return this.count;
+ });
+ this.__defineGetter__("X", function() {
+ return Math.floor(this.x);
+ });
+ this.__defineGetter__("Y", function() {
+ return Math.floor(this.y);
+ });
+ this.incrementCount = function() {
+ this.count++;
+ };
+ this.aboutEquals = function(moduleSize, i, j) {
+ if (Math.abs(i - this.y) <= moduleSize && Math.abs(j - this.x) <= moduleSize) {
+ let moduleSizeDiff = Math.abs(moduleSize - this.estimatedModuleSize);
+ return moduleSizeDiff <= 1 || moduleSizeDiff / this.estimatedModuleSize <= 1;
+ }
+ return false;
+ };
+}
+
+function AlignmentPatternFinder(image, startX, startY, width, height, moduleSize, resultPointCallback) {
+ this.image = image;
+ this.possibleCenters = [];
+ this.startX = startX;
+ this.startY = startY;
+ this.width = width;
+ this.height = height;
+ this.moduleSize = moduleSize;
+ this.crossCheckStateCount = new Array(0, 0, 0);
+ this.resultPointCallback = resultPointCallback;
+ this.centerFromEnd = function(stateCount, end) {
+ return end - stateCount[2] - stateCount[1] / 2;
+ };
+ this.foundPatternCross = function(stateCount) {
+ let moduleSize = this.moduleSize;
+ let maxVariance = moduleSize / 2;
+ for (let i = 0; i < 3; i++) {
+ if (Math.abs(moduleSize - stateCount[i]) >= maxVariance) {
+ return false;
+ }
+ }
+ return true;
+ };
+ this.crossCheckVertical = function(startI, centerJ, maxCount, originalStateCountTotal) {
+ let image = this.image;
+ let maxI = imgHeight;
+ let stateCount = this.crossCheckStateCount;
+ stateCount[0] = 0;
+ stateCount[1] = 0;
+ stateCount[2] = 0;
+ let i = startI;
+ while (i >= 0 && image[centerJ + i * imgWidth] && stateCount[1] <= maxCount) {
+ stateCount[1]++;
+ i--;
+ }
+ if (i < 0 || stateCount[1] > maxCount) {
+ return NaN;
+ }
+ while (i >= 0 && !image[centerJ + i * imgWidth] && stateCount[0] <= maxCount) {
+ stateCount[0]++;
+ i--;
+ }
+ if (stateCount[0] > maxCount) {
+ return NaN;
+ }
+ i = startI + 1;
+ while (i < maxI && image[centerJ + i * imgWidth] && stateCount[1] <= maxCount) {
+ stateCount[1]++;
+ i++;
+ }
+ if (i == maxI || stateCount[1] > maxCount) {
+ return NaN;
+ }
+ while (i < maxI && !image[centerJ + i * imgWidth] && stateCount[2] <= maxCount) {
+ stateCount[2]++;
+ i++;
+ }
+ if (stateCount[2] > maxCount) {
+ return NaN;
+ }
+ let stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2];
+ if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) {
+ return NaN;
+ }
+ return this.foundPatternCross(stateCount) ? this.centerFromEnd(stateCount, i) : NaN;
+ };
+ this.handlePossibleCenter = function(stateCount, i, j) {
+ let stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2];
+ let centerJ = this.centerFromEnd(stateCount, j);
+ let centerI = this.crossCheckVertical(i, Math.floor(centerJ), 2 * stateCount[1], stateCountTotal);
+ if (!isNaN(centerI)) {
+ let estimatedModuleSize = (stateCount[0] + stateCount[1] + stateCount[2]) / 3;
+ let max = this.possibleCenters.length;
+ for (let index = 0; index < max; index++) {
+ let center = this.possibleCenters[index];
+ if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) {
+ return new AlignmentPattern(centerJ, centerI, estimatedModuleSize);
+ }
+ }
+ let point = new AlignmentPattern(centerJ, centerI, estimatedModuleSize);
+ this.possibleCenters.push(point);
+ if (this.resultPointCallback !== null) {
+ this.resultPointCallback.foundPossibleResultPoint(point);
+ }
+ }
+ return null;
+ };
+ this.find = function() {
+ let startX = this.startX;
+ let height = this.height;
+ let maxJ = startX + width;
+ let middleI = startY + (height >> 1);
+ let stateCount = new Array(0, 0, 0);
+ for (let iGen = 0; iGen < height; iGen++) {
+ let i = middleI + ((iGen & 1) === 0 ? iGen + 1 >> 1 : -(iGen + 1 >> 1));
+ stateCount[0] = 0;
+ stateCount[1] = 0;
+ stateCount[2] = 0;
+ let j = startX;
+ while (j < maxJ && !image[j + imgWidth * i]) {
+ j++;
+ }
+ let currentState = 0;
+ while (j < maxJ) {
+ if (image[j + i * imgWidth]) {
+ if (currentState == 1) {
+ stateCount[currentState]++;
+ } else {
+ if (currentState == 2) {
+ if (this.foundPatternCross(stateCount)) {
+ let confirmed = this.handlePossibleCenter(stateCount, i, j);
+ if (confirmed !== null) {
+ return confirmed;
+ }
+ }
+ stateCount[0] = stateCount[2];
+ stateCount[1] = 1;
+ stateCount[2] = 0;
+ currentState = 1;
+ } else {
+ stateCount[++currentState]++;
+ }
+ }
+ } else {
+ if (currentState == 1) {
+ currentState++;
+ }
+ stateCount[currentState]++;
+ }
+ j++;
+ }
+ if (this.foundPatternCross(stateCount)) {
+ let confirmed = this.handlePossibleCenter(stateCount, i, maxJ);
+ if (confirmed !== null) {
+ return confirmed;
+ }
+ }
+ }
+ if (this.possibleCenters.length !== 0) {
+ return this.possibleCenters[0];
+ }
+ throw "Couldn't find enough alignment patterns";
+ };
+}
+
+function QRCodeDataBlockReader(blocks, version, numErrorCorrectionCode) {
+ this.blockPointer = 0;
+ this.bitPointer = 7;
+ this.dataLength = 0;
+ this.blocks = blocks;
+ this.numErrorCorrectionCode = numErrorCorrectionCode;
+ if (version <= 9) this.dataLengthMode = 0; else if (version >= 10 && version <= 26) this.dataLengthMode = 1; else if (version >= 27 && version <= 40) this.dataLengthMode = 2;
+ this.getNextBits = function(numBits) {
+ let bits = 0;
+ if (numBits < this.bitPointer + 1) {
+ let mask = 0;
+ for (let i = 0; i < numBits; i++) {
+ mask += 1 << i;
+ }
+ mask <<= this.bitPointer - numBits + 1;
+ bits = (this.blocks[this.blockPointer] & mask) >> this.bitPointer - numBits + 1;
+ this.bitPointer -= numBits;
+ return bits;
+ } else if (numBits < this.bitPointer + 1 + 8) {
+ let mask1 = 0;
+ for (let i = 0; i < this.bitPointer + 1; i++) {
+ mask1 += 1 << i;
+ }
+ bits = (this.blocks[this.blockPointer] & mask1) << numBits - (this.bitPointer + 1);
+ this.blockPointer++;
+ bits += this.blocks[this.blockPointer] >> 8 - (numBits - (this.bitPointer + 1));
+ this.bitPointer = this.bitPointer - numBits % 8;
+ if (this.bitPointer < 0) {
+ this.bitPointer = 8 + this.bitPointer;
+ }
+ return bits;
+ } else if (numBits < this.bitPointer + 1 + 16) {
+ let mask1 = 0;
+ let mask3 = 0;
+ for (let i = 0; i < this.bitPointer + 1; i++) {
+ mask1 += 1 << i;
+ }
+ let bitsFirstBlock = (this.blocks[this.blockPointer] & mask1) << numBits - (this.bitPointer + 1);
+ this.blockPointer++;
+ let bitsSecondBlock = this.blocks[this.blockPointer] << numBits - (this.bitPointer + 1 + 8);
+ this.blockPointer++;
+ for (let i = 0; i < numBits - (this.bitPointer + 1 + 8); i++) {
+ mask3 += 1 << i;
+ }
+ mask3 <<= 8 - (numBits - (this.bitPointer + 1 + 8));
+ let bitsThirdBlock = (this.blocks[this.blockPointer] & mask3) >> 8 - (numBits - (this.bitPointer + 1 + 8));
+ bits = bitsFirstBlock + bitsSecondBlock + bitsThirdBlock;
+ this.bitPointer = this.bitPointer - (numBits - 8) % 8;
+ if (this.bitPointer < 0) {
+ this.bitPointer = 8 + this.bitPointer;
+ }
+ return bits;
+ } else {
+ return 0;
+ }
+ };
+ this.NextMode = function() {
+ if (this.blockPointer > this.blocks.length - this.numErrorCorrectionCode - 2) return 0; else return this.getNextBits(4);
+ };
+ this.getDataLength = function(modeIndicator) {
+ let index = 0;
+ while (true) {
+ if (modeIndicator >> index == 1) break;
+ index++;
+ }
+ return this.getNextBits(sizeOfDataLengthInfo[this.dataLengthMode][index]);
+ };
+ this.getRomanAndFigureString = function(dataLength) {
+ var length = dataLength;
+ let intData = 0;
+ let strData = "";
+ let tableRomanAndFigure = new Array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "$", "%", "*", "+", "-", ".", "/", ":");
+ do {
+ if (length > 1) {
+ intData = this.getNextBits(11);
+ let firstLetter = Math.floor(intData / 45);
+ let secondLetter = intData % 45;
+ strData += tableRomanAndFigure[firstLetter];
+ strData += tableRomanAndFigure[secondLetter];
+ length -= 2;
+ } else if (length == 1) {
+ intData = this.getNextBits(6);
+ strData += tableRomanAndFigure[intData];
+ length -= 1;
+ }
+ } while (length > 0);
+ return strData;
+ };
+ this.getFigureString = function(dataLength) {
+ var length = dataLength;
+ let intData = 0;
+ let strData = "";
+ do {
+ if (length >= 3) {
+ intData = this.getNextBits(10);
+ if (intData < 100) strData += "0";
+ if (intData < 10) strData += "0";
+ length -= 3;
+ } else if (length == 2) {
+ intData = this.getNextBits(7);
+ if (intData < 10) strData += "0";
+ length -= 2;
+ } else if (length == 1) {
+ intData = this.getNextBits(4);
+ length -= 1;
+ }
+ strData += intData;
+ } while (length > 0);
+ return strData;
+ };
+ this.get8bitByteArray = function(dataLength) {
+ var length = dataLength;
+ let intData = 0;
+ let output = [];
+ do {
+ intData = this.getNextBits(8);
+ output.push(intData);
+ length--;
+ } while (length > 0);
+ return output;
+ };
+ this.getKanjiString = function(dataLength) {
+ var length = dataLength;
+ let intData = 0;
+ let unicodeString = "";
+ do {
+ intData = this.getNextBits(13);
+ let lowerByte = intData % 192;
+ let higherByte = intData / 192;
+ let tempWord = (higherByte << 8) + lowerByte;
+ let shiftjisWord = 0;
+ if (tempWord + 33088 <= 40956) {
+ shiftjisWord = tempWord + 33088;
+ } else {
+ shiftjisWord = tempWord + 49472;
+ }
+ unicodeString += String.fromCharCode(shiftjisWord);
+ length--;
+ } while (length > 0);
+ return unicodeString;
+ };
+ this.__defineGetter__("DataByte", function() {
+ let output = [];
+ let MODE_NUMBER = 1;
+ let MODE_ROMAN_AND_NUMBER = 2;
+ let MODE_8BIT_BYTE = 4;
+ let MODE_KANJI = 8;
+ do {
+ let mode = this.NextMode();
+ if (mode === 0) {
+ if (output.length > 0) break; else throw "Empty data block";
+ }
+ if (mode != MODE_NUMBER && mode != MODE_ROMAN_AND_NUMBER && mode != MODE_8BIT_BYTE && mode != MODE_KANJI) {
+ throw "Invalid mode: " + mode + " in (block:" + this.blockPointer + " bit:" + this.bitPointer + ")";
+ }
+ let dataLength = this.getDataLength(mode);
+ if (dataLength < 1) {
+ throw "Invalid data length: " + dataLength;
+ }
+ let temp_str;
+ let ta;
+ switch (mode) {
+ case MODE_NUMBER:
+ temp_str = this.getFigureString(dataLength);
+ ta = new Array(temp_str.length);
+ for (let j = 0; j < temp_str.length; j++) ta[j] = temp_str.charCodeAt(j);
+ output.push(ta);
+ break;
+
+ case MODE_ROMAN_AND_NUMBER:
+ temp_str = this.getRomanAndFigureString(dataLength);
+ ta = new Array(temp_str.length);
+ for (let j = 0; j < temp_str.length; j++) ta[j] = temp_str.charCodeAt(j);
+ output.push(ta);
+ break;
+
+ case MODE_8BIT_BYTE:
+ let temp_sbyteArray3 = this.get8bitByteArray(dataLength);
+ output.push(temp_sbyteArray3);
+ break;
+
+ case MODE_KANJI:
+ temp_str = this.getKanjiString(dataLength);
+ output.push(temp_str);
+ break;
+ }
+ } while (true);
+ return output;
+ });
+}
diff --git a/devtools/shared/qrcode/decoder/moz.build b/devtools/shared/qrcode/decoder/moz.build
new file mode 100644
index 0000000000..4442a2e906
--- /dev/null
+++ b/devtools/shared/qrcode/decoder/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ 'index.js',
+)
diff --git a/devtools/shared/qrcode/encoder/LICENSE b/devtools/shared/qrcode/encoder/LICENSE
new file mode 100644
index 0000000000..a93630a3ba
--- /dev/null
+++ b/devtools/shared/qrcode/encoder/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2009 Kazuhiko Arase
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/devtools/shared/qrcode/encoder/index.js b/devtools/shared/qrcode/encoder/index.js
new file mode 100644
index 0000000000..487d0f471a
--- /dev/null
+++ b/devtools/shared/qrcode/encoder/index.js
@@ -0,0 +1,1674 @@
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = function() {
+
+ //---------------------------------------------------------------------
+ // qrcode
+ //---------------------------------------------------------------------
+
+ /**
+ * qrcode
+ * @param typeNumber 1 to 10
+ * @param errorCorrectLevel 'L','M','Q','H'
+ */
+ var qrcode = function(typeNumber, errorCorrectLevel) {
+
+ var PAD0 = 0xEC;
+ var PAD1 = 0x11;
+
+ var _typeNumber = typeNumber;
+ var _errorCorrectLevel = QRErrorCorrectLevel[errorCorrectLevel];
+ var _modules = null;
+ var _moduleCount = 0;
+ var _dataCache = null;
+ var _dataList = new Array();
+
+ var _this = {};
+
+ var makeImpl = function(test, maskPattern) {
+
+ _moduleCount = _typeNumber * 4 + 17;
+ _modules = function(moduleCount) {
+ var modules = new Array(moduleCount);
+ for (var row = 0; row < moduleCount; row += 1) {
+ modules[row] = new Array(moduleCount);
+ for (var col = 0; col < moduleCount; col += 1) {
+ modules[row][col] = null;
+ }
+ }
+ return modules;
+ }(_moduleCount);
+
+ setupPositionProbePattern(0, 0);
+ setupPositionProbePattern(_moduleCount - 7, 0);
+ setupPositionProbePattern(0, _moduleCount - 7);
+ setupPositionAdjustPattern();
+ setupTimingPattern();
+ setupTypeInfo(test, maskPattern);
+
+ if (_typeNumber >= 7) {
+ setupTypeNumber(test);
+ }
+
+ if (_dataCache == null) {
+ _dataCache = createData(_typeNumber, _errorCorrectLevel, _dataList);
+ }
+
+ mapData(_dataCache, maskPattern);
+ };
+
+ var setupPositionProbePattern = function(row, col) {
+
+ for (var r = -1; r <= 7; r += 1) {
+
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+ for (var c = -1; c <= 7; c += 1) {
+
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+ if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
+ || (0 <= c && c <= 6 && (r == 0 || r == 6) )
+ || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ };
+
+ var getBestMaskPattern = function() {
+
+ var minLostPoint = 0;
+ var pattern = 0;
+
+ for (var i = 0; i < 8; i += 1) {
+
+ makeImpl(true, i);
+
+ var lostPoint = QRUtil.getLostPoint(_this);
+
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+
+ return pattern;
+ };
+
+ var setupTimingPattern = function() {
+
+ for (var r = 8; r < _moduleCount - 8; r += 1) {
+ if (_modules[r][6] != null) {
+ continue;
+ }
+ _modules[r][6] = (r % 2 == 0);
+ }
+
+ for (var c = 8; c < _moduleCount - 8; c += 1) {
+ if (_modules[6][c] != null) {
+ continue;
+ }
+ _modules[6][c] = (c % 2 == 0);
+ }
+ };
+
+ var setupPositionAdjustPattern = function() {
+
+ var pos = QRUtil.getPatternPosition(_typeNumber);
+
+ for (var i = 0; i < pos.length; i += 1) {
+
+ for (var j = 0; j < pos.length; j += 1) {
+
+ var row = pos[i];
+ var col = pos[j];
+
+ if (_modules[row][col] != null) {
+ continue;
+ }
+
+ for (var r = -2; r <= 2; r += 1) {
+
+ for (var c = -2; c <= 2; c += 1) {
+
+ if (r == -2 || r == 2 || c == -2 || c == 2
+ || (r == 0 && c == 0) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var setupTypeNumber = function(test) {
+
+ var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
+ }
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ };
+
+ var setupTypeInfo = function(test, maskPattern) {
+
+ var data = (_errorCorrectLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+
+ // vertical
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 6) {
+ _modules[i][8] = mod;
+ } else if (i < 8) {
+ _modules[i + 1][8] = mod;
+ } else {
+ _modules[_moduleCount - 15 + i][8] = mod;
+ }
+ }
+
+ // horizontal
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 8) {
+ _modules[8][_moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ _modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ _modules[8][15 - i - 1] = mod;
+ }
+ }
+
+ // fixed module
+ _modules[_moduleCount - 8][8] = (!test);
+ };
+
+ var mapData = function(data, maskPattern) {
+
+ var inc = -1;
+ var row = _moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+ for (var col = _moduleCount - 1; col > 0; col -= 2) {
+
+ if (col == 6) col -= 1;
+
+ while (true) {
+
+ for (var c = 0; c < 2; c += 1) {
+
+ if (_modules[row][col - c] == null) {
+
+ var dark = false;
+
+ if (byteIndex < data.length) {
+ dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
+ }
+
+ var mask = maskFunc(row, col - c);
+
+ if (mask) {
+ dark = !dark;
+ }
+
+ _modules[row][col - c] = dark;
+ bitIndex -= 1;
+
+ if (bitIndex == -1) {
+ byteIndex += 1;
+ bitIndex = 7;
+ }
+ }
+ }
+
+ row += inc;
+
+ if (row < 0 || _moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ };
+
+ var createBytes = function(buffer, rsBlocks) {
+
+ var offset = 0;
+
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+
+ dcdata[r] = new Array(dcCount);
+
+ for (var i = 0; i < dcdata[r].length; i += 1) {
+ dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+ }
+ offset += dcCount;
+
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i += 1) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
+ }
+ }
+
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+
+ var data = new Array(totalCodeCount);
+ var index = 0;
+
+ for (var i = 0; i < maxDcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < dcdata[r].length) {
+ data[index] = dcdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ for (var i = 0; i < maxEcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < ecdata[r].length) {
+ data[index] = ecdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ return data;
+ };
+
+ var createData = function(typeNumber, errorCorrectLevel, dataList) {
+
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
+
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < dataList.length; i += 1) {
+ var data = dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+ data.write(buffer);
+ }
+
+ // calc num max data.
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw new Error('code length overflow. ('
+ + buffer.getLengthInBits()
+ + '>'
+ + totalDataCount * 8
+ + ')');
+ }
+
+ // end code
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+
+ // padding
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+
+ // padding
+ while (true) {
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD0, 8);
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD1, 8);
+ }
+
+ return createBytes(buffer, rsBlocks);
+ };
+
+ _this.addData = function(data) {
+ var newData = qr8BitByte(data);
+ _dataList.push(newData);
+ _dataCache = null;
+ };
+
+ _this.isDark = function(row, col) {
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+ throw new Error(row + ',' + col);
+ }
+ return _modules[row][col];
+ };
+
+ _this.getModuleCount = function() {
+ return _moduleCount;
+ };
+
+ _this.make = function() {
+ makeImpl(false, getBestMaskPattern() );
+ };
+
+ _this.createTableTag = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var qrHtml = '';
+
+ qrHtml += '<table style="';
+ qrHtml += ' border-width: 0px; border-style: none;';
+ qrHtml += ' border-collapse: collapse;';
+ qrHtml += ' padding: 0px; margin: ' + margin + 'px;';
+ qrHtml += '">';
+ qrHtml += '<tbody>';
+
+ for (var r = 0; r < _this.getModuleCount(); r += 1) {
+
+ qrHtml += '<tr>';
+
+ for (var c = 0; c < _this.getModuleCount(); c += 1) {
+ qrHtml += '<td style="';
+ qrHtml += ' border-width: 0px; border-style: none;';
+ qrHtml += ' border-collapse: collapse;';
+ qrHtml += ' padding: 0px; margin: 0px;';
+ qrHtml += ' width: ' + cellSize + 'px;';
+ qrHtml += ' height: ' + cellSize + 'px;';
+ qrHtml += ' background-color: ';
+ qrHtml += _this.isDark(r, c)? '#000000' : '#ffffff';
+ qrHtml += ';';
+ qrHtml += '"/>';
+ }
+
+ qrHtml += '</tr>';
+ }
+
+ qrHtml += '</tbody>';
+ qrHtml += '</table>';
+
+ return qrHtml;
+ };
+
+ _this.createImgTag = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ return createImgTag(size, size, function(x, y) {
+ if (min <= x && x < max && min <= y && y < max) {
+ var c = Math.floor( (x - min) / cellSize);
+ var r = Math.floor( (y - min) / cellSize);
+ return _this.isDark(r, c)? 0 : 1;
+ } else {
+ return 1;
+ }
+ } );
+ };
+
+ _this.createImgData = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var base64 = createGifData(size, size, function(x, y) {
+ if (min <= x && x < max && min <= y && y < max) {
+ var c = Math.floor( (x - min) / cellSize);
+ var r = Math.floor( (y - min) / cellSize);
+ return _this.isDark(r, c)? 0 : 1;
+ } else {
+ return 1;
+ }
+ } );
+
+ return {
+ src: 'data:image/gif;base64,' + base64,
+ width: size,
+ height: size
+ };
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrcode.stringToBytes
+ //---------------------------------------------------------------------
+
+ qrcode.stringToBytes = function(s) {
+ var bytes = new Array();
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ bytes.push(c & 0xff);
+ }
+ return bytes;
+ };
+
+ //---------------------------------------------------------------------
+ // qrcode.createStringToBytes
+ //---------------------------------------------------------------------
+
+ /**
+ * @param unicodeData base64 string of byte array.
+ * [16bit Unicode],[16bit Bytes], ...
+ * @param numChars
+ */
+ qrcode.createStringToBytes = function(unicodeData, numChars) {
+
+ // create conversion map.
+
+ var unicodeMap = function() {
+
+ var bin = base64DecodeInputStream(unicodeData);
+ var read = function() {
+ var b = bin.read();
+ if (b == -1) throw new Error();
+ return b;
+ };
+
+ var count = 0;
+ var unicodeMap = {};
+ while (true) {
+ var b0 = bin.read();
+ if (b0 == -1) break;
+ var b1 = read();
+ var b2 = read();
+ var b3 = read();
+ var k = String.fromCharCode( (b0 << 8) | b1);
+ var v = (b2 << 8) | b3;
+ unicodeMap[k] = v;
+ count += 1;
+ }
+ if (count != numChars) {
+ throw new Error(count + ' != ' + numChars);
+ }
+
+ return unicodeMap;
+ }();
+
+ var unknownChar = '?'.charCodeAt(0);
+
+ return function(s) {
+ var bytes = new Array();
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ if (c < 128) {
+ bytes.push(c);
+ } else {
+ var b = unicodeMap[s.charAt(i)];
+ if (typeof b == 'number') {
+ if ( (b & 0xff) == b) {
+ // 1byte
+ bytes.push(b);
+ } else {
+ // 2bytes
+ bytes.push(b >>> 8);
+ bytes.push(b & 0xff);
+ }
+ } else {
+ bytes.push(unknownChar);
+ }
+ }
+ }
+ return bytes;
+ };
+ };
+
+ //---------------------------------------------------------------------
+ // QRMode
+ //---------------------------------------------------------------------
+
+ var QRMode = {
+ MODE_NUMBER : 1 << 0,
+ MODE_ALPHA_NUM : 1 << 1,
+ MODE_8BIT_BYTE : 1 << 2,
+ MODE_KANJI : 1 << 3
+ };
+
+ //---------------------------------------------------------------------
+ // QRErrorCorrectLevel
+ //---------------------------------------------------------------------
+
+ var QRErrorCorrectLevel = {
+ L : 1,
+ M : 0,
+ Q : 3,
+ H : 2
+ };
+ // mozilla: Add module support
+ exports.QRErrorCorrectLevel = QRErrorCorrectLevel;
+
+ //---------------------------------------------------------------------
+ // QRMaskPattern
+ //---------------------------------------------------------------------
+
+ var QRMaskPattern = {
+ PATTERN000 : 0,
+ PATTERN001 : 1,
+ PATTERN010 : 2,
+ PATTERN011 : 3,
+ PATTERN100 : 4,
+ PATTERN101 : 5,
+ PATTERN110 : 6,
+ PATTERN111 : 7
+ };
+
+ //---------------------------------------------------------------------
+ // QRUtil
+ //---------------------------------------------------------------------
+
+ var QRUtil = function() {
+
+ var PATTERN_POSITION_TABLE = [
+ [],
+ [6, 18],
+ [6, 22],
+ [6, 26],
+ [6, 30],
+ [6, 34],
+ [6, 22, 38],
+ [6, 24, 42],
+ [6, 26, 46],
+ [6, 28, 50],
+ [6, 30, 54],
+ [6, 32, 58],
+ [6, 34, 62],
+ [6, 26, 46, 66],
+ [6, 26, 48, 70],
+ [6, 26, 50, 74],
+ [6, 30, 54, 78],
+ [6, 30, 56, 82],
+ [6, 30, 58, 86],
+ [6, 34, 62, 90],
+ [6, 28, 50, 72, 94],
+ [6, 26, 50, 74, 98],
+ [6, 30, 54, 78, 102],
+ [6, 28, 54, 80, 106],
+ [6, 32, 58, 84, 110],
+ [6, 30, 58, 86, 114],
+ [6, 34, 62, 90, 118],
+ [6, 26, 50, 74, 98, 122],
+ [6, 30, 54, 78, 102, 126],
+ [6, 26, 52, 78, 104, 130],
+ [6, 30, 56, 82, 108, 134],
+ [6, 34, 60, 86, 112, 138],
+ [6, 30, 58, 86, 114, 142],
+ [6, 34, 62, 90, 118, 146],
+ [6, 30, 54, 78, 102, 126, 150],
+ [6, 24, 50, 76, 102, 128, 154],
+ [6, 28, 54, 80, 106, 132, 158],
+ [6, 32, 58, 84, 110, 136, 162],
+ [6, 26, 54, 82, 110, 138, 166],
+ [6, 30, 58, 86, 114, 142, 170]
+ ];
+ var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);
+ var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);
+ var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
+
+ var _this = {};
+
+ var getBCHDigit = function(data) {
+ var digit = 0;
+ while (data != 0) {
+ digit += 1;
+ data >>>= 1;
+ }
+ return digit;
+ };
+
+ _this.getBCHTypeInfo = function(data) {
+ var d = data << 10;
+ while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
+ d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) );
+ }
+ return ( (data << 10) | d) ^ G15_MASK;
+ };
+
+ _this.getBCHTypeNumber = function(data) {
+ var d = data << 12;
+ while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
+ d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) );
+ }
+ return (data << 12) | d;
+ };
+
+ _this.getPatternPosition = function(typeNumber) {
+ return PATTERN_POSITION_TABLE[typeNumber - 1];
+ };
+
+ _this.getMaskFunction = function(maskPattern) {
+
+ switch (maskPattern) {
+
+ case QRMaskPattern.PATTERN000 :
+ return function(i, j) { return (i + j) % 2 == 0; };
+ case QRMaskPattern.PATTERN001 :
+ return function(i, j) { return i % 2 == 0; };
+ case QRMaskPattern.PATTERN010 :
+ return function(i, j) { return j % 3 == 0; };
+ case QRMaskPattern.PATTERN011 :
+ return function(i, j) { return (i + j) % 3 == 0; };
+ case QRMaskPattern.PATTERN100 :
+ return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; };
+ case QRMaskPattern.PATTERN101 :
+ return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };
+ case QRMaskPattern.PATTERN110 :
+ return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; };
+ case QRMaskPattern.PATTERN111 :
+ return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; };
+
+ default :
+ throw new Error('bad maskPattern:' + maskPattern);
+ }
+ };
+
+ _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
+ var a = qrPolynomial([1], 0);
+ for (var i = 0; i < errorCorrectLength; i += 1) {
+ a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) );
+ }
+ return a;
+ };
+
+ _this.getLengthInBits = function(mode, type) {
+
+ if (1 <= type && type < 10) {
+
+ // 1 - 9
+
+ switch(mode) {
+ case QRMode.MODE_NUMBER : return 10;
+ case QRMode.MODE_ALPHA_NUM : return 9;
+ case QRMode.MODE_8BIT_BYTE : return 8;
+ case QRMode.MODE_KANJI : return 8;
+ default :
+ throw new Error('mode:' + mode);
+ }
+
+ } else if (type < 27) {
+
+ // 10 - 26
+
+ switch(mode) {
+ case QRMode.MODE_NUMBER : return 12;
+ case QRMode.MODE_ALPHA_NUM : return 11;
+ case QRMode.MODE_8BIT_BYTE : return 16;
+ case QRMode.MODE_KANJI : return 10;
+ default :
+ throw new Error('mode:' + mode);
+ }
+
+ } else if (type < 41) {
+
+ // 27 - 40
+
+ switch(mode) {
+ case QRMode.MODE_NUMBER : return 14;
+ case QRMode.MODE_ALPHA_NUM : return 13;
+ case QRMode.MODE_8BIT_BYTE : return 16;
+ case QRMode.MODE_KANJI : return 12;
+ default :
+ throw new Error('mode:' + mode);
+ }
+
+ } else {
+ throw new Error('type:' + type);
+ }
+ };
+
+ _this.getLostPoint = function(qrcode) {
+
+ var moduleCount = qrcode.getModuleCount();
+
+ var lostPoint = 0;
+
+ // LEVEL1
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount; col += 1) {
+
+ var sameCount = 0;
+ var dark = qrcode.isDark(row, col);
+
+ for (var r = -1; r <= 1; r += 1) {
+
+ if (row + r < 0 || moduleCount <= row + r) {
+ continue;
+ }
+
+ for (var c = -1; c <= 1; c += 1) {
+
+ if (col + c < 0 || moduleCount <= col + c) {
+ continue;
+ }
+
+ if (r == 0 && c == 0) {
+ continue;
+ }
+
+ if (dark == qrcode.isDark(row + r, col + c) ) {
+ sameCount += 1;
+ }
+ }
+ }
+
+ if (sameCount > 5) {
+ lostPoint += (3 + sameCount - 5);
+ }
+ }
+ };
+
+ // LEVEL2
+
+ for (var row = 0; row < moduleCount - 1; row += 1) {
+ for (var col = 0; col < moduleCount - 1; col += 1) {
+ var count = 0;
+ if (qrcode.isDark(row, col) ) count += 1;
+ if (qrcode.isDark(row + 1, col) ) count += 1;
+ if (qrcode.isDark(row, col + 1) ) count += 1;
+ if (qrcode.isDark(row + 1, col + 1) ) count += 1;
+ if (count == 0 || count == 4) {
+ lostPoint += 3;
+ }
+ }
+ }
+
+ // LEVEL3
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount - 6; col += 1) {
+ if (qrcode.isDark(row, col)
+ && !qrcode.isDark(row, col + 1)
+ && qrcode.isDark(row, col + 2)
+ && qrcode.isDark(row, col + 3)
+ && qrcode.isDark(row, col + 4)
+ && !qrcode.isDark(row, col + 5)
+ && qrcode.isDark(row, col + 6) ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount - 6; row += 1) {
+ if (qrcode.isDark(row, col)
+ && !qrcode.isDark(row + 1, col)
+ && qrcode.isDark(row + 2, col)
+ && qrcode.isDark(row + 3, col)
+ && qrcode.isDark(row + 4, col)
+ && !qrcode.isDark(row + 5, col)
+ && qrcode.isDark(row + 6, col) ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ // LEVEL4
+
+ var darkCount = 0;
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount; row += 1) {
+ if (qrcode.isDark(row, col) ) {
+ darkCount += 1;
+ }
+ }
+ }
+
+ var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
+ lostPoint += ratio * 10;
+
+ return lostPoint;
+ };
+
+ return _this;
+ }();
+
+ //---------------------------------------------------------------------
+ // QRMath
+ //---------------------------------------------------------------------
+
+ var QRMath = function() {
+
+ var EXP_TABLE = new Array(256);
+ var LOG_TABLE = new Array(256);
+
+ // initialize tables
+ for (var i = 0; i < 8; i += 1) {
+ EXP_TABLE[i] = 1 << i;
+ }
+ for (var i = 8; i < 256; i += 1) {
+ EXP_TABLE[i] = EXP_TABLE[i - 4]
+ ^ EXP_TABLE[i - 5]
+ ^ EXP_TABLE[i - 6]
+ ^ EXP_TABLE[i - 8];
+ }
+ for (var i = 0; i < 255; i += 1) {
+ LOG_TABLE[EXP_TABLE[i] ] = i;
+ }
+
+ var _this = {};
+
+ _this.glog = function(n) {
+
+ if (n < 1) {
+ throw new Error('glog(' + n + ')');
+ }
+
+ return LOG_TABLE[n];
+ };
+
+ _this.gexp = function(n) {
+
+ while (n < 0) {
+ n += 255;
+ }
+
+ while (n >= 256) {
+ n -= 255;
+ }
+
+ return EXP_TABLE[n];
+ };
+
+ return _this;
+ }();
+
+ //---------------------------------------------------------------------
+ // qrPolynomial
+ //---------------------------------------------------------------------
+
+ function qrPolynomial(num, shift) {
+
+ if (typeof num.length == 'undefined') {
+ throw new Error(num.length + '/' + shift);
+ }
+
+ var _num = function() {
+ var offset = 0;
+ while (offset < num.length && num[offset] == 0) {
+ offset += 1;
+ }
+ var _num = new Array(num.length - offset + shift);
+ for (var i = 0; i < num.length - offset; i += 1) {
+ _num[i] = num[i + offset];
+ }
+ return _num;
+ }();
+
+ var _this = {};
+
+ _this.getAt = function(index) {
+ return _num[index];
+ };
+
+ _this.getLength = function() {
+ return _num.length;
+ };
+
+ _this.multiply = function(e) {
+
+ var num = new Array(_this.getLength() + e.getLength() - 1);
+
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ for (var j = 0; j < e.getLength(); j += 1) {
+ num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) );
+ }
+ }
+
+ return qrPolynomial(num, 0);
+ };
+
+ _this.mod = function(e) {
+
+ if (_this.getLength() - e.getLength() < 0) {
+ return _this;
+ }
+
+ var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) );
+
+ var num = new Array(_this.getLength() );
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ num[i] = _this.getAt(i);
+ }
+
+ for (var i = 0; i < e.getLength(); i += 1) {
+ num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio);
+ }
+
+ // recursive call
+ return qrPolynomial(num, 0).mod(e);
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // QRRSBlock
+ //---------------------------------------------------------------------
+
+ var QRRSBlock = function() {
+
+ var RS_BLOCK_TABLE = [
+
+ // L
+ // M
+ // Q
+ // H
+
+ // 1
+ [1, 26, 19],
+ [1, 26, 16],
+ [1, 26, 13],
+ [1, 26, 9],
+
+ // 2
+ [1, 44, 34],
+ [1, 44, 28],
+ [1, 44, 22],
+ [1, 44, 16],
+
+ // 3
+ [1, 70, 55],
+ [1, 70, 44],
+ [2, 35, 17],
+ [2, 35, 13],
+
+ // 4
+ [1, 100, 80],
+ [2, 50, 32],
+ [2, 50, 24],
+ [4, 25, 9],
+
+ // 5
+ [1, 134, 108],
+ [2, 67, 43],
+ [2, 33, 15, 2, 34, 16],
+ [2, 33, 11, 2, 34, 12],
+
+ // 6
+ [2, 86, 68],
+ [4, 43, 27],
+ [4, 43, 19],
+ [4, 43, 15],
+
+ // 7
+ [2, 98, 78],
+ [4, 49, 31],
+ [2, 32, 14, 4, 33, 15],
+ [4, 39, 13, 1, 40, 14],
+
+ // 8
+ [2, 121, 97],
+ [2, 60, 38, 2, 61, 39],
+ [4, 40, 18, 2, 41, 19],
+ [4, 40, 14, 2, 41, 15],
+
+ // 9
+ [2, 146, 116],
+ [3, 58, 36, 2, 59, 37],
+ [4, 36, 16, 4, 37, 17],
+ [4, 36, 12, 4, 37, 13],
+
+ // 10
+ [2, 86, 68, 2, 87, 69],
+ [4, 69, 43, 1, 70, 44],
+ [6, 43, 19, 2, 44, 20],
+ [6, 43, 15, 2, 44, 16]
+ ];
+
+ var qrRSBlock = function(totalCount, dataCount) {
+ var _this = {};
+ _this.totalCount = totalCount;
+ _this.dataCount = dataCount;
+ return _this;
+ };
+
+ var _this = {};
+
+ var getRsBlockTable = function(typeNumber, errorCorrectLevel) {
+
+ switch(errorCorrectLevel) {
+ case QRErrorCorrectLevel.L :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+ case QRErrorCorrectLevel.M :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+ case QRErrorCorrectLevel.Q :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+ case QRErrorCorrectLevel.H :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+ default :
+ return undefined;
+ }
+ };
+
+ _this.getRSBlocks = function(typeNumber, errorCorrectLevel) {
+
+ var rsBlock = getRsBlockTable(typeNumber, errorCorrectLevel);
+
+ if (typeof rsBlock == 'undefined') {
+ throw new Error('bad rs block @ typeNumber:' + typeNumber +
+ '/errorCorrectLevel:' + errorCorrectLevel);
+ }
+
+ var length = rsBlock.length / 3;
+
+ var list = new Array();
+
+ for (var i = 0; i < length; i += 1) {
+
+ var count = rsBlock[i * 3 + 0];
+ var totalCount = rsBlock[i * 3 + 1];
+ var dataCount = rsBlock[i * 3 + 2];
+
+ for (var j = 0; j < count; j += 1) {
+ list.push(qrRSBlock(totalCount, dataCount) );
+ }
+ }
+
+ return list;
+ };
+
+ return _this;
+ }();
+
+ // mozilla: Add module support
+ exports.QRRSBlock = QRRSBlock;
+
+ //---------------------------------------------------------------------
+ // qrBitBuffer
+ //---------------------------------------------------------------------
+
+ var qrBitBuffer = function() {
+
+ var _buffer = new Array();
+ var _length = 0;
+
+ var _this = {};
+
+ _this.getBuffer = function() {
+ return _buffer;
+ };
+
+ _this.getAt = function(index) {
+ var bufIndex = Math.floor(index / 8);
+ return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1;
+ };
+
+ _this.put = function(num, length) {
+ for (var i = 0; i < length; i += 1) {
+ _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1);
+ }
+ };
+
+ _this.getLengthInBits = function() {
+ return _length;
+ };
+
+ _this.putBit = function(bit) {
+
+ var bufIndex = Math.floor(_length / 8);
+ if (_buffer.length <= bufIndex) {
+ _buffer.push(0);
+ }
+
+ if (bit) {
+ _buffer[bufIndex] |= (0x80 >>> (_length % 8) );
+ }
+
+ _length += 1;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qr8BitByte
+ //---------------------------------------------------------------------
+
+ var qr8BitByte = function(data) {
+
+ var _mode = QRMode.MODE_8BIT_BYTE;
+ var _data = data;
+ var _bytes = qrcode.stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _bytes.length;
+ };
+
+ _this.write = function(buffer) {
+ for (var i = 0; i < _bytes.length; i += 1) {
+ buffer.put(_bytes[i], 8);
+ }
+ };
+
+ return _this;
+ };
+
+ //=====================================================================
+ // GIF Support etc.
+ //
+
+ //---------------------------------------------------------------------
+ // byteArrayOutputStream
+ //---------------------------------------------------------------------
+
+ var byteArrayOutputStream = function() {
+
+ var _bytes = new Array();
+
+ var _this = {};
+
+ _this.writeByte = function(b) {
+ _bytes.push(b & 0xff);
+ };
+
+ _this.writeShort = function(i) {
+ _this.writeByte(i);
+ _this.writeByte(i >>> 8);
+ };
+
+ _this.writeBytes = function(b, off, len) {
+ off = off || 0;
+ len = len || b.length;
+ for (var i = 0; i < len; i += 1) {
+ _this.writeByte(b[i + off]);
+ }
+ };
+
+ _this.writeString = function(s) {
+ for (var i = 0; i < s.length; i += 1) {
+ _this.writeByte(s.charCodeAt(i) );
+ }
+ };
+
+ _this.toByteArray = function() {
+ return _bytes;
+ };
+
+ _this.toString = function() {
+ var s = '';
+ s += '[';
+ for (var i = 0; i < _bytes.length; i += 1) {
+ if (i > 0) {
+ s += ',';
+ }
+ s += _bytes[i];
+ }
+ s += ']';
+ return s;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64EncodeOutputStream
+ //---------------------------------------------------------------------
+
+ var base64EncodeOutputStream = function() {
+
+ var _buffer = 0;
+ var _buflen = 0;
+ var _length = 0;
+ var _base64 = '';
+
+ var _this = {};
+
+ var writeEncoded = function(b) {
+ _base64 += String.fromCharCode(encode(b & 0x3f) );
+ };
+
+ var encode = function(n) {
+ if (n < 0) {
+ // error.
+ } else if (n < 26) {
+ return 0x41 + n;
+ } else if (n < 52) {
+ return 0x61 + (n - 26);
+ } else if (n < 62) {
+ return 0x30 + (n - 52);
+ } else if (n == 62) {
+ return 0x2b;
+ } else if (n == 63) {
+ return 0x2f;
+ }
+ throw new Error('n:' + n);
+ };
+
+ _this.writeByte = function(n) {
+
+ _buffer = (_buffer << 8) | (n & 0xff);
+ _buflen += 8;
+ _length += 1;
+
+ while (_buflen >= 6) {
+ writeEncoded(_buffer >>> (_buflen - 6) );
+ _buflen -= 6;
+ }
+ };
+
+ _this.flush = function() {
+
+ if (_buflen > 0) {
+ writeEncoded(_buffer << (6 - _buflen) );
+ _buffer = 0;
+ _buflen = 0;
+ }
+
+ if (_length % 3 != 0) {
+ // padding
+ var padlen = 3 - _length % 3;
+ for (var i = 0; i < padlen; i += 1) {
+ _base64 += '=';
+ }
+ }
+ };
+
+ _this.toString = function() {
+ return _base64;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64DecodeInputStream
+ //---------------------------------------------------------------------
+
+ var base64DecodeInputStream = function(str) {
+
+ var _str = str;
+ var _pos = 0;
+ var _buffer = 0;
+ var _buflen = 0;
+
+ var _this = {};
+
+ _this.read = function() {
+
+ while (_buflen < 8) {
+
+ if (_pos >= _str.length) {
+ if (_buflen == 0) {
+ return -1;
+ }
+ throw new Error('unexpected end of file./' + _buflen);
+ }
+
+ var c = _str.charAt(_pos);
+ _pos += 1;
+
+ if (c == '=') {
+ _buflen = 0;
+ return -1;
+ } else if (c.match(/^\s$/) ) {
+ // ignore if whitespace.
+ continue;
+ }
+
+ _buffer = (_buffer << 6) | decode(c.charCodeAt(0) );
+ _buflen += 6;
+ }
+
+ var n = (_buffer >>> (_buflen - 8) ) & 0xff;
+ _buflen -= 8;
+ return n;
+ };
+
+ var decode = function(c) {
+ if (0x41 <= c && c <= 0x5a) {
+ return c - 0x41;
+ } else if (0x61 <= c && c <= 0x7a) {
+ return c - 0x61 + 26;
+ } else if (0x30 <= c && c <= 0x39) {
+ return c - 0x30 + 52;
+ } else if (c == 0x2b) {
+ return 62;
+ } else if (c == 0x2f) {
+ return 63;
+ } else {
+ throw new Error('c:' + c);
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // gifImage (B/W)
+ //---------------------------------------------------------------------
+
+ var gifImage = function(width, height) {
+
+ var _width = width;
+ var _height = height;
+ var _data = new Array(width * height);
+
+ var _this = {};
+
+ _this.setPixel = function(x, y, pixel) {
+ _data[y * _width + x] = pixel;
+ };
+
+ _this.write = function(out) {
+
+ //---------------------------------
+ // GIF Signature
+
+ out.writeString('GIF87a');
+
+ //---------------------------------
+ // Screen Descriptor
+
+ out.writeShort(_width);
+ out.writeShort(_height);
+
+ out.writeByte(0x80); // 2bit
+ out.writeByte(0);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Global Color Map
+
+ // black
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+
+ // white
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+
+ //---------------------------------
+ // Image Descriptor
+
+ out.writeString(',');
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(_width);
+ out.writeShort(_height);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Local Color Map
+
+ //---------------------------------
+ // Raster Data
+
+ var lzwMinCodeSize = 2;
+ var raster = getLZWRaster(lzwMinCodeSize);
+
+ out.writeByte(lzwMinCodeSize);
+
+ var offset = 0;
+
+ while (raster.length - offset > 255) {
+ out.writeByte(255);
+ out.writeBytes(raster, offset, 255);
+ offset += 255;
+ }
+
+ out.writeByte(raster.length - offset);
+ out.writeBytes(raster, offset, raster.length - offset);
+ out.writeByte(0x00);
+
+ //---------------------------------
+ // GIF Terminator
+ out.writeString(';');
+ };
+
+ var bitOutputStream = function(out) {
+
+ var _out = out;
+ var _bitLength = 0;
+ var _bitBuffer = 0;
+
+ var _this = {};
+
+ _this.write = function(data, length) {
+
+ if ( (data >>> length) != 0) {
+ throw new Error('length over');
+ }
+
+ while (_bitLength + length >= 8) {
+ _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) );
+ length -= (8 - _bitLength);
+ data >>>= (8 - _bitLength);
+ _bitBuffer = 0;
+ _bitLength = 0;
+ }
+
+ _bitBuffer = (data << _bitLength) | _bitBuffer;
+ _bitLength = _bitLength + length;
+ };
+
+ _this.flush = function() {
+ if (_bitLength > 0) {
+ _out.writeByte(_bitBuffer);
+ }
+ };
+
+ return _this;
+ };
+
+ var getLZWRaster = function(lzwMinCodeSize) {
+
+ var clearCode = 1 << lzwMinCodeSize;
+ var endCode = (1 << lzwMinCodeSize) + 1;
+ var bitLength = lzwMinCodeSize + 1;
+
+ // Setup LZWTable
+ var table = lzwTable();
+
+ for (var i = 0; i < clearCode; i += 1) {
+ table.add(String.fromCharCode(i) );
+ }
+ table.add(String.fromCharCode(clearCode) );
+ table.add(String.fromCharCode(endCode) );
+
+ var byteOut = byteArrayOutputStream();
+ var bitOut = bitOutputStream(byteOut);
+
+ // clear code
+ bitOut.write(clearCode, bitLength);
+
+ var dataIndex = 0;
+
+ var s = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ while (dataIndex < _data.length) {
+
+ var c = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ if (table.contains(s + c) ) {
+
+ s = s + c;
+
+ } else {
+
+ bitOut.write(table.indexOf(s), bitLength);
+
+ if (table.size() < 0xfff) {
+
+ if (table.size() == (1 << bitLength) ) {
+ bitLength += 1;
+ }
+
+ table.add(s + c);
+ }
+
+ s = c;
+ }
+ }
+
+ bitOut.write(table.indexOf(s), bitLength);
+
+ // end code
+ bitOut.write(endCode, bitLength);
+
+ bitOut.flush();
+
+ return byteOut.toByteArray();
+ };
+
+ var lzwTable = function() {
+
+ var _map = {};
+ var _size = 0;
+
+ var _this = {};
+
+ _this.add = function(key) {
+ if (_this.contains(key) ) {
+ throw new Error('dup key:' + key);
+ }
+ _map[key] = _size;
+ _size += 1;
+ };
+
+ _this.size = function() {
+ return _size;
+ };
+
+ _this.indexOf = function(key) {
+ return _map[key];
+ };
+
+ _this.contains = function(key) {
+ return typeof _map[key] != 'undefined';
+ };
+
+ return _this;
+ };
+
+ return _this;
+ };
+
+ var createGifData = function(width, height, getPixel) {
+
+ var gif = gifImage(width, height);
+ for (var y = 0; y < height; y += 1) {
+ for (var x = 0; x < width; x += 1) {
+ gif.setPixel(x, y, getPixel(x, y) );
+ }
+ }
+
+ var b = byteArrayOutputStream();
+ gif.write(b);
+
+ var base64 = base64EncodeOutputStream();
+ var bytes = b.toByteArray();
+ for (var i = 0; i < bytes.length; i += 1) {
+ base64.writeByte(bytes[i]);
+ }
+ base64.flush();
+ return base64;
+
+ };
+
+ var createImgTag = function(width, height, getPixel, alt) {
+
+ var base64 = createGifData(width, height, getPixel);
+ var img = '';
+ img += '<img';
+ img += '\u0020src="';
+ img += 'data:image/gif;base64,';
+ img += base64;
+ img += '"';
+ img += '\u0020width="';
+ img += width;
+ img += '"';
+ img += '\u0020height="';
+ img += height;
+ img += '"';
+ if (alt) {
+ img += '\u0020alt="';
+ img += alt;
+ img += '"';
+ }
+ img += '/>';
+
+ return img;
+ };
+
+ //---------------------------------------------------------------------
+ // returns qrcode function.
+
+ return qrcode;
+}();
+
+// mozilla: Add module support
+exports.Encoder = qrcode;
diff --git a/devtools/shared/qrcode/encoder/moz.build b/devtools/shared/qrcode/encoder/moz.build
new file mode 100644
index 0000000000..4442a2e906
--- /dev/null
+++ b/devtools/shared/qrcode/encoder/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ 'index.js',
+)
diff --git a/devtools/shared/qrcode/index.js b/devtools/shared/qrcode/index.js
new file mode 100644
index 0000000000..4dcc4f6598
--- /dev/null
+++ b/devtools/shared/qrcode/index.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";
+
+// Lazily require encoder and decoder in case only one is needed
+Object.defineProperty(this, "Encoder", {
+ get: () =>
+ require("resource://devtools/shared/qrcode/encoder/index.js").Encoder,
+});
+Object.defineProperty(this, "QRRSBlock", {
+ get: () =>
+ require("resource://devtools/shared/qrcode/encoder/index.js").QRRSBlock,
+});
+Object.defineProperty(this, "QRErrorCorrectLevel", {
+ get: () =>
+ require("resource://devtools/shared/qrcode/encoder/index.js")
+ .QRErrorCorrectLevel,
+});
+Object.defineProperty(this, "decoder", {
+ get: () => {
+ // Some applications don't ship the decoder, see moz.build
+ try {
+ return require("resource://devtools/shared/qrcode/decoder/index.js");
+ } catch (e) {
+ return null;
+ }
+ },
+});
+
+/**
+ * There are many "versions" of QR codes, which describes how many dots appear
+ * in the resulting image, thus limiting the amount of data that can be
+ * represented.
+ *
+ * The encoder used here allows for versions 1 - 10 (more dots for larger
+ * versions).
+ *
+ * It expects you to pick a version large enough to contain your message. Here
+ * we search for the mimimum version based on the message length.
+ * @param string message
+ * Text to encode
+ * @param string quality
+ * Quality level: L, M, Q, H
+ * @return integer
+ */
+exports.findMinimumVersion = function (message, quality) {
+ const msgLength = message.length;
+ const qualityLevel = QRErrorCorrectLevel[quality];
+ for (let version = 1; version <= 10; version++) {
+ const rsBlocks = QRRSBlock.getRSBlocks(version, qualityLevel);
+ let maxLength = rsBlocks.reduce((prev, block) => {
+ return prev + block.dataCount;
+ }, 0);
+ // Remove two bytes to fit header info
+ maxLength -= 2;
+ if (msgLength <= maxLength) {
+ return version;
+ }
+ }
+ throw new Error("Message too large");
+};
+
+/**
+ * Simple wrapper around the underlying encoder's API.
+ * @param string message
+ * Text to encode
+ * @param string quality (optional)
+ Quality level: L, M, Q, H
+ * @param integer version (optional)
+ * QR code "version" large enough to contain the message
+ * @return object with the following fields:
+ * * src: an image encoded a data URI
+ * * height: image height
+ * * width: image width
+ */
+exports.encodeToDataURI = function (message, quality, version) {
+ quality = quality || "H";
+ version = version || exports.findMinimumVersion(message, quality);
+ const encoder = new Encoder(version, quality);
+ encoder.addData(message);
+ encoder.make();
+ return encoder.createImgData();
+};
+
+/**
+ * Simple wrapper around the underlying decoder's API.
+ * @param string URI
+ * URI of an image of a QR code
+ * @return Promise
+ * The promise will be resolved with a string, which is the data inside
+ * the QR code.
+ */
+exports.decodeFromURI = function (URI) {
+ if (!decoder) {
+ return Promise.reject();
+ }
+ return new Promise((resolve, reject) => {
+ decoder.decodeFromURI(URI, resolve, reject);
+ });
+};
+
+/**
+ * Decode a QR code that has been drawn to a canvas element.
+ * @param Canvas canvas
+ * <canvas> element to read from
+ * @return string
+ * The data inside the QR code
+ */
+exports.decodeFromCanvas = function (canvas) {
+ if (!decoder) {
+ throw new Error("Decoder not available");
+ }
+ return decoder.decodeFromCanvas(canvas);
+};
diff --git a/devtools/shared/qrcode/moz.build b/devtools/shared/qrcode/moz.build
new file mode 100644
index 0000000000..1c171dd4dd
--- /dev/null
+++ b/devtools/shared/qrcode/moz.build
@@ -0,0 +1,18 @@
+# -*- 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 += ["encoder"]
+
+# Save file size on Fennec until there are active plans to use the decoder there
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ DIRS += ["decoder"]
+
+DevToolsModules(
+ "index.js",
+)
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
diff --git a/devtools/shared/qrcode/tests/chrome/chrome.toml b/devtools/shared/qrcode/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..8df695b2ae
--- /dev/null
+++ b/devtools/shared/qrcode/tests/chrome/chrome.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+tags = "devtools"
+
+["test_decode.html"]
diff --git a/devtools/shared/qrcode/tests/chrome/test_decode.html b/devtools/shared/qrcode/tests/chrome/test_decode.html
new file mode 100644
index 0000000000..cc74de2967
--- /dev/null
+++ b/devtools/shared/qrcode/tests/chrome/test_decode.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test decoding a simple message
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test decoding a simple message</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">
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+
+ const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+
+ const QR = require("devtools/shared/qrcode/index");
+
+ SimpleTest.waitForExplicitFinish();
+
+ const testImage =
+ "data:image/gif;base64,R0lGODdhOgA6AIAAAAAAAP///ywAAAAAOgA6AAAC" +
+ "/4yPqcvtD6OctNqLs968+w+G4gKU5nkaKKquLuW+QVy2tAkDTj3rfQts8CRDko" +
+ "+HPPoYRUgy9YsyldDm44mLWhHYZM6W7WaDqyCRGkZDySxpRGw2sqvLt1q5w/fo" +
+ "XyE6vnUQOJUHBlinMGh046V1F5PDqNcoqcgBOWKBKbK2N+aY+Ih49VkmqMcl2l" +
+ "dkhZUK1umE6jZXJ2ZJaujZaRqH4bpb2uZrJxvIt4Ebe9qoYYrJOsw8apz2bCut" +
+ "m9kqDcw52uuImyr5Oh1KXH1jrn2anuunywtODU/o2c6teceW39ZcLFg/fNMo1b" +
+ "t3jVw2dwTPwJq1KYG3gAklCgu37yGxeScYKyiCc+7DR34hPVQiuQ7UhJMagyEb" +
+ "lymmzJk0a9q8iTOnzp0NCgAAOw==";
+
+ (async function () {
+ let result = await QR.decodeFromURI(testImage);
+ is(result, "HELLO", "Decoded data URI result matches");
+ const canvas = await drawToCanvas(testImage);
+ result = QR.decodeFromCanvas(canvas);
+ is(result, "HELLO", "Decoded canvas result matches");
+ })().then(SimpleTest.finish, ok.bind(null, false));
+
+ function drawToCanvas(src) {
+ return new Promise(resolve => {
+ const canvas = document.createElement("canvas");
+ const context = canvas.getContext("2d");
+ const image = new Image();
+
+ image.onload = () => {
+ canvas.width = image.width;
+ canvas.height = image.height;
+ context.drawImage(image, 0, 0);
+ resolve(canvas);
+ };
+ image.src = src;
+ });
+ }
+};
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/qrcode/tests/xpcshell/.eslintrc.js b/devtools/shared/qrcode/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..7f6b62a9e5
--- /dev/null
+++ b/devtools/shared/qrcode/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/qrcode/tests/xpcshell/test_encode.js b/devtools/shared/qrcode/tests/xpcshell/test_encode.js
new file mode 100644
index 0000000000..7c82cf50c0
--- /dev/null
+++ b/devtools/shared/qrcode/tests/xpcshell/test_encode.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test encoding a simple message.
+ */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+
+const QR = require("resource://devtools/shared/qrcode/index.js");
+
+function run_test() {
+ const imgData = QR.encodeToDataURI("HELLO", "L");
+ Assert.equal(
+ imgData.src,
+ "data:image/gif;base64,R0lGODdhOgA6AIAAAAAAAP///ywAAAAAOgA6AAAC" +
+ "/4yPqcvtD6OctNqLs968+w+G4gKU5nkaKKquLuW+QVy2tAkDTj3rfQts8CRDko" +
+ "+HPPoYRUgy9YsyldDm44mLWhHYZM6W7WaDqyCRGkZDySxpRGw2sqvLt1q5w/fo" +
+ "XyE6vnUQOJUHBlinMGh046V1F5PDqNcoqcgBOWKBKbK2N+aY+Ih49VkmqMcl2l" +
+ "dkhZUK1umE6jZXJ2ZJaujZaRqH4bpb2uZrJxvIt4Ebe9qoYYrJOsw8apz2bCut" +
+ "m9kqDcw52uuImyr5Oh1KXH1jrn2anuunywtODU/o2c6teceW39ZcLFg/fNMo1b" +
+ "t3jVw2dwTPwJq1KYG3gAklCgu37yGxeScYKyiCc+7DR34hPVQiuQ7UhJMagyEb" +
+ "lymmzJk0a9q8iTOnzp0NCgAAOw=="
+ );
+ Assert.equal(imgData.width, 58);
+ Assert.equal(imgData.height, 58);
+}
diff --git a/devtools/shared/qrcode/tests/xpcshell/xpcshell.toml b/devtools/shared/qrcode/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..6eebf920a6
--- /dev/null
+++ b/devtools/shared/qrcode/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = "devtools"
+head = ""
+firefox-appdir = "browser"
+
+["test_encode.js"]
diff --git a/devtools/shared/security/DevToolsSocketStatus.sys.mjs b/devtools/shared/security/DevToolsSocketStatus.sys.mjs
new file mode 100644
index 0000000000..8acfbdd8c4
--- /dev/null
+++ b/devtools/shared/security/DevToolsSocketStatus.sys.mjs
@@ -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/. */
+
+/**
+ * Singleton that should be updated whenever a socket is opened or closed for
+ * incoming connections.
+ *
+ * Notifies observers via "devtools-socket" when the status might have have
+ * changed.
+ *
+ * Currently observed by browser/base/content/browser.js in order to display
+ * the "remote control" visual cue also used for Marionette and Remote Agent.
+ */
+export const DevToolsSocketStatus = {
+ _browserToolboxSockets: 0,
+ _otherSockets: 0,
+
+ /**
+ * Check if there are opened DevTools sockets.
+ *
+ * @param {Object=} options
+ * @param {boolean=} options.excludeBrowserToolboxSockets
+ * Exclude sockets opened by local Browser Toolbox sessions. Defaults to
+ * false.
+ *
+ * @return {boolean}
+ * Returns true if there are DevTools sockets opened and matching the
+ * provided options if any.
+ */
+ hasSocketOpened(options = {}) {
+ const { excludeBrowserToolboxSockets = false } = options;
+ if (excludeBrowserToolboxSockets) {
+ return this._otherSockets > 0;
+ }
+ return this._browserToolboxSockets + this._otherSockets > 0;
+ },
+
+ notifySocketOpened(options) {
+ const { fromBrowserToolbox } = options;
+ if (fromBrowserToolbox) {
+ this._browserToolboxSockets++;
+ } else {
+ this._otherSockets++;
+ }
+
+ Services.obs.notifyObservers(this, "devtools-socket");
+ },
+
+ notifySocketClosed(options) {
+ const { fromBrowserToolbox } = options;
+ if (fromBrowserToolbox) {
+ this._browserToolboxSockets--;
+ } else {
+ this._otherSockets--;
+ }
+
+ Services.obs.notifyObservers(this, "devtools-socket");
+ },
+};
diff --git a/devtools/shared/security/auth.js b/devtools/shared/security/auth.js
new file mode 100644
index 0000000000..57e3ebe6ff
--- /dev/null
+++ b/devtools/shared/security/auth.js
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+loader.lazyRequireGetter(
+ this,
+ "prompt",
+ "resource://devtools/shared/security/prompt.js"
+);
+
+/**
+ * A simple enum-like object with keys mirrored to values.
+ * This makes comparison to a specfic value simpler without having to repeat and
+ * mis-type the value.
+ */
+function createEnum(obj) {
+ for (const key in obj) {
+ obj[key] = key;
+ }
+ return obj;
+}
+
+/**
+ * |allowConnection| implementations can return various values as their |result|
+ * field to indicate what action to take. By specifying these, we can
+ * centralize the common actions available, while still allowing embedders to
+ * present their UI in whatever way they choose.
+ */
+var AuthenticationResult = (exports.AuthenticationResult = createEnum({
+ /**
+ * Close all listening sockets, and disable them from opening again.
+ */
+ DISABLE_ALL: null,
+
+ /**
+ * Deny the current connection.
+ */
+ DENY: null,
+
+ /**
+ * Additional data needs to be exchanged before a result can be determined.
+ */
+ PENDING: null,
+
+ /**
+ * Allow the current connection.
+ */
+ ALLOW: null,
+
+ /**
+ * Allow the current connection, and persist this choice for future
+ * connections from the same client. This requires a trustable mechanism to
+ * identify the client in the future.
+ */
+ ALLOW_PERSIST: null,
+}));
+
+/**
+ * An |Authenticator| implements an authentication mechanism via various hooks
+ * in the client and server debugger socket connection path (see socket.js).
+ *
+ * |Authenticator|s are stateless objects. Each hook method is passed the state
+ * it needs by the client / server code in socket.js.
+ *
+ * Separate instances of the |Authenticator| are created for each use (client
+ * connection, server listener) in case some methods are customized by the
+ * embedder for a given use case.
+ */
+var Authenticators = {};
+
+/**
+ * The Prompt authenticator displays a server-side user prompt that includes
+ * connection details, and asks the user to verify the connection. There are
+ * no cryptographic properties at work here, so it is up to the user to be sure
+ * that the client can be trusted.
+ */
+var Prompt = (Authenticators.Prompt = {});
+
+Prompt.mode = "PROMPT";
+
+Prompt.Client = function () {};
+Prompt.Client.prototype = {
+ mode: Prompt.mode,
+
+ /**
+ * When client is about to make a new connection, verify that the connection settings
+ * are compatible with this authenticator.
+ * @throws if validation requirements are not met
+ */
+ validateSettings() {},
+
+ /**
+ * When client has just made a new socket connection, validate the connection
+ * to ensure it meets the authenticator's policies.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ * @return boolean
+ * Whether the connection is valid.
+ */
+ validateConnection() {
+ return true;
+ },
+
+ /**
+ * Work with the server to complete any additional steps required by this
+ * authenticator's policies.
+ *
+ * Debugging commences after this hook completes successfully.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param transport DebuggerTransport
+ * A transport that can be used to communicate with the server.
+ * @return A promise can be used if there is async behavior.
+ */
+ authenticate() {},
+};
+
+Prompt.Server = function () {};
+Prompt.Server.prototype = {
+ mode: Prompt.mode,
+
+ /**
+ * Augment the service discovery advertisement with any additional data needed
+ * to support this authentication mode.
+ *
+ * @param listener SocketListener
+ * The socket listener that was just opened.
+ * @param advertisement object
+ * The advertisement being built.
+ */
+ augmentAdvertisement(listener, advertisement) {
+ advertisement.authentication = Prompt.mode;
+ },
+
+ /**
+ * Determine whether a connection the server should be allowed or not based on
+ * this authenticator's policies.
+ *
+ * @param session object
+ * In PROMPT mode, the |session| includes:
+ * {
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * },
+ * transport
+ * }
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ authenticate({ client, server }) {
+ if (!Services.prefs.getBoolPref("devtools.debugger.prompt-connection")) {
+ return AuthenticationResult.ALLOW;
+ }
+ return this.allowConnection({
+ authentication: this.mode,
+ client,
+ server,
+ });
+ },
+
+ /**
+ * Prompt the user to accept or decline the incoming connection. The default
+ * implementation is used unless this is overridden on a particular
+ * authenticator instance.
+ *
+ * It is expected that the implementation of |allowConnection| will show a
+ * prompt to the user so that they can allow or deny the connection.
+ *
+ * @param session object
+ * In PROMPT mode, the |session| includes:
+ * {
+ * authentication: "PROMPT",
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ allowConnection: prompt.Server.defaultAllowConnection,
+};
+
+exports.Authenticators = {
+ get(mode) {
+ if (!mode) {
+ mode = Prompt.mode;
+ }
+ for (const key in Authenticators) {
+ const auth = Authenticators[key];
+ if (auth.mode === mode) {
+ return auth;
+ }
+ }
+ throw new Error("Unknown authenticator mode: " + mode);
+ },
+};
diff --git a/devtools/shared/security/moz.build b/devtools/shared/security/moz.build
new file mode 100644
index 0000000000..65a667c8d4
--- /dev/null
+++ b/devtools/shared/security/moz.build
@@ -0,0 +1,15 @@
+# -*- 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/.
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+DevToolsModules(
+ "auth.js",
+ "DevToolsSocketStatus.sys.mjs",
+ "prompt.js",
+ "socket.js",
+)
diff --git a/devtools/shared/security/prompt.js b/devtools/shared/security/prompt.js
new file mode 100644
index 0000000000..b03204cc34
--- /dev/null
+++ b/devtools/shared/security/prompt.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";
+
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+loader.lazyRequireGetter(
+ this,
+ "AuthenticationResult",
+ "resource://devtools/shared/security/auth.js",
+ true
+);
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/shared/locales/debugger.properties"
+);
+
+var Client = (exports.Client = {});
+var Server = (exports.Server = {});
+
+/**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually. For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+Client.defaultSendOOB = ({ authResult, oob }) => {
+ // Only show in the PENDING state
+ if (authResult != AuthenticationResult.PENDING) {
+ throw new Error("Expected PENDING result, got " + authResult);
+ }
+ const title = L10N.getStr("clientSendOOBTitle");
+ const header = L10N.getStr("clientSendOOBHeader");
+ const hashMsg = L10N.getFormatStr("clientSendOOBHash", oob.sha256);
+ const token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
+ const tokenMsg = L10N.getFormatStr("clientSendOOBToken", token);
+ const msg = `${header}\n\n${hashMsg}\n${tokenMsg}`;
+ const prompt = Services.prompt;
+ const flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_CANCEL;
+
+ // Listen for the window our prompt opens, so we can close it programatically
+ let promptWindow;
+ const windowListener = {
+ onOpenWindow(xulWindow) {
+ const win = xulWindow.docShell.domWindow;
+ win.addEventListener(
+ "load",
+ function () {
+ if (
+ win.document.documentElement.getAttribute("id") != "commonDialog"
+ ) {
+ return;
+ }
+ // Found the window
+ promptWindow = win;
+ Services.wm.removeListener(windowListener);
+ },
+ { once: true }
+ );
+ },
+ onCloseWindow() {},
+ };
+ Services.wm.addListener(windowListener);
+
+ // nsIPrompt is typically a blocking API, so |executeSoon| to get around this
+ DevToolsUtils.executeSoon(() => {
+ prompt.confirmEx(null, title, msg, flags, null, null, null, null, {
+ value: false,
+ });
+ });
+
+ return {
+ close() {
+ if (!promptWindow) {
+ return;
+ }
+ promptWindow.document.documentElement.acceptDialog();
+ promptWindow = null;
+ },
+ };
+};
+
+/**
+ * Prompt the user to accept or decline the incoming connection. This is the
+ * default implementation that products embedding the devtools server may
+ * choose to override. This can be overridden via |allowConnection| on the
+ * socket's authenticator instance.
+ *
+ * @param session object
+ * The session object will contain at least the following fields:
+ * {
+ * authentication,
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * Specific authentication modes may include additional fields. Check
+ * the different |allowConnection| methods in ./auth.js.
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+Server.defaultAllowConnection = ({ client, server }) => {
+ const title = L10N.getStr("remoteIncomingPromptTitle");
+ const header = L10N.getStr("remoteIncomingPromptHeader");
+ const clientEndpoint = `${client.host}:${client.port}`;
+ const clientMsg = L10N.getFormatStr(
+ "remoteIncomingPromptClientEndpoint",
+ clientEndpoint
+ );
+ const serverEndpoint = `${server.host}:${server.port}`;
+ const serverMsg = L10N.getFormatStr(
+ "remoteIncomingPromptServerEndpoint",
+ serverEndpoint
+ );
+ const footer = L10N.getStr("remoteIncomingPromptFooter");
+ const msg = `${header}\n\n${clientMsg}\n${serverMsg}\n\n${footer}`;
+ const disableButton = L10N.getStr("remoteIncomingPromptDisable");
+ const prompt = Services.prompt;
+ const flags =
+ prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK +
+ prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL +
+ prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING +
+ prompt.BUTTON_POS_1_DEFAULT;
+ const result = prompt.confirmEx(
+ null,
+ title,
+ msg,
+ flags,
+ null,
+ null,
+ disableButton,
+ null,
+ { value: false }
+ );
+ if (result === 0) {
+ return AuthenticationResult.ALLOW;
+ }
+ if (result === 2) {
+ return AuthenticationResult.DISABLE_ALL;
+ }
+ return AuthenticationResult.DENY;
+};
+
+/**
+ * During OOB_CERT authentication, the user must transfer some data through some
+ * out of band mechanism from the client to the server to authenticate the
+ * devices.
+ *
+ * This implementation prompts the user for a token as constructed by
+ * |Client.defaultSendOOB| that the user needs to transfer manually. For a
+ * mobile device, you should override this implementation with something more
+ * convenient, such as reading a QR code.
+ *
+ * @return An object containing:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * A promise that will be resolved to the above is also allowed.
+ */
+Server.defaultReceiveOOB = () => {
+ const title = L10N.getStr("serverReceiveOOBTitle");
+ const msg = L10N.getStr("serverReceiveOOBBody");
+ let input = { value: null };
+ const prompt = Services.prompt;
+ const result = prompt.prompt(null, title, msg, input, null, { value: false });
+ if (!result) {
+ return null;
+ }
+ // Re-create original object from token
+ input = input.value.trim();
+ let sha256 = input.substring(0, 64);
+ sha256 = sha256.replace(/\w{2}/g, "$&:").slice(0, -1).toUpperCase();
+ const k = input.substring(64);
+ return { sha256, k };
+};
diff --git a/devtools/shared/security/socket.js b/devtools/shared/security/socket.js
new file mode 100644
index 0000000000..961ca13310
--- /dev/null
+++ b/devtools/shared/security/socket.js
@@ -0,0 +1,685 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Ensure PSM is initialized to support TLS sockets
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+var { dumpn } = DevToolsUtils;
+loader.lazyRequireGetter(
+ this,
+ "WebSocketServer",
+ "resource://devtools/server/socket/websocket-server.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "DebuggerTransport",
+ "resource://devtools/shared/transport/transport.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WebSocketDebuggerTransport",
+ "resource://devtools/shared/transport/websocket-transport.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "discovery",
+ "resource://devtools/shared/discovery/discovery.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "Authenticators",
+ "resource://devtools/shared/security/auth.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "AuthenticationResult",
+ "resource://devtools/shared/security/auth.js",
+ true
+);
+const lazy = {};
+
+DevToolsUtils.defineLazyGetter(
+ lazy,
+ "DevToolsSocketStatus",
+ () =>
+ ChromeUtils.importESModule(
+ "resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs",
+ {
+ // DevToolsSocketStatus is also accessed by non-devtools modules and
+ // should be loaded in the regular / shared global.
+ loadInDevToolsLoader: false,
+ }
+ ).DevToolsSocketStatus
+);
+
+loader.lazyRequireGetter(
+ this,
+ "EventEmitter",
+ "resource://devtools/shared/event-emitter.js"
+);
+
+DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
+ return Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+ );
+});
+
+DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
+ return Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+});
+
+var DebuggerSocket = {};
+
+/**
+ * Connects to a devtools server socket.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param webSocket boolean (optional)
+ * Whether to use WebSocket protocol to connect. Defaults to false.
+ * @param authenticator Authenticator (optional)
+ * |Authenticator| instance matching the mode in use by the server.
+ * Defaults to a PROMPT instance if not supplied.
+ * @return promise
+ * Resolved to a DebuggerTransport instance.
+ */
+DebuggerSocket.connect = async function (settings) {
+ // Default to PROMPT |Authenticator| instance if not supplied
+ if (!settings.authenticator) {
+ settings.authenticator = new (Authenticators.get().Client)();
+ }
+ _validateSettings(settings);
+ // eslint-disable-next-line no-shadow
+ const { host, port, authenticator } = settings;
+ const transport = await _getTransport(settings);
+ await authenticator.authenticate({
+ host,
+ port,
+ transport,
+ });
+ transport.connectionSettings = settings;
+ return transport;
+};
+
+/**
+ * Validate that the connection settings have been set to a supported configuration.
+ */
+function _validateSettings(settings) {
+ const { authenticator } = settings;
+
+ authenticator.validateSettings(settings);
+}
+
+/**
+ * Try very hard to create a DevTools transport, potentially making several
+ * connect attempts in the process.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param webSocket boolean (optional)
+ * Whether to use WebSocket protocol to connect to the server. Defaults to false.
+ * @param authenticator Authenticator
+ * |Authenticator| instance matching the mode in use by the server.
+ * Defaults to a PROMPT instance if not supplied.
+ * @return transport DebuggerTransport
+ * A possible DevTools transport (if connection succeeded and streams
+ * are actually alive and working)
+ */
+var _getTransport = async function (settings) {
+ const { host, port, webSocket } = settings;
+
+ if (webSocket) {
+ // Establish a connection and wait until the WebSocket is ready to send and receive
+ const socket = await new Promise((resolve, reject) => {
+ const s = new WebSocket(`ws://${host}:${port}`);
+ s.onopen = () => resolve(s);
+ s.onerror = err => reject(err);
+ });
+
+ return new WebSocketDebuggerTransport(socket);
+ }
+
+ const attempt = await _attemptTransport(settings);
+ if (attempt.transport) {
+ // Success
+ return attempt.transport;
+ }
+
+ throw new Error("Connection failed");
+};
+
+/**
+ * Make a single attempt to connect and create a DevTools transport.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param authenticator Authenticator
+ * |Authenticator| instance matching the mode in use by the server.
+ * Defaults to a PROMPT instance if not supplied.
+ * @return transport DebuggerTransport
+ * A possible DevTools transport (if connection succeeded and streams
+ * are actually alive and working)
+ * @return s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ */
+var _attemptTransport = async function (settings) {
+ const { authenticator } = settings;
+ // _attemptConnect only opens the streams. Any failures at that stage
+ // aborts the connection process immedidately.
+ const { s, input, output } = await _attemptConnect(settings);
+
+ // Check if the input stream is alive.
+ let alive;
+ try {
+ const results = await _isInputAlive(input);
+ alive = results.alive;
+ } catch (e) {
+ // For other unexpected errors, like NS_ERROR_CONNECTION_REFUSED, we reach
+ // this block.
+ input.close();
+ output.close();
+ throw e;
+ }
+
+ // The |Authenticator| examines the connection as well and may determine it
+ // should be dropped.
+ alive =
+ alive &&
+ authenticator.validateConnection({
+ host: settings.host,
+ port: settings.port,
+ socket: s,
+ });
+
+ let transport;
+ if (alive) {
+ transport = new DebuggerTransport(input, output);
+ } else {
+ // Something went wrong, close the streams.
+ input.close();
+ output.close();
+ }
+
+ return { transport, s };
+};
+
+/**
+ * Try to connect to a remote server socket.
+ *
+ * If successsful, the socket transport and its opened streams are returned.
+ * Typically, this will only fail if the host / port is unreachable. Other
+ * problems, such as security errors, will allow this stage to succeed, but then
+ * fail later when the streams are actually used.
+ * @return s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ * @return input nsIAsyncInputStream
+ * The socket's input stream.
+ * @return output nsIAsyncOutputStream
+ * The socket's output stream.
+ */
+var _attemptConnect = async function ({ host, port }) {
+ const s = socketTransportService.createTransport([], host, port, null, null);
+
+ // Force disabling IPV6 if we aren't explicitely connecting to an IPv6 address
+ // It fails intermitently on MacOS when opening the Browser Toolbox (bug 1615412)
+ if (!host.includes(":")) {
+ s.connectionFlags |= Ci.nsISocketTransport.DISABLE_IPV6;
+ }
+
+ // By default the CONNECT socket timeout is very long, 65535 seconds,
+ // so that if we race to be in CONNECT state while the server socket is still
+ // initializing, the connection is stuck in connecting state for 18.20 hours!
+ s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
+
+ let input;
+ let output;
+ return new Promise((resolve, reject) => {
+ s.setEventSink(
+ {
+ onTransportStatus(transport, status) {
+ if (status != Ci.nsISocketTransport.STATUS_CONNECTING_TO) {
+ return;
+ }
+ try {
+ input = s.openInputStream(0, 0, 0);
+ } catch (e) {
+ reject(e);
+ }
+ resolve({ s, input, output });
+ },
+ },
+ Services.tm.currentThread
+ );
+
+ // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
+ // where the nsISocketTransport gets shutdown in between its instantiation and
+ // the call to this method.
+ try {
+ output = s.openOutputStream(0, 0, 0);
+ } catch (e) {
+ reject(e);
+ }
+ }).catch(e => {
+ if (input) {
+ input.close();
+ }
+ if (output) {
+ output.close();
+ }
+ DevToolsUtils.reportException("_attemptConnect", e);
+ });
+};
+
+/**
+ * Check if the input stream is alive.
+ */
+function _isInputAlive(input) {
+ return new Promise((resolve, reject) => {
+ input.asyncWait(
+ {
+ onInputStreamReady(stream) {
+ try {
+ stream.available();
+ resolve({ alive: true });
+ } catch (e) {
+ reject(e);
+ }
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ });
+}
+
+/**
+ * Creates a new socket listener for remote connections to the DevToolsServer.
+ * This helps contain and organize the parts of the server that may differ or
+ * are particular to one given listener mechanism vs. another.
+ * This can be closed at any later time by calling |close|.
+ * If remote connections are disabled, an error is thrown.
+ *
+ * @param {DevToolsServer} devToolsServer
+ * @param {Object} socketOptions
+ * options of socket as follows
+ * {
+ * authenticator:
+ * Controls the |Authenticator| used, which hooks various socket steps to
+ * implement an authentication policy. It is expected that different use
+ * cases may override pieces of the |Authenticator|. See auth.js.
+ * We set the default |Authenticator|, which is |Prompt|.
+ * discoverable:
+ * Controls whether this listener is announced via the service discovery
+ * mechanism. Defaults is false.
+ * fromBrowserToolbox:
+ * Should only be passed when opening a socket for a Browser Toolbox
+ * session. DevToolsSocketStatus will track the socket separately to
+ * avoid triggering the visual cue in the URL bar.
+ * portOrPath:
+ * The port or path to listen on.
+ * If given an integer, the port to listen on. Use -1 to choose any available
+ * port. Otherwise, the path to the unix socket domain file to listen on.
+ * Defaults is null.
+ * webSocket:
+ * Whether to use WebSocket protocol. Defaults is false.
+ * }
+ */
+function SocketListener(devToolsServer, socketOptions) {
+ this._devToolsServer = devToolsServer;
+
+ // Set socket options with default value
+ this._socketOptions = {
+ authenticator:
+ socketOptions.authenticator || new (Authenticators.get().Server)(),
+ discoverable: !!socketOptions.discoverable,
+ fromBrowserToolbox: !!socketOptions.fromBrowserToolbox,
+ portOrPath: socketOptions.portOrPath || null,
+ webSocket: !!socketOptions.webSocket,
+ };
+
+ EventEmitter.decorate(this);
+}
+
+SocketListener.prototype = {
+ get authenticator() {
+ return this._socketOptions.authenticator;
+ },
+
+ get discoverable() {
+ return this._socketOptions.discoverable;
+ },
+
+ get fromBrowserToolbox() {
+ return this._socketOptions.fromBrowserToolbox;
+ },
+
+ get portOrPath() {
+ return this._socketOptions.portOrPath;
+ },
+
+ get webSocket() {
+ return this._socketOptions.webSocket;
+ },
+
+ /**
+ * Validate that all options have been set to a supported configuration.
+ */
+ _validateOptions() {
+ if (this.portOrPath === null) {
+ throw new Error("Must set a port / path to listen on.");
+ }
+ if (this.discoverable && !Number(this.portOrPath)) {
+ throw new Error("Discovery only supported for TCP sockets.");
+ }
+ },
+
+ /**
+ * Listens on the given port or socket file for remote debugger connections.
+ */
+ open() {
+ this._validateOptions();
+ this._devToolsServer.addSocketListener(this);
+
+ let flags = Ci.nsIServerSocket.KeepWhenOffline;
+ // A preference setting can force binding on the loopback interface.
+ if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+ flags |= Ci.nsIServerSocket.LoopbackOnly;
+ }
+
+ const self = this;
+ return (async function () {
+ const backlog = 4;
+ self._socket = self._createSocketInstance();
+ if (self.isPortBased) {
+ const port = Number(self.portOrPath);
+ self._socket.initSpecialConnection(port, flags, backlog);
+ } else if (self.portOrPath.startsWith("/")) {
+ const file = nsFile(self.portOrPath);
+ if (file.exists()) {
+ file.remove(false);
+ }
+ self._socket.initWithFilename(file, parseInt("666", 8), backlog);
+ } else {
+ // Path isn't absolute path, so we use abstract socket address
+ self._socket.initWithAbstractAddress(self.portOrPath, backlog);
+ }
+ self._socket.asyncListen(self);
+ dumpn("Socket listening on: " + (self.port || self.portOrPath));
+ })()
+ .then(() => {
+ lazy.DevToolsSocketStatus.notifySocketOpened({
+ fromBrowserToolbox: self.fromBrowserToolbox,
+ });
+ this._advertise();
+ })
+ .catch(e => {
+ dumpn(
+ "Could not start debugging listener on '" +
+ this.portOrPath +
+ "': " +
+ e
+ );
+ this.close();
+ });
+ },
+
+ _advertise() {
+ if (!this.discoverable || !this.port) {
+ return;
+ }
+
+ const advertisement = {
+ port: this.port,
+ };
+
+ this.authenticator.augmentAdvertisement(this, advertisement);
+
+ discovery.addService("devtools", advertisement);
+ },
+
+ _createSocketInstance() {
+ return Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ },
+
+ /**
+ * Closes the SocketListener. Notifies the server to remove the listener from
+ * the set of active SocketListeners.
+ */
+ close() {
+ if (this.discoverable && this.port) {
+ discovery.removeService("devtools");
+ }
+ if (this._socket) {
+ this._socket.close();
+ this._socket = null;
+
+ lazy.DevToolsSocketStatus.notifySocketClosed({
+ fromBrowserToolbox: this.fromBrowserToolbox,
+ });
+ }
+ this._devToolsServer.removeSocketListener(this);
+ },
+
+ get host() {
+ if (!this._socket) {
+ return null;
+ }
+ if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+ return "127.0.0.1";
+ }
+ return "0.0.0.0";
+ },
+
+ /**
+ * Gets whether this listener uses a port number vs. a path.
+ */
+ get isPortBased() {
+ return !!Number(this.portOrPath);
+ },
+
+ /**
+ * Gets the port that a TCP socket listener is listening on, or null if this
+ * is not a TCP socket (so there is no port).
+ */
+ get port() {
+ if (!this.isPortBased || !this._socket) {
+ return null;
+ }
+ return this._socket.port;
+ },
+
+ onAllowedConnection(transport) {
+ dumpn("onAllowedConnection, transport: " + transport);
+ this.emit("accepted", transport, this);
+ },
+
+ // nsIServerSocketListener implementation
+
+ onSocketAccepted: DevToolsUtils.makeInfallible(function (
+ socket,
+ socketTransport
+ ) {
+ const connection = new ServerSocketConnection(this, socketTransport);
+ connection.once("allowed", this.onAllowedConnection.bind(this));
+ },
+ "SocketListener.onSocketAccepted"),
+
+ onStopListening(socket, status) {
+ dumpn("onStopListening, status: " + status);
+ },
+};
+
+/**
+ * A |ServerSocketConnection| is created by a |SocketListener| for each accepted
+ * incoming socket.
+ */
+function ServerSocketConnection(listener, socketTransport) {
+ this._listener = listener;
+ this._socketTransport = socketTransport;
+ this._handle();
+ EventEmitter.decorate(this);
+}
+
+ServerSocketConnection.prototype = {
+ get authentication() {
+ return this._listener.authenticator.mode;
+ },
+
+ get host() {
+ return this._socketTransport.host;
+ },
+
+ get port() {
+ return this._socketTransport.port;
+ },
+
+ get address() {
+ return this.host + ":" + this.port;
+ },
+
+ get client() {
+ const client = {
+ host: this.host,
+ port: this.port,
+ };
+ return client;
+ },
+
+ get server() {
+ const server = {
+ host: this._listener.host,
+ port: this._listener.port,
+ };
+ return server;
+ },
+
+ /**
+ * This is the main authentication workflow. If any pieces reject a promise,
+ * the connection is denied. If the entire process resolves successfully,
+ * the connection is finally handed off to the |DevToolsServer|.
+ */
+ async _handle() {
+ dumpn("Debugging connection starting authentication on " + this.address);
+ try {
+ await this._createTransport();
+ await this._authenticate();
+ this.allow();
+ } catch (e) {
+ this.deny(e);
+ }
+ },
+
+ /**
+ * We need to open the streams early on, as that is required in the case of
+ * TLS sockets to keep the handshake moving.
+ */
+ async _createTransport() {
+ const input = this._socketTransport.openInputStream(0, 0, 0);
+ const output = this._socketTransport.openOutputStream(0, 0, 0);
+
+ if (this._listener.webSocket) {
+ const socket = await WebSocketServer.accept(
+ this._socketTransport,
+ input,
+ output
+ );
+ this._transport = new WebSocketDebuggerTransport(socket);
+ } else {
+ this._transport = new DebuggerTransport(input, output);
+ }
+
+ // Start up the transport to observe the streams in case they are closed
+ // early. This allows us to clean up our state as well.
+ this._transport.hooks = {
+ onTransportClosed: reason => {
+ this.deny(reason);
+ },
+ };
+ this._transport.ready();
+ },
+
+ async _authenticate() {
+ const result = await this._listener.authenticator.authenticate({
+ client: this.client,
+ server: this.server,
+ transport: this._transport,
+ });
+
+ // If result is fine, we can stop here
+ if (
+ result === AuthenticationResult.ALLOW ||
+ result === AuthenticationResult.ALLOW_PERSIST
+ ) {
+ return;
+ }
+
+ if (result === AuthenticationResult.DISABLE_ALL) {
+ this._listener._devToolsServer.closeAllSocketListeners();
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
+ }
+
+ // If we got an error (DISABLE_ALL, DENY, …), let's throw a NS_ERROR_CONNECTION_REFUSED
+ // exception
+ throw Components.Exception("", Cr.NS_ERROR_CONNECTION_REFUSED);
+ },
+
+ deny(result) {
+ if (this._destroyed) {
+ return;
+ }
+ let errorName = result;
+ for (const name in Cr) {
+ if (Cr[name] === result) {
+ errorName = name;
+ break;
+ }
+ }
+ dumpn(
+ "Debugging connection denied on " + this.address + " (" + errorName + ")"
+ );
+ if (this._transport) {
+ this._transport.hooks = null;
+ this._transport.close(result);
+ }
+ this._socketTransport.close(result);
+ this.destroy();
+ },
+
+ allow() {
+ if (this._destroyed) {
+ return;
+ }
+ dumpn("Debugging connection allowed on " + this.address);
+ this.emit("allowed", this._transport);
+ this.destroy();
+ },
+
+ destroy() {
+ this._destroyed = true;
+ this._listener = null;
+ this._socketTransport = null;
+ this._transport = null;
+ },
+};
+
+exports.DebuggerSocket = DebuggerSocket;
+exports.SocketListener = SocketListener;
diff --git a/devtools/shared/security/tests/chrome/chrome.toml b/devtools/shared/security/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..036961ca81
--- /dev/null
+++ b/devtools/shared/security/tests/chrome/chrome.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = "devtools"
+
+["test_websocket-transport.html"]
diff --git a/devtools/shared/security/tests/chrome/test_websocket-transport.html b/devtools/shared/security/tests/chrome/test_websocket-transport.html
new file mode 100644
index 0000000000..8e4652fb1d
--- /dev/null
+++ b/devtools/shared/security/tests/chrome/test_websocket-transport.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test the WebSocket debugger transport</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>
+"use strict";
+
+window.onload = function() {
+ const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ // eslint-disable-next-line mozilla/reject-some-requires
+ const {DevToolsClient} = require("devtools/client/devtools-client");
+ const {DevToolsServer} = require("devtools/server/devtools-server");
+ const { SocketListener } = require("devtools/shared/security/socket");
+
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+ Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
+
+ SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ Services.prefs.clearUserPref("devtools.debugger.prompt-connection");
+ });
+
+ add_task(async function() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ is(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ const socketOptions = {
+ portOrPath: -1,
+ webSocket: true,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ is(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ webSocket: true,
+ });
+ ok(transport, "Client transport created");
+
+ const client = new DevToolsClient(transport);
+ const onUnexpectedClose = () => {
+ ok(false, "Closed unexpectedly");
+ };
+ client.on("closed", onUnexpectedClose);
+
+ await client.connect();
+
+ // Send a message the server
+ const reply = await client.mainRoot.getRoot();
+ is(reply.from, "root", "Got expected response");
+
+ client.off("closed", onUnexpectedClose);
+ transport.close();
+ listener.close();
+ is(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ DevToolsServer.destroy();
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/shared/security/tests/xpcshell/.eslintrc.js b/devtools/shared/security/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/security/tests/xpcshell/head_dbg.js b/devtools/shared/security/tests/xpcshell/head_dbg.js
new file mode 100644
index 0000000000..30359e6ac3
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/head_dbg.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* exported DevToolsClient, initTestDevToolsServer */
+
+const { loader, require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const xpcInspector = require("xpcInspector");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+// We need to require lazily since will be crashed if we load SocketListener too early
+// in xpc shell test due to SocketListener loads PSM module.
+loader.lazyRequireGetter(
+ this,
+ "SocketListener",
+ "resource://devtools/shared/security/socket.js",
+ true
+);
+
+// We do not want to log packets by default, because in some tests,
+// we can be sending large amounts of data. The test harness has
+// trouble dealing with logging all the data, and we end up with
+// intermittent time outs (e.g. bug 775924).
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// Services.prefs.setBoolPref("devtools.debugger.log.verbose", true);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+
+// Convert an nsIScriptError 'logLevel' value into an appropriate string.
+function scriptErrorLogLevel(message) {
+ switch (message.logLevel) {
+ case Ci.nsIConsoleMessage.info:
+ return "info";
+ case Ci.nsIConsoleMessage.warn:
+ return "warning";
+ default:
+ Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error);
+ return "error";
+ }
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+var listener = {
+ observe(message) {
+ let string;
+ try {
+ message.QueryInterface(Ci.nsIScriptError);
+ dump(
+ message.sourceName +
+ ":" +
+ message.lineNumber +
+ ": " +
+ scriptErrorLogLevel(message) +
+ ": " +
+ message.errorMessage +
+ "\n"
+ );
+ string = message.errorMessage;
+ } catch (ex) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ string = "" + message.message;
+ } catch (e) {
+ string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (xpcInspector.eventLoopNestLevel > 0) {
+ xpcInspector.exitNestedEventLoop();
+ }
+
+ info("head_dbg.js got console message: " + string + "\n");
+ },
+};
+
+Services.console.registerListener(listener);
+
+/**
+ * Initialize the testing devtools server.
+ */
+function initTestDevToolsServer() {
+ const { createRootActor } = require("xpcshell-test/testactors");
+ DevToolsServer.setRootActor(createRootActor);
+ DevToolsServer.init();
+}
diff --git a/devtools/shared/security/tests/xpcshell/test_devtools_socket_status.js b/devtools/shared/security/tests/xpcshell/test_devtools_socket_status.js
new file mode 100644
index 0000000000..2e41583b05
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/test_devtools_socket_status.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+} = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+);
+
+const { DevToolsSocketStatus } = ChromeUtils.importESModule(
+ "resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs"
+);
+
+add_task(async function () {
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+ Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
+
+ info("Without any server started, all states should be set to false");
+ checkSocketStatus(false, false);
+
+ info("Start a first server, expect all states to change to true");
+ const server = await setupDevToolsServer({ fromBrowserToolbox: false });
+ checkSocketStatus(true, true);
+
+ info("Start another server, expect all states to remain true");
+ const otherServer = await setupDevToolsServer({ fromBrowserToolbox: false });
+ checkSocketStatus(true, true);
+
+ info("Shutdown one of the servers, expect all states to remain true");
+ teardownDevToolsServer(otherServer);
+ checkSocketStatus(true, true);
+
+ info("Shutdown the other server, expect all states to change to false");
+ teardownDevToolsServer(server);
+ checkSocketStatus(false, false);
+
+ info(
+ "Start a 'browser toolbox' server, expect only the 'include' state to become true"
+ );
+ const browserToolboxServer = await setupDevToolsServer({
+ fromBrowserToolbox: true,
+ });
+ checkSocketStatus(true, false);
+
+ info(
+ "Shutdown the 'browser toolbox' server, expect all states to become false"
+ );
+ teardownDevToolsServer(browserToolboxServer);
+ checkSocketStatus(false, false);
+
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ Services.prefs.clearUserPref("devtools.debugger.prompt-connection");
+});
+
+function checkSocketStatus(expectedExcludeFalse, expectedExcludeTrue) {
+ const openedDefault = DevToolsSocketStatus.hasSocketOpened();
+ const openedExcludeFalse = DevToolsSocketStatus.hasSocketOpened({
+ excludeBrowserToolboxSockets: false,
+ });
+ const openedExcludeTrue = DevToolsSocketStatus.hasSocketOpened({
+ excludeBrowserToolboxSockets: true,
+ });
+
+ equal(
+ openedDefault,
+ openedExcludeFalse,
+ "DevToolsSocketStatus.hasSocketOpened should default to excludeBrowserToolboxSockets=false"
+ );
+ equal(
+ openedExcludeFalse,
+ expectedExcludeFalse,
+ "DevToolsSocketStatus matches the expectation for excludeBrowserToolboxSockets=false"
+ );
+ equal(
+ openedExcludeTrue,
+ expectedExcludeTrue,
+ "DevToolsSocketStatus matches the expectation for excludeBrowserToolboxSockets=true"
+ );
+}
+
+async function setupDevToolsServer({ fromBrowserToolbox }) {
+ info("Use the dedicated system principal loader for the DevToolsServer.");
+ const requester = {};
+ const loader = useDistinctSystemPrincipalLoader(requester);
+
+ const { DevToolsServer } = loader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+ const socketOptions = {
+ fromBrowserToolbox,
+ // Pass -1 to automatically choose an available port
+ portOrPath: -1,
+ };
+
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+
+ // Note that useDistinctSystemPrincipalLoader will lead to reuse the same
+ // loader if we are creating several servers in a row. The DevToolsServer
+ // singleton might already have sockets opened.
+ const listeningSockets = DevToolsServer.listeningSockets;
+ await listener.open();
+ equal(
+ DevToolsServer.listeningSockets,
+ listeningSockets + 1,
+ "A new listening socket was created"
+ );
+
+ return { DevToolsServer, listener, requester };
+}
+
+function teardownDevToolsServer({ DevToolsServer, listener, requester }) {
+ info("Close the listener socket");
+ const listeningSockets = DevToolsServer.listeningSockets;
+ listener.close();
+ equal(
+ DevToolsServer.listeningSockets,
+ listeningSockets - 1,
+ "A listening socket was closed"
+ );
+
+ if (DevToolsServer.listeningSockets == 0) {
+ info("Destroy the temporary devtools server");
+ DevToolsServer.destroy();
+ }
+
+ if (requester) {
+ releaseDistinctSystemPrincipalLoader(requester);
+ }
+}
diff --git a/devtools/shared/security/tests/xpcshell/testactors.js b/devtools/shared/security/tests/xpcshell/testactors.js
new file mode 100644
index 0000000000..0a35f05287
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/testactors.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { RootActor } = require("resource://devtools/server/actors/root.js");
+const {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+
+exports.createRootActor = function createRootActor(connection) {
+ const root = new RootActor(connection, {
+ globalActorFactories: ActorRegistry.globalActorFactories,
+ });
+ root.applicationType = "xpcshell-tests";
+ return root;
+};
diff --git a/devtools/shared/security/tests/xpcshell/xpcshell.toml b/devtools/shared/security/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..f0d3c8f1d2
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = "devtools"
+head = "head_dbg.js"
+skip-if = ["os == 'android'"]
+firefox-appdir = "browser"
+support-files = ["testactors.js"]
+
+["test_devtools_socket_status.js"]
diff --git a/devtools/shared/specs/accessibility.js b/devtools/shared/specs/accessibility.js
new file mode 100644
index 0000000000..acfd7db068
--- /dev/null
+++ b/devtools/shared/specs/accessibility.js
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, generateActorSpec, RetVal, types } = protocol;
+
+types.addActorType("accessible");
+
+/**
+ * Accessible with children listed in the ancestry structure calculated by the
+ * walker.
+ */
+types.addDictType("accessibleWithChildren", {
+ // Accessible
+ accessible: "accessible",
+ // Accessible's children
+ children: "array:accessible",
+});
+
+/**
+ * Data passed via "audit-event" to the client. It may include type, a list of
+ * ancestries for accessible actors that have failing accessibility checks or
+ * a progress information.
+ */
+types.addDictType("auditEventData", {
+ type: "string",
+ // List of ancestries (array:accessibleWithChildren)
+ ancestries: "nullable:array:array:accessibleWithChildren",
+ // Audit progress information
+ progress: "nullable:json",
+});
+
+/**
+ * Accessible relation object described by its type that also includes relation targets.
+ */
+types.addDictType("accessibleRelation", {
+ // Accessible relation type
+ type: "string",
+ // Accessible relation's targets
+ targets: "array:accessible",
+});
+
+const accessibleSpec = generateActorSpec({
+ typeName: "accessible",
+
+ events: {
+ "actions-change": {
+ type: "actionsChange",
+ actions: Arg(0, "array:string"),
+ },
+ "name-change": {
+ type: "nameChange",
+ name: Arg(0, "string"),
+ parent: Arg(1, "nullable:accessible"),
+ },
+ "value-change": {
+ type: "valueChange",
+ value: Arg(0, "string"),
+ },
+ "description-change": {
+ type: "descriptionChange",
+ description: Arg(0, "string"),
+ },
+ "states-change": {
+ type: "statesChange",
+ states: Arg(0, "array:string"),
+ },
+ "attributes-change": {
+ type: "attributesChange",
+ attributes: Arg(0, "json"),
+ },
+ "shortcut-change": {
+ type: "shortcutChange",
+ shortcut: Arg(0, "string"),
+ },
+ reorder: {
+ type: "reorder",
+ childCount: Arg(0, "number"),
+ },
+ "text-change": {
+ type: "textChange",
+ },
+ "index-in-parent-change": {
+ type: "indexInParentChange",
+ indexInParent: Arg(0, "number"),
+ },
+ audited: {
+ type: "audited",
+ audit: Arg(0, "nullable:json"),
+ },
+ },
+
+ methods: {
+ audit: {
+ request: { options: Arg(0, "nullable:json") },
+ response: {
+ audit: RetVal("nullable:json"),
+ },
+ },
+ children: {
+ request: {},
+ response: {
+ children: RetVal("array:accessible"),
+ },
+ },
+ getRelations: {
+ request: {},
+ response: {
+ relations: RetVal("array:accessibleRelation"),
+ },
+ },
+ hydrate: {
+ request: {},
+ response: {
+ properties: RetVal("json"),
+ },
+ },
+ snapshot: {
+ request: {},
+ response: {
+ snapshot: RetVal("json"),
+ },
+ },
+ },
+});
+
+const accessibleWalkerSpec = generateActorSpec({
+ typeName: "accessiblewalker",
+
+ events: {
+ "document-ready": {
+ type: "documentReady",
+ },
+ "picker-accessible-picked": {
+ type: "pickerAccessiblePicked",
+ accessible: Arg(0, "nullable:accessible"),
+ },
+ "picker-accessible-previewed": {
+ type: "pickerAccessiblePreviewed",
+ accessible: Arg(0, "nullable:accessible"),
+ },
+ "picker-accessible-hovered": {
+ type: "pickerAccessibleHovered",
+ accessible: Arg(0, "nullable:accessible"),
+ },
+ "picker-accessible-canceled": {
+ type: "pickerAccessibleCanceled",
+ },
+ "highlighter-event": {
+ type: "highlighter-event",
+ data: Arg(0, "json"),
+ },
+ "audit-event": {
+ type: "audit-event",
+ audit: Arg(0, "auditEventData"),
+ },
+ },
+
+ methods: {
+ children: {
+ request: {},
+ response: {
+ children: RetVal("array:accessible"),
+ },
+ },
+ getAccessibleFor: {
+ request: { node: Arg(0, "domnode") },
+ response: {
+ accessible: RetVal("nullable:accessible"),
+ },
+ },
+ getAncestry: {
+ request: { accessible: Arg(0, "accessible") },
+ response: {
+ ancestry: RetVal("array:accessibleWithChildren"),
+ },
+ },
+ startAudit: {
+ request: { options: Arg(0, "nullable:json") },
+ },
+ highlightAccessible: {
+ request: {
+ accessible: Arg(0, "accessible"),
+ options: Arg(1, "nullable:json"),
+ },
+ response: {
+ value: RetVal("nullable:boolean"),
+ },
+ },
+ unhighlight: {
+ request: {},
+ },
+ pick: {},
+ pickAndFocus: {},
+ cancelPick: {},
+ showTabbingOrder: {
+ request: {
+ elm: Arg(0, "domnode"),
+ index: Arg(1, "number"),
+ },
+ response: {
+ tabbingOrderInfo: RetVal("json"),
+ },
+ },
+ hideTabbingOrder() {},
+ },
+});
+
+const simulatorSpec = generateActorSpec({
+ typeName: "simulator",
+
+ methods: {
+ simulate: {
+ request: { options: Arg(0, "nullable:json") },
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ },
+});
+
+const accessibilitySpec = generateActorSpec({
+ typeName: "accessibility",
+
+ events: {
+ init: {
+ type: "init",
+ },
+ shutdown: {
+ type: "shutdown",
+ },
+ },
+
+ methods: {
+ getTraits: {
+ request: {},
+ response: { traits: RetVal("json") },
+ },
+ bootstrap: {
+ request: {},
+ response: {
+ state: RetVal("json"),
+ },
+ },
+ getWalker: {
+ request: {},
+ response: {
+ walker: RetVal("accessiblewalker"),
+ },
+ },
+ getSimulator: {
+ request: {},
+ response: {
+ simulator: RetVal("nullable:simulator"),
+ },
+ },
+ },
+});
+
+const parentAccessibilitySpec = generateActorSpec({
+ typeName: "parentaccessibility",
+
+ events: {
+ "can-be-disabled-change": {
+ type: "canBeDisabledChange",
+ canBeDisabled: Arg(0, "boolean"),
+ },
+ "can-be-enabled-change": {
+ type: "canBeEnabledChange",
+ canBeEnabled: Arg(0, "boolean"),
+ },
+ },
+
+ methods: {
+ bootstrap: {
+ request: {},
+ response: {
+ state: RetVal("json"),
+ },
+ },
+ enable: {
+ request: {},
+ response: {},
+ },
+ disable: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+exports.accessibleSpec = accessibleSpec;
+exports.accessibleWalkerSpec = accessibleWalkerSpec;
+exports.accessibilitySpec = accessibilitySpec;
+exports.parentAccessibilitySpec = parentAccessibilitySpec;
+exports.simulatorSpec = simulatorSpec;
diff --git a/devtools/shared/specs/addon/addons.js b/devtools/shared/specs/addon/addons.js
new file mode 100644
index 0000000000..309a7acdf5
--- /dev/null
+++ b/devtools/shared/specs/addon/addons.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";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const addonsSpec = generateActorSpec({
+ typeName: "addons",
+
+ methods: {
+ installTemporaryAddon: {
+ request: {
+ addonPath: Arg(0, "string"),
+ openDevTools: Arg(1, "nullable:boolean"),
+ },
+ response: { addon: RetVal("json") },
+ },
+
+ uninstallAddon: {
+ request: {
+ addonId: Arg(0, "string"),
+ },
+ response: {},
+ },
+ },
+});
+
+exports.addonsSpec = addonsSpec;
diff --git a/devtools/shared/specs/addon/moz.build b/devtools/shared/specs/addon/moz.build
new file mode 100644
index 0000000000..e382173641
--- /dev/null
+++ b/devtools/shared/specs/addon/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(
+ "addons.js",
+ "webextension-inspected-window.js",
+)
diff --git a/devtools/shared/specs/addon/webextension-inspected-window.js b/devtools/shared/specs/addon/webextension-inspected-window.js
new file mode 100644
index 0000000000..3a0cf166ed
--- /dev/null
+++ b/devtools/shared/specs/addon/webextension-inspected-window.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+/**
+ * Sent with the eval and reload requests, used to inform the
+ * webExtensionInspectedWindowActor about the caller information
+ * to be able to evaluate code as being executed from the caller
+ * WebExtension sources, or log errors with information that can
+ * help the addon developer to more easily identify the affected
+ * lines in his own addon code.
+ */
+types.addDictType("webExtensionCallerInfo", {
+ // Information related to the line of code that has originated
+ // the request.
+ url: "string",
+ lineNumber: "nullable:number",
+
+ // The called addonId.
+ addonId: "string",
+});
+
+/**
+ * RDP type related to the inspectedWindow.eval method request.
+ */
+types.addDictType("webExtensionEvalOptions", {
+ frameURL: "nullable:string",
+ contextSecurityOrigin: "nullable:string",
+ useContentScriptContext: "nullable:boolean",
+
+ // Return the evalResult as a grip (used by the WebExtensions
+ // devtools inspector's sidebar.setExpression API method).
+ evalResultAsGrip: "nullable:boolean",
+
+ // The actor ID of the node selected in the inspector if any,
+ // used to provide the '$0' binding.
+ toolboxSelectedNodeActorID: "nullable:string",
+
+ // The actor ID of the console actor,
+ // used to provide the 'inspect' binding.
+ toolboxConsoleActorID: "nullable:string",
+});
+
+/**
+ * RDP type related to the inspectedWindow.eval method result errors.
+ *
+ * This type has been modelled on the same data format
+ * used in the corresponding chrome API method.
+ */
+types.addDictType("webExtensionEvalExceptionInfo", {
+ // The following properties are set if the error has not occurred
+ // in the evaluated JS code.
+ isError: "nullable:boolean",
+ code: "nullable:string",
+ description: "nullable:string",
+ details: "nullable:array:json",
+
+ // The following properties are set if the error has occurred
+ // in the evaluated JS code.
+ isException: "nullable:string",
+ value: "nullable:string",
+});
+
+/**
+ * RDP type related to the inspectedWindow.eval method result.
+ */
+types.addDictType("webExtensionEvalResult", {
+ // The following properties are set if the evaluation has been
+ // completed successfully.
+ value: "nullable:json",
+ valueGrip: "nullable:json",
+ // The following properties are set if the evalutation has been
+ // completed with errors.
+ exceptionInfo: "nullable:webExtensionEvalExceptionInfo",
+});
+
+/**
+ * RDP type related to the inspectedWindow.reload method request.
+ */
+types.addDictType("webExtensionReloadOptions", {
+ ignoreCache: "nullable:boolean",
+ userAgent: "nullable:string",
+ injectedScript: "nullable:string",
+});
+
+const webExtensionInspectedWindowSpec = generateActorSpec({
+ typeName: "webExtensionInspectedWindow",
+
+ methods: {
+ reload: {
+ request: {
+ webExtensionCallerInfo: Arg(0, "webExtensionCallerInfo"),
+ options: Arg(1, "webExtensionReloadOptions"),
+ },
+ },
+ eval: {
+ request: {
+ webExtensionCallerInfo: Arg(0, "webExtensionCallerInfo"),
+ expression: Arg(1, "string"),
+ options: Arg(2, "webExtensionEvalOptions"),
+ },
+
+ response: {
+ evalResult: RetVal("webExtensionEvalResult"),
+ },
+ },
+ },
+});
+
+exports.webExtensionInspectedWindowSpec = webExtensionInspectedWindowSpec;
diff --git a/devtools/shared/specs/animation.js b/devtools/shared/specs/animation.js
new file mode 100644
index 0000000000..95acd606b5
--- /dev/null
+++ b/devtools/shared/specs/animation.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+/**
+ * Sent with the 'mutations' event as part of an array of changes, used to
+ * inform fronts of the type of change that occured.
+ */
+types.addDictType("animationMutationChange", {
+ // The type of change ("added" or "removed").
+ type: "string",
+ // The changed AnimationPlayerActor.
+ player: "animationplayer",
+});
+
+const animationPlayerSpec = generateActorSpec({
+ typeName: "animationplayer",
+
+ events: {
+ changed: {
+ type: "changed",
+ state: Arg(0, "json"),
+ },
+ },
+
+ methods: {
+ release: { release: true },
+ getCurrentState: {
+ request: {},
+ response: {
+ data: RetVal("json"),
+ },
+ },
+ getAnimationTypes: {
+ request: {
+ propertyNames: Arg(0, "array:string"),
+ },
+ response: {
+ animationTypes: RetVal("json"),
+ },
+ },
+ },
+});
+
+exports.animationPlayerSpec = animationPlayerSpec;
+
+const animationsSpec = generateActorSpec({
+ typeName: "animations",
+
+ events: {
+ mutations: {
+ type: "mutations",
+ changes: Arg(0, "array:animationMutationChange"),
+ },
+ },
+
+ methods: {
+ setWalkerActor: {
+ request: {
+ walker: Arg(0, "domwalker"),
+ },
+ response: {},
+ },
+ getAnimationPlayersForNode: {
+ request: {
+ actorID: Arg(0, "domnode"),
+ },
+ response: {
+ players: RetVal("array:animationplayer"),
+ },
+ },
+ pauseSome: {
+ request: {
+ players: Arg(0, "array:animationplayer"),
+ },
+ response: {},
+ },
+ playSome: {
+ request: {
+ players: Arg(0, "array:animationplayer"),
+ },
+ response: {},
+ },
+ setCurrentTimes: {
+ request: {
+ players: Arg(0, "array:animationplayer"),
+ time: Arg(1, "number"),
+ shouldPause: Arg(2, "boolean"),
+ },
+ response: {},
+ },
+ setPlaybackRates: {
+ request: {
+ players: Arg(0, "array:animationplayer"),
+ rate: Arg(1, "number"),
+ },
+ response: {},
+ },
+ },
+});
+
+exports.animationsSpec = animationsSpec;
diff --git a/devtools/shared/specs/array-buffer.js b/devtools/shared/specs/array-buffer.js
new file mode 100644
index 0000000000..ba7650ff61
--- /dev/null
+++ b/devtools/shared/specs/array-buffer.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal, generateActorSpec } = protocol;
+
+const arrayBufferSpec = generateActorSpec({
+ typeName: "arraybuffer",
+
+ methods: {
+ slice: {
+ request: {
+ start: Arg(0),
+ count: Arg(1),
+ },
+ response: RetVal("json"),
+ },
+ release: { release: true },
+ },
+});
+
+exports.arrayBufferSpec = arrayBufferSpec;
diff --git a/devtools/shared/specs/blackboxing.js b/devtools/shared/specs/blackboxing.js
new file mode 100644
index 0000000000..baf773f77f
--- /dev/null
+++ b/devtools/shared/specs/blackboxing.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Arg,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("blackboxing.position", {
+ line: "number",
+ column: "number",
+});
+
+types.addDictType("blackboxing.range", {
+ start: "blackboxing.position",
+ end: "blackboxing.position",
+});
+
+const blackboxingSpec = generateActorSpec({
+ typeName: "blackboxing",
+
+ methods: {
+ blackbox: {
+ request: {
+ url: Arg(0, "string"),
+ range: Arg(1, "array:blackboxing.range"),
+ },
+ },
+ unblackbox: {
+ request: {
+ url: Arg(0, "string"),
+ range: Arg(1, "array:blackboxing.range"),
+ },
+ },
+ },
+});
+
+exports.blackboxingSpec = blackboxingSpec;
diff --git a/devtools/shared/specs/breakpoint-list.js b/devtools/shared/specs/breakpoint-list.js
new file mode 100644
index 0000000000..3e4e0eff00
--- /dev/null
+++ b/devtools/shared/specs/breakpoint-list.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/. */
+
+"use strict";
+
+const {
+ generateActorSpec,
+ Arg,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("breakpoint-list.breakpoint-options", {
+ condition: "nullable:string",
+ logValue: "nullable:string",
+});
+
+const breakpointListSpec = generateActorSpec({
+ typeName: "breakpoint-list",
+
+ methods: {
+ setBreakpoint: {
+ request: {
+ location: Arg(0, "json"),
+ options: Arg(1, "breakpoint-list.breakpoint-options"),
+ },
+ },
+ removeBreakpoint: {
+ request: {
+ location: Arg(0, "json"),
+ },
+ },
+
+ setXHRBreakpoint: {
+ request: {
+ path: Arg(0, "string"),
+ method: Arg(1, "string"),
+ },
+ },
+ removeXHRBreakpoint: {
+ request: {
+ path: Arg(0, "string"),
+ method: Arg(1, "string"),
+ },
+ },
+ setActiveEventBreakpoints: {
+ request: {
+ ids: Arg(0, "array:string"),
+ },
+ },
+ },
+});
+
+exports.breakpointListSpec = breakpointListSpec;
diff --git a/devtools/shared/specs/changes.js b/devtools/shared/specs/changes.js
new file mode 100644
index 0000000000..ba8f2fd042
--- /dev/null
+++ b/devtools/shared/specs/changes.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 {
+ Arg,
+ generateActorSpec,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const changesSpec = generateActorSpec({
+ typeName: "changes",
+
+ events: {
+ "add-change": {
+ type: "addChange",
+ change: Arg(0, "json"),
+ },
+ "remove-change": {
+ type: "removeChange",
+ change: Arg(0, "json"),
+ },
+ "clear-changes": {
+ type: "clearChanges",
+ },
+ },
+
+ methods: {
+ allChanges: {
+ response: {
+ changes: RetVal("array:json"),
+ },
+ },
+ start: {}, // no arguments, no response
+ },
+});
+
+exports.changesSpec = changesSpec;
diff --git a/devtools/shared/specs/compatibility.js b/devtools/shared/specs/compatibility.js
new file mode 100644
index 0000000000..0af211860a
--- /dev/null
+++ b/devtools/shared/specs/compatibility.js
@@ -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/. */
+
+"use strict";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("browsertype", {
+ id: "string",
+ name: "string",
+ version: "string",
+ status: "string",
+});
+
+types.addDictType("compatibilityissues", {
+ type: "string",
+ property: "string",
+ aliases: "nullable:array:string",
+ url: "nullable:string",
+ specUrl: "nullable:string",
+ deprecated: "boolean",
+ experimental: "boolean",
+ unsupportedBrowsers: "array:browsertype",
+});
+
+types.addDictType("declaration", {
+ name: "string",
+ value: "string",
+});
+
+const compatibilitySpec = generateActorSpec({
+ typeName: "compatibility",
+
+ methods: {
+ // While not being used on the client at the moment, keep this method in case
+ // we need traits again to support backwards compatibility for the Compatibility
+ // actor.
+ getTraits: {
+ request: {},
+ response: { traits: RetVal("json") },
+ },
+ getCSSDeclarationBlockIssues: {
+ request: {
+ domRulesDeclarations: Arg(0, "array:array:declaration"),
+ targetBrowsers: Arg(1, "array:browsertype"),
+ },
+ response: {
+ compatibilityIssues: RetVal("array:array:compatibilityissues"),
+ },
+ },
+ getNodeCssIssues: {
+ request: {
+ node: Arg(0, "domnode"),
+ targetBrowsers: Arg(1, "array:browsertype"),
+ },
+ response: {
+ compatibilityIssues: RetVal("array:compatibilityissues"),
+ },
+ },
+ },
+});
+
+exports.compatibilitySpec = compatibilitySpec;
diff --git a/devtools/shared/specs/css-properties.js b/devtools/shared/specs/css-properties.js
new file mode 100644
index 0000000000..04aadd22c6
--- /dev/null
+++ b/devtools/shared/specs/css-properties.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const cssPropertiesSpec = generateActorSpec({
+ typeName: "cssProperties",
+
+ methods: {
+ getCSSDatabase: {
+ request: {},
+
+ response: RetVal("json"),
+ },
+ },
+});
+
+exports.cssPropertiesSpec = cssPropertiesSpec;
diff --git a/devtools/shared/specs/descriptors/moz.build b/devtools/shared/specs/descriptors/moz.build
new file mode 100644
index 0000000000..bf297b3dcb
--- /dev/null
+++ b/devtools/shared/specs/descriptors/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(
+ "process.js",
+ "tab.js",
+ "webextension.js",
+ "worker.js",
+)
diff --git a/devtools/shared/specs/descriptors/process.js b/devtools/shared/specs/descriptors/process.js
new file mode 100644
index 0000000000..f9cb1d4ce6
--- /dev/null
+++ b/devtools/shared/specs/descriptors/process.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 {
+ generateActorSpec,
+ RetVal,
+ Option,
+} = require("resource://devtools/shared/protocol.js");
+
+const processDescriptorSpec = generateActorSpec({
+ typeName: "processDescriptor",
+
+ methods: {
+ getTarget: {
+ request: {},
+ response: {
+ process: RetVal("json"),
+ },
+ },
+ getWatcher: {
+ request: {},
+ response: RetVal("watcher"),
+ },
+ reloadDescriptor: {
+ request: {
+ bypassCache: Option(0, "boolean"),
+ },
+ response: {},
+ },
+ },
+
+ events: {
+ "descriptor-destroyed": {
+ type: "descriptor-destroyed",
+ },
+ },
+});
+
+exports.processDescriptorSpec = processDescriptorSpec;
diff --git a/devtools/shared/specs/descriptors/tab.js b/devtools/shared/specs/descriptors/tab.js
new file mode 100644
index 0000000000..c2c0f69431
--- /dev/null
+++ b/devtools/shared/specs/descriptors/tab.js
@@ -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/. */
+"use strict";
+
+const {
+ generateActorSpec,
+ Option,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const tabDescriptorSpec = generateActorSpec({
+ typeName: "tabDescriptor",
+
+ methods: {
+ getTarget: {
+ request: {},
+ response: {
+ frame: RetVal("json"),
+ },
+ },
+ getFavicon: {
+ request: {},
+ response: {
+ favicon: RetVal("string"),
+ },
+ },
+ getWatcher: {
+ request: {
+ isServerTargetSwitchingEnabled: Option(0, "boolean"),
+ isPopupDebuggingEnabled: Option(0, "boolean"),
+ },
+ response: RetVal("watcher"),
+ },
+ reloadDescriptor: {
+ request: {
+ bypassCache: Option(0, "boolean"),
+ },
+ response: {},
+ },
+ },
+
+ events: {
+ "descriptor-destroyed": {
+ type: "descriptor-destroyed",
+ },
+ },
+});
+
+exports.tabDescriptorSpec = tabDescriptorSpec;
diff --git a/devtools/shared/specs/descriptors/webextension.js b/devtools/shared/specs/descriptors/webextension.js
new file mode 100644
index 0000000000..692896fc0a
--- /dev/null
+++ b/devtools/shared/specs/descriptors/webextension.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 {
+ RetVal,
+ generateActorSpec,
+ Option,
+} = require("resource://devtools/shared/protocol.js");
+
+const webExtensionDescriptorSpec = generateActorSpec({
+ typeName: "webExtensionDescriptor",
+
+ methods: {
+ reload: {
+ request: {},
+ response: { addon: RetVal("json") },
+ },
+
+ terminateBackgroundScript: {
+ request: {},
+ response: {},
+ },
+
+ // @backward-compat { version 70 } The method is now called getTarget
+ connect: {
+ request: {},
+ response: { form: RetVal("json") },
+ },
+
+ getTarget: {
+ request: {},
+ response: { form: RetVal("json") },
+ },
+
+ reloadDescriptor: {
+ request: {
+ bypassCache: Option(0, "boolean"),
+ },
+ response: {},
+ },
+ getWatcher: {
+ request: {
+ isServerTargetSwitchingEnabled: Option(0, "boolean"),
+ },
+ response: RetVal("watcher"),
+ },
+ },
+
+ events: {
+ "descriptor-destroyed": {
+ type: "descriptor-destroyed",
+ },
+ },
+});
+
+exports.webExtensionDescriptorSpec = webExtensionDescriptorSpec;
diff --git a/devtools/shared/specs/descriptors/worker.js b/devtools/shared/specs/descriptors/worker.js
new file mode 100644
index 0000000000..1bf21bcc0a
--- /dev/null
+++ b/devtools/shared/specs/descriptors/worker.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 {
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const workerDescriptorSpec = generateActorSpec({
+ typeName: "workerDescriptor",
+
+ methods: {
+ detach: {
+ request: {},
+ response: {},
+ },
+ getTarget: {
+ request: {},
+ response: RetVal("json"),
+ },
+ },
+
+ events: {
+ "descriptor-destroyed": {
+ type: "descriptor-destroyed",
+ },
+ },
+});
+
+exports.workerDescriptorSpec = workerDescriptorSpec;
diff --git a/devtools/shared/specs/device.js b/devtools/shared/specs/device.js
new file mode 100644
index 0000000000..cfe392950d
--- /dev/null
+++ b/devtools/shared/specs/device.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+const deviceSpec = generateActorSpec({
+ typeName: "device",
+
+ methods: {
+ getDescription: { request: {}, response: { value: RetVal("json") } },
+ },
+});
+
+exports.deviceSpec = deviceSpec;
diff --git a/devtools/shared/specs/environment.js b/devtools/shared/specs/environment.js
new file mode 100644
index 0000000000..e98d906838
--- /dev/null
+++ b/devtools/shared/specs/environment.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";
+
+const { generateActorSpec } = require("resource://devtools/shared/protocol.js");
+
+const environmentSpec = generateActorSpec({
+ typeName: "environment",
+
+ methods: {},
+});
+
+exports.environmentSpec = environmentSpec;
diff --git a/devtools/shared/specs/frame.js b/devtools/shared/specs/frame.js
new file mode 100644
index 0000000000..03127e1d9f
--- /dev/null
+++ b/devtools/shared/specs/frame.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 {
+ generateActorSpec,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const frameSpec = generateActorSpec({
+ typeName: "frame",
+
+ methods: {
+ getEnvironment: {
+ response: RetVal("json"),
+ },
+ },
+});
+
+exports.frameSpec = frameSpec;
diff --git a/devtools/shared/specs/heap-snapshot-file.js b/devtools/shared/specs/heap-snapshot-file.js
new file mode 100644
index 0000000000..9153fce005
--- /dev/null
+++ b/devtools/shared/specs/heap-snapshot-file.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const heapSnapshotFileSpec = generateActorSpec({
+ typeName: "heapSnapshotFile",
+
+ methods: {
+ transferHeapSnapshot: {
+ request: {
+ snapshotId: Arg(0, "string"),
+ },
+ },
+ },
+});
+
+exports.heapSnapshotFileSpec = heapSnapshotFileSpec;
diff --git a/devtools/shared/specs/highlighters.js b/devtools/shared/specs/highlighters.js
new file mode 100644
index 0000000000..94d437f797
--- /dev/null
+++ b/devtools/shared/specs/highlighters.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 {
+ Arg,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const customHighlighterSpec = generateActorSpec({
+ typeName: "customhighlighter",
+
+ events: {
+ "highlighter-event": {
+ type: "highlighter-event",
+ data: Arg(0, "json"),
+ },
+ },
+
+ methods: {
+ release: {
+ release: true,
+ },
+ show: {
+ request: {
+ node: Arg(0, "nullable:domnode"),
+ options: Arg(1, "nullable:json"),
+ },
+ response: {
+ value: RetVal("nullable:boolean"),
+ },
+ },
+ hide: {
+ request: {},
+ },
+ finalize: {
+ oneway: true,
+ },
+ },
+});
+
+exports.customHighlighterSpec = customHighlighterSpec;
diff --git a/devtools/shared/specs/index.js b/devtools/shared/specs/index.js
new file mode 100644
index 0000000000..6a6c6c0f89
--- /dev/null
+++ b/devtools/shared/specs/index.js
@@ -0,0 +1,409 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Registry indexing all specs and front modules
+//
+// All spec and front modules should be listed here
+// in order to be referenced by any other spec or front module.
+
+// Declare in which spec module and front module a set of types are defined.
+// This array should be sorted by `spec` attribute, and this is verified in the
+// test devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
+const Types = (exports.__TypesForTests = [
+ {
+ types: [
+ "accessible",
+ "accessiblewalker",
+ "accessibility",
+ "parentaccessibility",
+ ],
+ spec: "devtools/shared/specs/accessibility",
+ front: "devtools/client/fronts/accessibility",
+ },
+ {
+ types: ["addons"],
+ spec: "devtools/shared/specs/addon/addons",
+ front: "devtools/client/fronts/addon/addons",
+ },
+ {
+ types: ["webExtensionInspectedWindow"],
+ spec: "devtools/shared/specs/addon/webextension-inspected-window",
+ front: "devtools/client/fronts/addon/webextension-inspected-window",
+ },
+ {
+ types: ["animationplayer", "animations"],
+ spec: "devtools/shared/specs/animation",
+ front: "devtools/client/fronts/animation",
+ },
+ {
+ types: ["arraybuffer"],
+ spec: "devtools/shared/specs/array-buffer",
+ front: "devtools/client/fronts/array-buffer",
+ },
+ {
+ types: ["blackboxing"],
+ spec: "devtools/shared/specs/blackboxing",
+ front: "devtools/client/fronts/blackboxing",
+ },
+ {
+ types: ["breakpoint-list"],
+ spec: "devtools/shared/specs/breakpoint-list",
+ front: "devtools/client/fronts/breakpoint-list",
+ },
+ {
+ types: ["changes"],
+ spec: "devtools/shared/specs/changes",
+ front: "devtools/client/fronts/changes",
+ },
+ {
+ types: ["compatibility"],
+ spec: "devtools/shared/specs/compatibility",
+ front: "devtools/client/fronts/compatibility",
+ },
+ {
+ types: ["cssProperties"],
+ spec: "devtools/shared/specs/css-properties",
+ front: "devtools/client/fronts/css-properties",
+ },
+ {
+ types: ["processDescriptor"],
+ spec: "devtools/shared/specs/descriptors/process",
+ front: "devtools/client/fronts/descriptors/process",
+ },
+ {
+ types: ["tabDescriptor"],
+ spec: "devtools/shared/specs/descriptors/tab",
+ front: "devtools/client/fronts/descriptors/tab",
+ },
+ {
+ types: ["webExtensionDescriptor"],
+ spec: "devtools/shared/specs/descriptors/webextension",
+ front: "devtools/client/fronts/descriptors/webextension",
+ },
+ {
+ types: ["workerDescriptor"],
+ spec: "devtools/shared/specs/descriptors/worker",
+ front: "devtools/client/fronts/descriptors/worker",
+ },
+ {
+ types: ["device"],
+ spec: "devtools/shared/specs/device",
+ front: "devtools/client/fronts/device",
+ },
+ {
+ types: ["environment"],
+ spec: "devtools/shared/specs/environment",
+ front: null,
+ },
+ {
+ types: ["frame"],
+ spec: "devtools/shared/specs/frame",
+ front: "devtools/client/fronts/frame",
+ },
+ /* heap snapshot has old fashion client and no front */
+ {
+ types: ["heapSnapshotFile"],
+ spec: "devtools/shared/specs/heap-snapshot-file",
+ front: null,
+ },
+ {
+ types: ["customhighlighter"],
+ spec: "devtools/shared/specs/highlighters",
+ front: "devtools/client/fronts/highlighters",
+ },
+ {
+ types: ["inspector"],
+ spec: "devtools/shared/specs/inspector",
+ front: "devtools/client/fronts/inspector",
+ },
+ {
+ types: ["flexbox", "grid", "layout"],
+ spec: "devtools/shared/specs/layout",
+ front: "devtools/client/fronts/layout",
+ },
+ {
+ types: ["manifest"],
+ spec: "devtools/shared/specs/manifest",
+ front: "devtools/client/fronts/manifest",
+ },
+ {
+ types: ["memory"],
+ spec: "devtools/shared/specs/memory",
+ front: "devtools/client/fronts/memory",
+ },
+ {
+ types: ["networkContent"],
+ spec: "devtools/shared/specs/network-content",
+ front: "devtools/client/fronts/network-content",
+ },
+ {
+ types: ["netEvent"],
+ spec: "devtools/shared/specs/network-event",
+ front: null,
+ },
+ {
+ types: ["networkParent"],
+ spec: "devtools/shared/specs/network-parent",
+ front: "devtools/client/fronts/network-parent",
+ },
+ /* imageData isn't an actor but just a DictType */
+ {
+ types: ["imageData", "disconnectedNode", "disconnectedNodeArray"],
+ spec: "devtools/shared/specs/node",
+ front: null,
+ },
+ {
+ types: ["domnode", "domnodelist"],
+ spec: "devtools/shared/specs/node",
+ front: "devtools/client/fronts/node",
+ },
+ {
+ types: ["obj", "object.descriptor"],
+ spec: "devtools/shared/specs/object",
+ front: null,
+ },
+ {
+ types: ["objects-manager"],
+ spec: "devtools/shared/specs/objects-manager",
+ front: "devtools/client/fronts/objects-manager",
+ },
+ {
+ types: ["pagestyle"],
+ spec: "devtools/shared/specs/page-style",
+ front: "devtools/client/fronts/page-style",
+ },
+ {
+ types: ["perf"],
+ spec: "devtools/shared/specs/perf",
+ front: "devtools/client/fronts/perf",
+ },
+ {
+ types: ["preference"],
+ spec: "devtools/shared/specs/preference",
+ front: "devtools/client/fronts/preference",
+ },
+ {
+ types: ["privatePropertiesIterator"],
+ spec: "devtools/shared/specs/private-properties-iterator",
+ front: "devtools/client/fronts/private-properties-iterator",
+ },
+ {
+ types: ["propertyIterator"],
+ spec: "devtools/shared/specs/property-iterator",
+ front: "devtools/client/fronts/property-iterator",
+ },
+ {
+ types: ["reflow"],
+ spec: "devtools/shared/specs/reflow",
+ front: "devtools/client/fronts/reflow",
+ },
+ {
+ types: ["responsive"],
+ spec: "devtools/shared/specs/responsive",
+ front: "devtools/client/fronts/responsive",
+ },
+ {
+ types: ["root"],
+ spec: "devtools/shared/specs/root",
+ front: "devtools/client/fronts/root",
+ },
+ {
+ types: ["screenshot"],
+ spec: "devtools/shared/specs/screenshot",
+ front: "devtools/client/fronts/screenshot",
+ },
+ {
+ types: ["screenshot-content"],
+ spec: "devtools/shared/specs/screenshot-content",
+ front: "devtools/client/fronts/screenshot-content",
+ },
+ {
+ types: ["source"],
+ spec: "devtools/shared/specs/source",
+ front: "devtools/client/fronts/source",
+ },
+ {
+ types: [
+ "Cache",
+ "cookies",
+ "localStorage",
+ "extensionStorage",
+ "indexedDB",
+ "sessionStorage",
+ ],
+ spec: "devtools/shared/specs/storage",
+ front: "devtools/client/fronts/storage",
+ },
+ /* longstring is special, it has a wrapper type. See its spec module */
+ {
+ types: ["longstring"],
+ spec: "devtools/shared/specs/string",
+ front: null,
+ },
+ {
+ types: ["longstractor"],
+ spec: "devtools/shared/specs/string",
+ front: "devtools/client/fronts/string",
+ },
+ {
+ types: ["domstylerule"],
+ spec: "devtools/shared/specs/style-rule",
+ front: "devtools/client/fronts/style-rule",
+ },
+ {
+ types: ["stylesheets"],
+ spec: "devtools/shared/specs/style-sheets",
+ front: "devtools/client/fronts/style-sheets",
+ },
+ {
+ types: ["symbol"],
+ spec: "devtools/shared/specs/symbol",
+ front: null,
+ },
+ {
+ types: ["symbolIterator"],
+ spec: "devtools/shared/specs/symbol-iterator",
+ front: "devtools/client/fronts/symbol-iterator",
+ },
+ {
+ types: ["target-configuration"],
+ spec: "devtools/shared/specs/target-configuration",
+ front: "devtools/client/fronts/target-configuration",
+ },
+ {
+ types: ["contentProcessTarget"],
+ spec: "devtools/shared/specs/targets/content-process",
+ front: null,
+ },
+ {
+ types: ["parentProcessTarget"],
+ spec: "devtools/shared/specs/targets/parent-process",
+ front: null,
+ },
+ {
+ types: ["webExtensionTarget"],
+ spec: "devtools/shared/specs/targets/webextension",
+ front: null,
+ },
+ {
+ types: ["windowGlobalTarget"],
+ spec: "devtools/shared/specs/targets/window-global",
+ front: "devtools/client/fronts/targets/window-global",
+ },
+ {
+ types: ["workerTarget"],
+ spec: "devtools/shared/specs/targets/worker",
+ front: "devtools/client/fronts/targets/worker",
+ },
+ {
+ types: ["thread"],
+ spec: "devtools/shared/specs/thread",
+ front: "devtools/client/fronts/thread",
+ },
+ {
+ types: ["thread-configuration"],
+ spec: "devtools/shared/specs/thread-configuration",
+ front: "devtools/client/fronts/thread-configuration",
+ },
+ {
+ types: ["tracer"],
+ spec: "devtools/shared/specs/tracer",
+ front: "devtools/client/fronts/tracer",
+ },
+ {
+ types: ["domwalker"],
+ spec: "devtools/shared/specs/walker",
+ front: "devtools/client/fronts/walker",
+ },
+ {
+ types: ["watcher"],
+ spec: "devtools/shared/specs/watcher",
+ front: "devtools/client/fronts/watcher",
+ },
+ {
+ types: ["console"],
+ spec: "devtools/shared/specs/webconsole",
+ front: "devtools/client/fronts/webconsole",
+ },
+ {
+ types: ["pushSubscription"],
+ spec: "devtools/shared/specs/worker/push-subscription",
+ front: "devtools/client/fronts/worker/push-subscription",
+ },
+ {
+ types: ["serviceWorker"],
+ spec: "devtools/shared/specs/worker/service-worker",
+ front: "devtools/client/fronts/worker/service-worker",
+ },
+ {
+ types: ["serviceWorkerRegistration"],
+ spec: "devtools/shared/specs/worker/service-worker-registration",
+ front: "devtools/client/fronts/worker/service-worker-registration",
+ },
+]);
+
+const lazySpecs = new Map();
+const lazyFronts = new Map();
+
+// Convert the human readable `Types` list into efficient maps
+Types.forEach(item => {
+ item.types.forEach(type => {
+ lazySpecs.set(type, item.spec);
+ lazyFronts.set(type, item.front);
+ });
+});
+
+/**
+ * Try lazy loading spec module for the given type.
+ *
+ * @param [string] type
+ * Type name
+ *
+ * @returns true, if it matched a lazy loaded type and tried to load it.
+ */
+function lazyLoadSpec(type) {
+ const modulePath = lazySpecs.get(type);
+ if (modulePath) {
+ try {
+ require(modulePath);
+ } catch (e) {
+ throw new Error(
+ `Unable to load lazy spec module '${modulePath}' for type '${type}'.
+ Error: ${e}`
+ );
+ }
+ lazySpecs.delete(type);
+ return true;
+ }
+ return false;
+}
+exports.lazyLoadSpec = lazyLoadSpec;
+
+/**
+ * Try lazy loading front module for the given type.
+ *
+ * @param [string] type
+ * Type name
+ *
+ * @returns true, if it matched a lazy loaded type and tried to load it.
+ */
+function lazyLoadFront(type) {
+ const modulePath = lazyFronts.get(type);
+ if (modulePath) {
+ try {
+ require(modulePath);
+ } catch (e) {
+ throw new Error(
+ `Unable to load lazy front module '${modulePath}' for type '${type}'.
+ Error: ${e}`
+ );
+ }
+ lazyFronts.delete(type);
+ return true;
+ }
+ return false;
+}
+exports.lazyLoadFront = lazyLoadFront;
diff --git a/devtools/shared/specs/inspector.js b/devtools/shared/specs/inspector.js
new file mode 100644
index 0000000000..949532f9c4
--- /dev/null
+++ b/devtools/shared/specs/inspector.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";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const inspectorSpec = generateActorSpec({
+ typeName: "inspector",
+
+ events: {
+ "color-picked": {
+ type: "colorPicked",
+ color: Arg(0, "string"),
+ },
+ "color-pick-canceled": {
+ type: "colorPickCanceled",
+ },
+ },
+
+ methods: {
+ getWalker: {
+ request: {
+ options: Arg(0, "nullable:json"),
+ },
+ response: {
+ walker: RetVal("domwalker"),
+ },
+ },
+ getPageStyle: {
+ request: {},
+ response: {
+ pageStyle: RetVal("pagestyle"),
+ },
+ },
+ getCompatibility: {
+ request: {},
+ response: {
+ compatibility: RetVal("compatibility"),
+ },
+ },
+ getHighlighterByType: {
+ request: {
+ typeName: Arg(0),
+ },
+ response: {
+ highlighter: RetVal("nullable:customhighlighter"),
+ },
+ },
+ getImageDataFromURL: {
+ request: { url: Arg(0), maxDim: Arg(1, "nullable:number") },
+ response: RetVal("imageData"),
+ },
+ resolveRelativeURL: {
+ request: { url: Arg(0, "string"), node: Arg(1, "nullable:domnode") },
+ response: { value: RetVal("string") },
+ },
+ pickColorFromPage: {
+ request: { options: Arg(0, "nullable:json") },
+ response: {},
+ },
+ cancelPickColorFromPage: {
+ request: {},
+ response: {},
+ },
+ supportsHighlighters: {
+ request: {},
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ },
+});
+
+exports.inspectorSpec = inspectorSpec;
diff --git a/devtools/shared/specs/layout.js b/devtools/shared/specs/layout.js
new file mode 100644
index 0000000000..69371cd023
--- /dev/null
+++ b/devtools/shared/specs/layout.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ generateActorSpec,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const flexboxSpec = generateActorSpec({
+ typeName: "flexbox",
+
+ methods: {
+ getFlexItems: {
+ request: {},
+ response: {
+ flexitems: RetVal("array:flexitem"),
+ },
+ },
+ },
+});
+
+const flexItemSpec = generateActorSpec({
+ typeName: "flexitem",
+
+ methods: {},
+});
+
+const gridSpec = generateActorSpec({
+ typeName: "grid",
+
+ methods: {},
+});
+
+const layoutSpec = generateActorSpec({
+ typeName: "layout",
+
+ methods: {
+ getCurrentFlexbox: {
+ request: {
+ node: Arg(0, "domnode"),
+ onlyLookAtParents: Arg(1, "nullable:boolean"),
+ },
+ response: {
+ flexbox: RetVal("nullable:flexbox"),
+ },
+ },
+
+ getCurrentGrid: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ grid: RetVal("nullable:grid"),
+ },
+ },
+
+ getGrids: {
+ request: {
+ rootNode: Arg(0, "domnode"),
+ },
+ response: {
+ grids: RetVal("array:grid"),
+ },
+ },
+ },
+});
+
+exports.flexboxSpec = flexboxSpec;
+exports.flexItemSpec = flexItemSpec;
+exports.gridSpec = gridSpec;
+exports.layoutSpec = layoutSpec;
diff --git a/devtools/shared/specs/manifest.js b/devtools/shared/specs/manifest.js
new file mode 100644
index 0000000000..ecddacca33
--- /dev/null
+++ b/devtools/shared/specs/manifest.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 {
+ generateActorSpec,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const manifestSpec = generateActorSpec({
+ typeName: "manifest",
+ methods: {
+ fetchCanonicalManifest: {
+ request: {},
+ response: RetVal("json"),
+ },
+ },
+});
+
+exports.manifestSpec = manifestSpec;
diff --git a/devtools/shared/specs/memory.js b/devtools/shared/specs/memory.js
new file mode 100644
index 0000000000..b70999c9f1
--- /dev/null
+++ b/devtools/shared/specs/memory.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/. */
+"use strict";
+
+const {
+ Arg,
+ RetVal,
+ types,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("AllocationsRecordingOptions", {
+ // The probability we sample any given allocation when recording
+ // allocations. Must be between 0.0 and 1.0. Defaults to 1.0, or sampling
+ // every allocation.
+ probability: "number",
+
+ // The maximum number of of allocation events to keep in the allocations
+ // log. If new allocations arrive, when we are already at capacity, the oldest
+ // allocation event is lost. This number must fit in a 32 bit signed integer.
+ maxLogLength: "number",
+});
+
+const memorySpec = generateActorSpec({
+ typeName: "memory",
+
+ /**
+ * The set of unsolicited events the MemoryActor emits that will be sent over
+ * the RDP (by protocol.js).
+ */
+ events: {
+ // Same format as the data passed to the
+ // `Debugger.Memory.prototype.onGarbageCollection` hook. See
+ // `js/src/doc/Debugger/Debugger.Memory.md` for documentation.
+ "garbage-collection": {
+ type: "garbage-collection",
+ data: Arg(0, "json"),
+ },
+
+ // Same data as the data from `getAllocations` -- only fired if
+ // `autoDrain` set during `startRecordingAllocations`.
+ allocations: {
+ type: "allocations",
+ data: Arg(0, "json"),
+ },
+ },
+
+ methods: {
+ attach: {
+ request: {},
+ response: {
+ type: RetVal("string"),
+ },
+ },
+ detach: {
+ request: {},
+ response: {
+ type: RetVal("string"),
+ },
+ },
+ getState: {
+ response: {
+ state: RetVal(0, "string"),
+ },
+ },
+ takeCensus: {
+ request: {},
+ response: RetVal("json"),
+ },
+ startRecordingAllocations: {
+ request: {
+ options: Arg(0, "nullable:AllocationsRecordingOptions"),
+ },
+ response: {
+ // Accept `nullable` in the case of server Gecko <= 37, handled on the front
+ value: RetVal(0, "nullable:number"),
+ },
+ },
+ stopRecordingAllocations: {
+ request: {},
+ response: {
+ // Accept `nullable` in the case of server Gecko <= 37, handled on the front
+ value: RetVal(0, "nullable:number"),
+ },
+ },
+ getAllocationsSettings: {
+ request: {},
+ response: {
+ options: RetVal(0, "json"),
+ },
+ },
+ getAllocations: {
+ request: {},
+ response: RetVal("json"),
+ },
+ forceGarbageCollection: {
+ request: {},
+ response: {},
+ },
+ forceCycleCollection: {
+ request: {},
+ response: {},
+ },
+ measure: {
+ request: {},
+ response: RetVal("json"),
+ },
+ residentUnique: {
+ request: {},
+ response: { value: RetVal("number") },
+ },
+ saveHeapSnapshot: {
+ request: {
+ boundaries: Arg(0, "nullable:json"),
+ },
+ response: {
+ snapshotId: RetVal("string"),
+ },
+ },
+ },
+});
+
+exports.memorySpec = memorySpec;
diff --git a/devtools/shared/specs/moz.build b/devtools/shared/specs/moz.build
new file mode 100644
index 0000000000..4982ec7924
--- /dev/null
+++ b/devtools/shared/specs/moz.build
@@ -0,0 +1,64 @@
+# -*- 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 += [
+ "addon",
+ "descriptors",
+ "style",
+ "targets",
+ "worker",
+]
+
+DevToolsModules(
+ "accessibility.js",
+ "animation.js",
+ "array-buffer.js",
+ "blackboxing.js",
+ "breakpoint-list.js",
+ "changes.js",
+ "compatibility.js",
+ "css-properties.js",
+ "device.js",
+ "environment.js",
+ "frame.js",
+ "heap-snapshot-file.js",
+ "highlighters.js",
+ "index.js",
+ "inspector.js",
+ "layout.js",
+ "manifest.js",
+ "memory.js",
+ "network-content.js",
+ "network-event.js",
+ "network-parent.js",
+ "node.js",
+ "object.js",
+ "objects-manager.js",
+ "page-style.js",
+ "perf.js",
+ "preference.js",
+ "private-properties-iterator.js",
+ "property-iterator.js",
+ "reflow.js",
+ "responsive.js",
+ "root.js",
+ "screenshot-content.js",
+ "screenshot.js",
+ "source.js",
+ "storage.js",
+ "string.js",
+ "style-rule.js",
+ "style-sheets.js",
+ "symbol-iterator.js",
+ "symbol.js",
+ "target-configuration.js",
+ "thread-configuration.js",
+ "thread.js",
+ "tracer.js",
+ "walker.js",
+ "watcher.js",
+ "webconsole.js",
+)
diff --git a/devtools/shared/specs/network-content.js b/devtools/shared/specs/network-content.js
new file mode 100644
index 0000000000..4c262ea59b
--- /dev/null
+++ b/devtools/shared/specs/network-content.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 {
+ generateActorSpec,
+ RetVal,
+ Arg,
+} = require("resource://devtools/shared/protocol.js");
+
+const networkContentSpec = generateActorSpec({
+ typeName: "networkContent",
+ methods: {
+ sendHTTPRequest: {
+ request: {
+ request: Arg(0, "json"),
+ },
+ response: RetVal("number"),
+ },
+ getStackTrace: {
+ request: { resourceId: Arg(0) },
+ response: {
+ // stacktrace is an "array:string", but not always.
+ stacktrace: RetVal("json"),
+ },
+ },
+ },
+});
+
+exports.networkContentSpec = networkContentSpec;
diff --git a/devtools/shared/specs/network-event.js b/devtools/shared/specs/network-event.js
new file mode 100644
index 0000000000..3f1cfe8538
--- /dev/null
+++ b/devtools/shared/specs/network-event.js
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ Option,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("netevent.headers-cookies", {
+ name: "string",
+ value: "longstring",
+});
+
+types.addDictType("netevent.headers", {
+ headers: "array:netevent.headers-cookies",
+ headersSize: "number",
+ rawHeaders: "nullable:longstring",
+});
+
+types.addDictType("netevent.cookies", {
+ cookies: "array:netevent.headers-cookies",
+});
+
+types.addDictType("netevent.postdata.text", {
+ text: "longstring",
+});
+
+types.addDictType("netevent.postdata", {
+ postData: "netevent.postdata.text",
+ postDataDiscarded: "boolean",
+});
+
+types.addDictType("netevent.cache", {
+ content: "json",
+});
+
+types.addDictType("netevent.content.content", {
+ text: "longstring",
+});
+
+types.addDictType("netevent.content", {
+ content: "netevent.content.content",
+ contentDiscarded: "boolean",
+});
+
+types.addDictType("netevent.timings.data", {
+ blocked: "number",
+ dns: "number",
+ ssl: "number",
+ connect: "number",
+ send: "number",
+ wait: "number",
+ receive: "number",
+});
+
+types.addDictType("netevent.timings", {
+ timings: "netevent.timings.data",
+ totalTime: "number",
+ offsets: "netevent.timings.data",
+ serverTimings: "array:netevent.timings.serverTiming",
+});
+
+types.addDictType("netevent.timings.serverTiming", {
+ name: "string",
+ description: "string",
+ duration: "number",
+});
+
+// See NetworkHelper.parseCertificateInfo for more details
+types.addDictType("netevent.cert", {
+ subject: "json",
+ issuer: "json",
+ validity: "json",
+ fingerprint: "json",
+});
+
+types.addDictType("netevent.secinfo", {
+ state: "string",
+ weaknessReasons: "array:string",
+ cipherSuite: "string",
+ keaGroupName: "string",
+ signatureSchemeName: "string",
+ protocolVersion: "string",
+ cert: "nullable:netevent.cert",
+ certificateTransparency: "number",
+ hsts: "boolean",
+ hpkp: "boolean",
+ errorMessage: "nullable:string",
+ usedEch: "boolean",
+ usedDelegatedCredentials: "boolean",
+ usedOcsp: "boolean",
+ usedPrivateDns: "boolean",
+});
+
+const networkEventSpec = generateActorSpec({
+ typeName: "netEvent",
+
+ events: {
+ // All these events end up emitting a `networkEventUpdate` RDP message
+ // `updateType` attribute allows to identify which kind of event is emitted.
+ // We use individual event at protocol.js level to workaround performance issue
+ // with `Option` types. (See bug 1449162)
+ "network-event-update:headers": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ headers: Option(1, "number"),
+ headersSize: Option(1, "number"),
+ },
+
+ "network-event-update:cookies": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ cookies: Option(1, "number"),
+ },
+
+ "network-event-update:post-data": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ dataSize: Option(1, "number"),
+ },
+
+ "network-event-update:response-start": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ response: Option(1, "json"),
+ },
+
+ "network-event-update:security-info": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ state: Option(1, "string"),
+ isRacing: Option(1, "boolean"),
+ },
+
+ "network-event-update:response-content": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ mimeType: Option(1, "string"),
+ contentSize: Option(1, "number"),
+ encoding: Option(1, "string"),
+ transferredSize: Option(1, "number"),
+ blockedReason: Option(1, "number"),
+ blockingExtension: Option(1, "string"),
+ },
+
+ "network-event-update:event-timings": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+
+ totalTime: Option(1, "number"),
+ },
+
+ "network-event-update:response-cache": {
+ type: "networkEventUpdate",
+ updateType: Arg(0, "string"),
+ },
+ },
+
+ methods: {
+ release: {
+ // This makes protocol.js call destroy method
+ release: true,
+ },
+ getRequestHeaders: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getRequestCookies: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getRequestPostData: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getResponseHeaders: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getResponseCookies: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getResponseCache: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getResponseContent: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getEventTimings: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getSecurityInfo: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getStackTrace: {
+ request: {},
+ // stacktrace is an "array:string", but not always.
+ response: RetVal("json"),
+ },
+ },
+});
+
+exports.networkEventSpec = networkEventSpec;
diff --git a/devtools/shared/specs/network-parent.js b/devtools/shared/specs/network-parent.js
new file mode 100644
index 0000000000..7438081eea
--- /dev/null
+++ b/devtools/shared/specs/network-parent.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";
+
+const {
+ generateActorSpec,
+ Arg,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const networkParentSpec = generateActorSpec({
+ typeName: "networkParent",
+
+ methods: {
+ setPersist: {
+ request: {
+ options: Arg(0, "boolean"),
+ },
+ response: {},
+ },
+ setNetworkThrottling: {
+ request: {
+ options: Arg(0, "json"),
+ },
+ response: {},
+ },
+ getNetworkThrottling: {
+ request: {},
+ response: {
+ state: RetVal("json"),
+ },
+ },
+ clearNetworkThrottling: {
+ request: {},
+ response: {},
+ },
+ setSaveRequestAndResponseBodies: {
+ request: {
+ save: Arg(0, "boolean"),
+ },
+ response: {},
+ },
+ setBlockedUrls: {
+ request: {
+ urls: Arg(0, "array:string"),
+ },
+ },
+ getBlockedUrls: {
+ request: {},
+ response: {
+ urls: RetVal("array:string"),
+ },
+ },
+ blockRequest: {
+ request: {
+ filters: Arg(0, "json"),
+ },
+ response: {},
+ },
+ unblockRequest: {
+ request: {
+ filters: Arg(0, "json"),
+ },
+ response: {},
+ },
+ override: {
+ request: {
+ url: Arg(0, "string"),
+ path: Arg(1, "string"),
+ },
+ },
+ removeOverride: {
+ request: {
+ url: Arg(0, "string"),
+ },
+ },
+ },
+});
+
+exports.networkParentSpec = networkParentSpec;
diff --git a/devtools/shared/specs/node.js b/devtools/shared/specs/node.js
new file mode 100644
index 0000000000..1daeb48a33
--- /dev/null
+++ b/devtools/shared/specs/node.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";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("imageData", {
+ // The image data
+ data: "nullable:longstring",
+ // The original image dimensions
+ size: "json",
+});
+
+types.addDictType("windowDimensions", {
+ // The window innerWidth
+ innerWidth: "nullable:number",
+ // The window innerHeight
+ innerHeight: "nullable:number",
+});
+
+/**
+ * Returned from any call that might return a node that isn't connected to root
+ * by nodes the child has seen, such as querySelector.
+ */
+types.addDictType("disconnectedNode", {
+ // The actual node to return
+ node: "domnode",
+
+ // Nodes that are needed to connect the node to a node the client has already
+ // seen
+ newParents: "array:domnode",
+});
+
+types.addDictType("disconnectedNodeArray", {
+ // The actual node list to return
+ nodes: "array:domnode",
+
+ // Nodes that are needed to connect those nodes to the root.
+ newParents: "array:domnode",
+});
+
+const nodeListSpec = generateActorSpec({
+ typeName: "domnodelist",
+
+ methods: {
+ item: {
+ request: { item: Arg(0) },
+ response: RetVal("disconnectedNode"),
+ },
+ items: {
+ request: {
+ start: Arg(0, "nullable:number"),
+ end: Arg(1, "nullable:number"),
+ },
+ response: RetVal("disconnectedNodeArray"),
+ },
+ release: {
+ release: true,
+ },
+ },
+});
+
+exports.nodeListSpec = nodeListSpec;
+
+const nodeSpec = generateActorSpec({
+ typeName: "domnode",
+
+ methods: {
+ getNodeValue: {
+ request: {},
+ response: {
+ value: RetVal("longstring"),
+ },
+ },
+ setNodeValue: {
+ request: { value: Arg(0) },
+ response: {},
+ },
+ getUniqueSelector: {
+ request: {},
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getCssPath: {
+ request: {},
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getXPath: {
+ request: {},
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ scrollIntoView: {
+ request: {},
+ response: {},
+ },
+ getImageData: {
+ request: { maxDim: Arg(0, "nullable:number") },
+ response: RetVal("imageData"),
+ },
+ getEventListenerInfo: {
+ request: {},
+ response: {
+ events: RetVal("json"),
+ },
+ },
+ enableEventListener: {
+ request: {
+ eventListenerInfoId: Arg(0),
+ },
+ response: {},
+ },
+ disableEventListener: {
+ request: {
+ eventListenerInfoId: Arg(0),
+ },
+ response: {},
+ },
+ modifyAttributes: {
+ request: {
+ modifications: Arg(0, "array:json"),
+ },
+ response: {},
+ },
+ getFontFamilyDataURL: {
+ request: { font: Arg(0, "string"), fillStyle: Arg(1, "nullable:string") },
+ response: RetVal("imageData"),
+ },
+ getClosestBackgroundColor: {
+ request: {},
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getBackgroundColor: {
+ request: {},
+ response: RetVal("json"),
+ },
+ getOwnerGlobalDimensions: {
+ request: {},
+ response: RetVal("windowDimensions"),
+ },
+ waitForFrameLoad: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+exports.nodeSpec = nodeSpec;
diff --git a/devtools/shared/specs/object.js b/devtools/shared/specs/object.js
new file mode 100644
index 0000000000..7af82cc419
--- /dev/null
+++ b/devtools/shared/specs/object.js
@@ -0,0 +1,214 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Arg,
+ RetVal,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("object.descriptor", {
+ configurable: "boolean",
+ enumerable: "boolean",
+ // Can be null if there is a getter for the property.
+ value: "nullable:json",
+ // Only set `value` exists.
+ writable: "nullable:boolean",
+ // Only set when `value` does not exist and there is a getter for the property.
+ get: "nullable:json",
+ // Only set when `value` does not exist and there is a setter for the property.
+ set: "nullable:json",
+});
+
+types.addDictType("object.completion", {
+ return: "nullable:json",
+ throw: "nullable:json",
+});
+
+types.addDictType("object.prototypeproperties", {
+ prototype: "object.descriptor",
+ ownProperties: "nullable:json",
+ ownSymbols: "nullable:array:object.descriptor",
+ safeGetterValues: "nullable:json",
+});
+
+types.addDictType("object.prototype", {
+ prototype: "object.descriptor",
+});
+
+types.addDictType("object.property", {
+ descriptor: "nullable:object.descriptor",
+});
+
+types.addDictType("object.propertyValue", {
+ value: "nullable:object.completion",
+});
+
+types.addDictType("object.apply", {
+ value: "nullable:object.completion",
+});
+
+types.addDictType("object.bindings", {
+ arguments: "array:json",
+ variables: "json",
+});
+
+types.addDictType("object.enumProperties.Options", {
+ enumEntries: "nullable:boolean",
+ ignoreNonIndexedProperties: "nullable:boolean",
+ ignoreIndexedProperties: "nullable:boolean",
+ query: "nullable:string",
+ sort: "nullable:boolean",
+});
+
+types.addDictType("object.dependentPromises", {
+ promises: "array:object.descriptor",
+});
+
+types.addDictType("object.originalSourceLocation", {
+ source: "source",
+ line: "number",
+ column: "number",
+ functionDisplayName: "string",
+});
+
+types.addDictType("object.promiseState", {
+ state: "string",
+ value: "nullable:object.descriptor",
+ reason: "nullable:object.descriptor",
+ creationTimestamp: "number",
+ timeToSettle: "nullable:number",
+});
+
+types.addDictType("object.proxySlots", {
+ proxyTarget: "object.descriptor",
+ proxyHandler: "object.descriptor",
+});
+
+types.addDictType("object.customFormatterBody", {
+ customFormatterBody: "json",
+});
+
+const objectSpec = generateActorSpec({
+ typeName: "obj",
+ methods: {
+ allocationStack: {
+ request: {},
+ response: {
+ allocationStack: RetVal("array:object.originalSourceLocation"),
+ },
+ },
+ dependentPromises: {
+ request: {},
+ response: RetVal("object.dependentPromises"),
+ },
+ enumEntries: {
+ request: {},
+ response: {
+ iterator: RetVal("propertyIterator"),
+ },
+ },
+ enumProperties: {
+ request: {
+ options: Arg(0, "nullable:object.enumProperties.Options"),
+ },
+ response: {
+ iterator: RetVal("propertyIterator"),
+ },
+ },
+ enumPrivateProperties: {
+ request: {},
+ response: {
+ iterator: RetVal("privatePropertiesIterator"),
+ },
+ },
+ enumSymbols: {
+ request: {},
+ response: {
+ iterator: RetVal("symbolIterator"),
+ },
+ },
+ fulfillmentStack: {
+ request: {},
+ response: {
+ fulfillmentStack: RetVal("array:object.originalSourceLocation"),
+ },
+ },
+ prototypeAndProperties: {
+ request: {},
+ response: RetVal("object.prototypeproperties"),
+ },
+ prototype: {
+ request: {},
+ response: RetVal("object.prototype"),
+ },
+ property: {
+ request: {
+ name: Arg(0, "string"),
+ },
+ response: RetVal("object.property"),
+ },
+ propertyValue: {
+ request: {
+ name: Arg(0, "string"),
+ receiverId: Arg(1, "nullable:string"),
+ },
+ response: RetVal("object.propertyValue"),
+ },
+ apply: {
+ request: {
+ context: Arg(0, "nullable:json"),
+ arguments: Arg(1, "nullable:array:json"),
+ },
+ response: RetVal("object.apply"),
+ },
+ rejectionStack: {
+ request: {},
+ response: {
+ rejectionStack: RetVal("array:object.originalSourceLocation"),
+ },
+ },
+ promiseState: {
+ request: {},
+ response: RetVal("object.promiseState"),
+ },
+ proxySlots: {
+ request: {},
+ response: RetVal("object.proxySlots"),
+ },
+ customFormatterBody: {
+ request: {},
+ response: RetVal("object.customFormatterBody"),
+ },
+ addWatchpoint: {
+ request: {
+ property: Arg(0, "string"),
+ label: Arg(1, "string"),
+ watchpointType: Arg(2, "string"),
+ },
+ oneway: true,
+ },
+ removeWatchpoint: {
+ request: {
+ property: Arg(0, "string"),
+ },
+ oneway: true,
+ },
+ removeWatchpoints: {
+ request: {},
+ oneway: true,
+ },
+ release: { release: true },
+ // Needed for the PauseScopedObjectActor which extends the ObjectActor.
+ threadGrip: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+exports.objectSpec = objectSpec;
diff --git a/devtools/shared/specs/objects-manager.js b/devtools/shared/specs/objects-manager.js
new file mode 100644
index 0000000000..31236db3e1
--- /dev/null
+++ b/devtools/shared/specs/objects-manager.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Arg,
+} = require("resource://devtools/shared/protocol.js");
+
+const objectsManagerSpec = generateActorSpec({
+ typeName: "objects-manager",
+
+ methods: {
+ releaseObjects: {
+ request: {
+ actorIDs: Arg(0, "array:string"),
+ },
+ response: {},
+ },
+ },
+});
+
+exports.objectsManagerSpec = objectsManagerSpec;
diff --git a/devtools/shared/specs/page-style.js b/devtools/shared/specs/page-style.js
new file mode 100644
index 0000000000..ffdec07611
--- /dev/null
+++ b/devtools/shared/specs/page-style.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ Option,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+// Load the shared types for style actors
+require("resource://devtools/shared/specs/style/style-types.js");
+
+const pageStyleSpec = generateActorSpec({
+ typeName: "pagestyle",
+
+ events: {
+ "stylesheet-updated": {
+ type: "styleSheetUpdated",
+ },
+ },
+
+ methods: {
+ getComputed: {
+ request: {
+ node: Arg(0, "domnode"),
+ markMatched: Option(1, "boolean"),
+ onlyMatched: Option(1, "boolean"),
+ filter: Option(1, "string"),
+ filterProperties: Option(1, "nullable:array:string"),
+ },
+ response: {
+ computed: RetVal("json"),
+ },
+ },
+ getAllUsedFontFaces: {
+ request: {
+ includePreviews: Option(0, "boolean"),
+ includeVariations: Option(1, "boolean"),
+ previewText: Option(0, "string"),
+ previewFontSize: Option(0, "string"),
+ previewFillStyle: Option(0, "string"),
+ },
+ response: {
+ fontFaces: RetVal("array:fontface"),
+ },
+ },
+ getUsedFontFaces: {
+ request: {
+ node: Arg(0, "domnode"),
+ includePreviews: Option(1, "boolean"),
+ includeVariations: Option(1, "boolean"),
+ previewText: Option(1, "string"),
+ previewFontSize: Option(1, "string"),
+ previewFillStyle: Option(1, "string"),
+ },
+ response: {
+ fontFaces: RetVal("array:fontface"),
+ },
+ },
+ getMatchedSelectors: {
+ request: {
+ node: Arg(0, "domnode"),
+ property: Arg(1, "string"),
+ filter: Option(2, "string"),
+ },
+ response: RetVal(
+ types.addDictType("matchedselectorresponse", {
+ rules: "array:domstylerule",
+ matched: "array:matchedselector",
+ })
+ ),
+ },
+ getRule: {
+ request: {
+ ruleId: Arg(0, "string"),
+ },
+ response: {
+ rule: RetVal("nullable:domstylerule"),
+ },
+ },
+ getApplied: {
+ request: {
+ node: Arg(0, "domnode"),
+ inherited: Option(1, "boolean"),
+ matchedSelectors: Option(1, "boolean"),
+ skipPseudo: Option(1, "boolean"),
+ filter: Option(1, "string"),
+ },
+ response: RetVal("appliedStylesReturn"),
+ },
+ isPositionEditable: {
+ request: { node: Arg(0, "domnode") },
+ response: { value: RetVal("boolean") },
+ },
+ getLayout: {
+ request: {
+ node: Arg(0, "domnode"),
+ autoMargins: Option(1, "boolean"),
+ },
+ response: RetVal("json"),
+ },
+ addNewRule: {
+ request: {
+ node: Arg(0, "domnode"),
+ pseudoClasses: Arg(1, "nullable:array:string"),
+ },
+ response: RetVal("appliedStylesReturn"),
+ },
+ getAttributesInOwnerDocument: {
+ request: {
+ search: Arg(0, "string"),
+ attributeType: Arg(1, "string"),
+ node: Arg(2, "nullable:domnode"),
+ },
+ response: {
+ attributes: RetVal("array:string"),
+ },
+ },
+ },
+});
+
+exports.pageStyleSpec = pageStyleSpec;
diff --git a/devtools/shared/specs/perf.js b/devtools/shared/specs/perf.js
new file mode 100644
index 0000000000..de8f9ffb21
--- /dev/null
+++ b/devtools/shared/specs/perf.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/. */
+"use strict";
+
+const {
+ Arg,
+ Option,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const perfDescription = {
+ typeName: "perf",
+
+ events: {
+ "profiler-started": {
+ type: "profiler-started",
+ entries: Arg(0, "number"),
+ interval: Arg(1, "number"),
+ features: Arg(2, "number"),
+ duration: Arg(3, "nullable:number"),
+ // The `activeTabID` option passed to `profiler_start` is used to
+ // determine the active tab when user starts the profiler.
+ // This is a parameter that is generated on the
+ // server, that's why we don't need to pass anything on `startProfiler`
+ // actor method. But we return this in "profiler-started" event because
+ // client may want to use that value.
+ activeTabID: Arg(4, "number"),
+ },
+ "profiler-stopped": {
+ type: "profiler-stopped",
+ },
+ },
+
+ methods: {
+ startProfiler: {
+ request: {
+ entries: Option(0, "number"),
+ duration: Option(0, "nullable:number"),
+ interval: Option(0, "number"),
+ features: Option(0, "array:string"),
+ threads: Option(0, "array:string"),
+ },
+ response: { value: RetVal("boolean") },
+ },
+
+ /**
+ * Returns null when unable to return the profile.
+ */
+ getProfileAndStopProfiler: {
+ request: {},
+ response: RetVal("nullable:json"),
+ },
+
+ stopProfilerAndDiscardProfile: {
+ request: {},
+ response: {},
+ },
+
+ getSymbolTable: {
+ request: {
+ debugPath: Arg(0, "string"),
+ breakpadId: Arg(1, "string"),
+ },
+ response: { value: RetVal("array:array:number") },
+ },
+
+ isActive: {
+ request: {},
+ response: { value: RetVal("boolean") },
+ },
+
+ isSupportedPlatform: {
+ request: {},
+ response: { value: RetVal("boolean") },
+ },
+
+ getSupportedFeatures: {
+ request: {},
+ response: { value: RetVal("array:string") },
+ },
+ },
+};
+
+exports.perfDescription = perfDescription;
+
+const perfSpec = generateActorSpec(perfDescription);
+
+exports.perfSpec = perfSpec;
diff --git a/devtools/shared/specs/preference.js b/devtools/shared/specs/preference.js
new file mode 100644
index 0000000000..b8d33261d4
--- /dev/null
+++ b/devtools/shared/specs/preference.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";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const preferenceSpec = generateActorSpec({
+ typeName: "preference",
+
+ methods: {
+ getTraits: {
+ request: {},
+ response: { traits: RetVal("json") },
+ },
+ getBoolPref: {
+ request: { value: Arg(0) },
+ response: { value: RetVal("boolean") },
+ },
+ getCharPref: {
+ request: { value: Arg(0) },
+ response: { value: RetVal("string") },
+ },
+ getIntPref: {
+ request: { value: Arg(0) },
+ response: { value: RetVal("number") },
+ },
+ getAllPrefs: {
+ request: {},
+ response: { value: RetVal("json") },
+ },
+ setBoolPref: {
+ request: { name: Arg(0), value: Arg(1) },
+ response: {},
+ },
+ setCharPref: {
+ request: { name: Arg(0), value: Arg(1) },
+ response: {},
+ },
+ setIntPref: {
+ request: { name: Arg(0), value: Arg(1) },
+ response: {},
+ },
+ clearUserPref: {
+ request: { name: Arg(0) },
+ response: {},
+ },
+ },
+});
+
+exports.preferenceSpec = preferenceSpec;
diff --git a/devtools/shared/specs/private-properties-iterator.js b/devtools/shared/specs/private-properties-iterator.js
new file mode 100644
index 0000000000..ef967d0698
--- /dev/null
+++ b/devtools/shared/specs/private-properties-iterator.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Option,
+ RetVal,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("privatepropertiesiterator.data", {
+ privateProperties: "array:privatepropertiesiterator.privateProperties",
+});
+
+types.addDictType("privatepropertiesiterator.privateProperties", {
+ name: "string",
+ descriptor: "nullable:object.descriptor",
+});
+
+const privatePropertiesIteratorSpec = generateActorSpec({
+ typeName: "privatePropertiesIterator",
+
+ methods: {
+ slice: {
+ request: {
+ start: Option(0, "number"),
+ count: Option(0, "number"),
+ },
+ response: RetVal("privatepropertiesiterator.data"),
+ },
+ all: {
+ request: {},
+ response: RetVal("privatepropertiesiterator.data"),
+ },
+ release: { release: true },
+ },
+});
+
+exports.privatePropertiesIteratorSpec = privatePropertiesIteratorSpec;
diff --git a/devtools/shared/specs/property-iterator.js b/devtools/shared/specs/property-iterator.js
new file mode 100644
index 0000000000..991a8cede1
--- /dev/null
+++ b/devtools/shared/specs/property-iterator.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 {
+ generateActorSpec,
+ Option,
+ RetVal,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("propertyiterator.data", {
+ ownProperties: "nullable:json",
+});
+
+const propertyIteratorSpec = generateActorSpec({
+ typeName: "propertyIterator",
+
+ methods: {
+ names: {
+ request: {
+ indexes: Option(0, "array:number"),
+ },
+ response: {
+ names: RetVal("array:string"),
+ },
+ },
+ slice: {
+ request: {
+ start: Option(0, "number"),
+ count: Option(0, "number"),
+ },
+ response: RetVal("propertyiterator.data"),
+ },
+ all: {
+ request: {},
+ response: RetVal("propertyiterator.data"),
+ },
+ release: { release: true },
+ },
+});
+
+exports.propertyIteratorSpec = propertyIteratorSpec;
diff --git a/devtools/shared/specs/reflow.js b/devtools/shared/specs/reflow.js
new file mode 100644
index 0000000000..9a6993ecbb
--- /dev/null
+++ b/devtools/shared/specs/reflow.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 {
+ Arg,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const reflowSpec = generateActorSpec({
+ typeName: "reflow",
+
+ events: {
+ /**
+ * The reflows event is emitted when reflows have been detected. The event
+ * is sent with an array of reflows that occured. Each item has the
+ * following properties:
+ * - start {Number}
+ * - end {Number}
+ * - isInterruptible {Boolean}
+ */
+ reflows: {
+ type: "reflows",
+ reflows: Arg(0, "array:json"),
+ },
+ },
+
+ methods: {
+ start: { oneway: true },
+ stop: { oneway: true },
+ },
+});
+
+exports.reflowSpec = reflowSpec;
diff --git a/devtools/shared/specs/responsive.js b/devtools/shared/specs/responsive.js
new file mode 100644
index 0000000000..49b04f7391
--- /dev/null
+++ b/devtools/shared/specs/responsive.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 {
+ Arg,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const responsiveSpec = generateActorSpec({
+ typeName: "responsive",
+
+ methods: {
+ toggleTouchSimulator: {
+ request: {
+ options: Arg(0, "json"),
+ },
+ response: {
+ valueChanged: RetVal("boolean"),
+ },
+ },
+
+ setElementPickerState: {
+ request: {
+ state: Arg(0, "boolean"),
+ pickerType: Arg(1, "string"),
+ },
+ response: {},
+ },
+
+ dispatchOrientationChangeEvent: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+exports.responsiveSpec = responsiveSpec;
diff --git a/devtools/shared/specs/root.js b/devtools/shared/specs/root.js
new file mode 100644
index 0000000000..f48f3b6262
--- /dev/null
+++ b/devtools/shared/specs/root.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";
+
+const {
+ types,
+ generateActorSpec,
+ RetVal,
+ Arg,
+ Option,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("root.listWorkers", {
+ workers: "array:workerDescriptor",
+});
+types.addDictType("root.listServiceWorkerRegistrations", {
+ registrations: "array:serviceWorkerRegistration",
+});
+
+const rootSpecPrototype = {
+ typeName: "root",
+
+ methods: {
+ getRoot: {
+ request: {},
+ response: RetVal("json"),
+ },
+
+ listTabs: {
+ request: {},
+ response: {
+ tabs: RetVal("array:tabDescriptor"),
+ },
+ },
+
+ getTab: {
+ request: {
+ browserId: Option(0, "number"),
+ },
+ response: {
+ tab: RetVal("tabDescriptor"),
+ },
+ },
+
+ listAddons: {
+ request: {
+ iconDataURL: Option(0, "boolean"),
+ },
+ response: {
+ addons: RetVal("array:webExtensionDescriptor"),
+ },
+ },
+
+ listWorkers: {
+ request: {},
+ response: RetVal("root.listWorkers"),
+ },
+
+ listServiceWorkerRegistrations: {
+ request: {},
+ response: RetVal("root.listServiceWorkerRegistrations"),
+ },
+
+ listProcesses: {
+ request: {},
+ response: {
+ processes: RetVal("array:processDescriptor"),
+ },
+ },
+
+ getProcess: {
+ request: {
+ id: Arg(0, "number"),
+ },
+ response: {
+ processDescriptor: RetVal("processDescriptor"),
+ },
+ },
+
+ watchResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ response: {},
+ },
+
+ unwatchResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ oneway: true,
+ },
+
+ clearResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ oneway: true,
+ },
+
+ requestTypes: {
+ request: {},
+ response: RetVal("json"),
+ },
+ },
+
+ events: {
+ tabListChanged: {
+ type: "tabListChanged",
+ },
+ workerListChanged: {
+ type: "workerListChanged",
+ },
+ addonListChanged: {
+ type: "addonListChanged",
+ },
+ serviceWorkerRegistrationListChanged: {
+ type: "serviceWorkerRegistrationListChanged",
+ },
+ processListChanged: {
+ type: "processListChanged",
+ },
+
+ "resource-available-form": {
+ type: "resource-available-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-destroyed-form": {
+ type: "resource-destroyed-form",
+ resources: Arg(0, "array:json"),
+ },
+ },
+};
+
+const rootSpec = generateActorSpec(rootSpecPrototype);
+
+exports.rootSpecPrototype = rootSpecPrototype;
+exports.rootSpec = rootSpec;
diff --git a/devtools/shared/specs/screenshot-content.js b/devtools/shared/specs/screenshot-content.js
new file mode 100644
index 0000000000..c5205fe5a4
--- /dev/null
+++ b/devtools/shared/specs/screenshot-content.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ RetVal,
+ Arg,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("screenshot-content.args", {
+ fullpage: "nullable:boolean",
+ selector: "nullable:string",
+ nodeActorID: "nullable:number",
+});
+
+const screenshotContentSpec = generateActorSpec({
+ typeName: "screenshot-content",
+
+ methods: {
+ prepareCapture: {
+ request: {
+ args: Arg(0, "screenshot-content.args"),
+ },
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ },
+});
+
+exports.screenshotContentSpec = screenshotContentSpec;
diff --git a/devtools/shared/specs/screenshot.js b/devtools/shared/specs/screenshot.js
new file mode 100644
index 0000000000..3aa4a7035f
--- /dev/null
+++ b/devtools/shared/specs/screenshot.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ RetVal,
+ Arg,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("screenshot.args", {
+ fullpage: "nullable:boolean",
+ file: "nullable:boolean",
+ clipboard: "nullable:boolean",
+ selector: "nullable:string",
+ dpr: "nullable:string",
+ delay: "nullable:string",
+});
+
+const screenshotSpec = generateActorSpec({
+ typeName: "screenshot",
+
+ methods: {
+ capture: {
+ request: {
+ args: Arg(0, "screenshot.args"),
+ },
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ },
+});
+
+exports.screenshotSpec = screenshotSpec;
diff --git a/devtools/shared/specs/source.js b/devtools/shared/specs/source.js
new file mode 100644
index 0000000000..b2cd2d977a
--- /dev/null
+++ b/devtools/shared/specs/source.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/. */
+"use strict";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+const longstringType = types.getType("longstring");
+const arraybufferType = types.getType("arraybuffer");
+// The sourcedata type needs some custom marshalling, because it is sometimes
+// returned as an arraybuffer and sometimes as a longstring.
+types.addType("sourcedata", {
+ write: (value, context, detail) => {
+ if (value.typeName === "arraybuffer") {
+ return arraybufferType.write(value, context, detail);
+ }
+ return longstringType.write(value, context, detail);
+ },
+ read: (value, context, detail) => {
+ if (value.typeName === "arraybuffer") {
+ return arraybufferType.read(value, context, detail);
+ }
+ return longstringType.read(value, context, detail);
+ },
+});
+
+types.addDictType("sourceposition", {
+ line: "number",
+ column: "number",
+});
+types.addDictType("nullablesourceposition", {
+ line: "nullable:number",
+ column: "nullable:number",
+});
+types.addDictType("breakpointquery", {
+ start: "nullable:nullablesourceposition",
+ end: "nullable:nullablesourceposition",
+});
+
+types.addDictType("source.onsource", {
+ contentType: "nullable:string",
+ source: "nullable:sourcedata",
+});
+
+const sourceSpec = generateActorSpec({
+ typeName: "source",
+
+ methods: {
+ getBreakpointPositionsCompressed: {
+ request: {
+ query: Arg(0, "nullable:breakpointquery"),
+ },
+ response: {
+ positions: RetVal("json"),
+ },
+ },
+ getBreakableLines: {
+ request: {},
+ response: {
+ lines: RetVal("json"),
+ },
+ },
+ source: {
+ request: {},
+ response: RetVal("source.onsource"),
+ },
+ setPausePoints: {
+ request: {
+ pausePoints: Arg(0, "json"),
+ },
+ },
+ blackbox: {
+ request: { range: Arg(0, "nullable:json") },
+ response: { pausedInSource: RetVal("boolean") },
+ },
+ unblackbox: {
+ request: { range: Arg(0, "nullable:json") },
+ },
+ },
+});
+
+exports.sourceSpec = sourceSpec;
diff --git a/devtools/shared/specs/storage.js b/devtools/shared/specs/storage.js
new file mode 100644
index 0000000000..51ee69fd04
--- /dev/null
+++ b/devtools/shared/specs/storage.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 protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal, types } = protocol;
+
+const childSpecs = {};
+
+function createStorageSpec(options) {
+ // common methods for all storage types
+ const methods = {
+ getStoreObjects: {
+ request: {
+ host: Arg(0),
+ names: Arg(1, "nullable:array:string"),
+ options: Arg(2, "nullable:json"),
+ },
+ response: RetVal(options.storeObjectType),
+ },
+ getFields: {
+ request: {
+ subType: Arg(0, "nullable:string"),
+ },
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ };
+
+ // extra methods specific for storage type
+ Object.assign(methods, options.methods);
+
+ childSpecs[options.typeName] = protocol.generateActorSpec({
+ typeName: options.typeName,
+ methods,
+ events: {
+ "single-store-update": {
+ type: "storesUpdate",
+ data: Arg(0, "storeUpdateObject"),
+ },
+ "single-store-cleared": {
+ type: "storesCleared",
+ data: Arg(0, "json"),
+ },
+ },
+ });
+}
+
+// Cookies store object
+types.addDictType("cookieobject", {
+ uniqueKey: "string",
+ name: "string",
+ value: "longstring",
+ path: "nullable:string",
+ host: "string",
+ hostOnly: "boolean",
+ isSecure: "boolean",
+ isHttpOnly: "boolean",
+ creationTime: "number",
+ lastAccessed: "number",
+ expires: "number",
+});
+
+// Array of cookie store objects
+types.addDictType("cookiestoreobject", {
+ total: "number",
+ offset: "number",
+ data: "array:nullable:cookieobject",
+});
+
+// Common methods for edit/remove
+const editRemoveMethods = {
+ getFields: {
+ request: {},
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ editItem: {
+ request: {
+ data: Arg(0, "json"),
+ },
+ response: {},
+ },
+ removeItem: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {},
+ },
+};
+
+// Cookies actor spec
+createStorageSpec({
+ typeName: "cookies",
+ storeObjectType: "cookiestoreobject",
+ methods: Object.assign(
+ {},
+ editRemoveMethods,
+ {
+ addItem: {
+ request: {
+ guid: Arg(0, "string"),
+ host: Arg(1, "nullable:string"),
+ },
+ response: {},
+ },
+ },
+ {
+ removeAll: {
+ request: {
+ host: Arg(0, "string"),
+ domain: Arg(1, "nullable:string"),
+ },
+ response: {},
+ },
+ },
+ {
+ removeAllSessionCookies: {
+ request: {
+ host: Arg(0, "string"),
+ domain: Arg(1, "nullable:string"),
+ },
+ response: {},
+ },
+ }
+ ),
+});
+
+// Local Storage / Session Storage store object
+types.addDictType("storageobject", {
+ name: "string",
+ value: "longstring",
+});
+
+// Common methods for local/session storage
+const storageMethods = Object.assign(
+ {},
+ editRemoveMethods,
+ {
+ addItem: {
+ request: {
+ guid: Arg(0, "string"),
+ host: Arg(1, "nullable:string"),
+ },
+ response: {},
+ },
+ },
+ {
+ removeAll: {
+ request: {
+ host: Arg(0, "string"),
+ },
+ response: {},
+ },
+ }
+);
+
+// Array of Local Storage / Session Storage store objects
+types.addDictType("storagestoreobject", {
+ total: "number",
+ offset: "number",
+ data: "array:nullable:storageobject",
+});
+
+createStorageSpec({
+ typeName: "localStorage",
+ storeObjectType: "storagestoreobject",
+ methods: storageMethods,
+});
+
+createStorageSpec({
+ typeName: "sessionStorage",
+ storeObjectType: "storagestoreobject",
+ methods: storageMethods,
+});
+
+types.addDictType("extensionobject", {
+ name: "nullable:string",
+ value: "nullable:longstring",
+ area: "string",
+ isValueEditable: "boolean",
+});
+
+types.addDictType("extensionstoreobject", {
+ total: "number",
+ offset: "number",
+ data: "array:nullable:extensionobject",
+});
+
+createStorageSpec({
+ typeName: "extensionStorage",
+ storeObjectType: "extensionstoreobject",
+ // Same as storageMethods except for addItem
+ methods: Object.assign({}, editRemoveMethods, {
+ removeAll: {
+ request: {
+ host: Arg(0, "string"),
+ },
+ response: {},
+ },
+ }),
+});
+
+types.addDictType("cacheobject", {
+ url: "string",
+ status: "string",
+});
+
+// Array of Cache store objects
+types.addDictType("cachestoreobject", {
+ total: "number",
+ offset: "number",
+ data: "array:nullable:cacheobject",
+});
+
+// Cache storage spec
+createStorageSpec({
+ typeName: "Cache",
+ storeObjectType: "cachestoreobject",
+ methods: {
+ removeAll: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {},
+ },
+ removeItem: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {},
+ },
+ },
+});
+
+// Indexed DB store object
+// This is a union on idb object, db metadata object and object store metadata
+// object
+types.addDictType("idbobject", {
+ uniqueKey: "string",
+ name: "nullable:string",
+ db: "nullable:string",
+ objectStore: "nullable:string",
+ origin: "nullable:string",
+ version: "nullable:number",
+ storage: "nullable:string",
+ objectStores: "nullable:number",
+ keyPath: "nullable:string",
+ autoIncrement: "nullable:boolean",
+ indexes: "nullable:string",
+ value: "nullable:longstring",
+});
+
+// Array of Indexed DB store objects
+types.addDictType("idbstoreobject", {
+ total: "number",
+ offset: "number",
+ data: "array:nullable:idbobject",
+});
+
+// Result of Indexed DB delete operation: can block or throw error
+types.addDictType("idbdeleteresult", {
+ blocked: "nullable:boolean",
+ error: "nullable:string",
+});
+
+createStorageSpec({
+ typeName: "indexedDB",
+ storeObjectType: "idbstoreobject",
+ methods: {
+ removeDatabase: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: RetVal("idbdeleteresult"),
+ },
+ removeAll: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {},
+ },
+ removeItem: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {},
+ },
+ },
+});
+
+// Update notification object
+types.addDictType("storeUpdateObject", {
+ changed: "nullable:json",
+ deleted: "nullable:json",
+ added: "nullable:json",
+});
+
+exports.childSpecs = childSpecs;
diff --git a/devtools/shared/specs/string.js b/devtools/shared/specs/string.js
new file mode 100644
index 0000000000..89b53c0a95
--- /dev/null
+++ b/devtools/shared/specs/string.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 protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal, generateActorSpec } = protocol;
+
+const longStringSpec = generateActorSpec({
+ typeName: "longstractor",
+
+ methods: {
+ substring: {
+ request: {
+ start: Arg(0),
+ end: Arg(1),
+ },
+ response: { substring: RetVal() },
+ },
+ release: { release: true },
+ },
+});
+
+exports.longStringSpec = longStringSpec;
+
+/**
+ * When a caller is expecting a LongString actor but the string is already available on
+ * client, the SimpleStringFront can be used as it shares the same API as a
+ * LongStringFront but will not make unnecessary trips to the server.
+ */
+class SimpleStringFront {
+ constructor(str) {
+ this.str = str;
+ }
+
+ get length() {
+ return this.str.length;
+ }
+
+ get initial() {
+ return this.str;
+ }
+
+ string() {
+ return Promise.resolve(this.str);
+ }
+
+ substring(start, end) {
+ return Promise.resolve(this.str.substring(start, end));
+ }
+
+ release() {
+ this.str = null;
+ return Promise.resolve(undefined);
+ }
+}
+
+exports.SimpleStringFront = SimpleStringFront;
+
+// The long string actor needs some custom marshalling, because it is sometimes
+// returned as a primitive rather than a complete form.
+
+var stringActorType = protocol.types.getType("longstractor");
+protocol.types.addType("longstring", {
+ _actor: true,
+ write: (value, context, detail) => {
+ if (!(context instanceof protocol.Actor)) {
+ throw Error("Passing a longstring as an argument isn't supported.");
+ }
+
+ if (value.short) {
+ return value.str;
+ }
+ return stringActorType.write(value, context, detail);
+ },
+ read: (value, context, detail) => {
+ if (context instanceof protocol.Actor) {
+ throw Error("Passing a longstring as an argument isn't supported.");
+ }
+ if (typeof value === "string") {
+ return new SimpleStringFront(value);
+ }
+ return stringActorType.read(value, context, detail);
+ },
+});
diff --git a/devtools/shared/specs/style-rule.js b/devtools/shared/specs/style-rule.js
new file mode 100644
index 0000000000..cb06a578b9
--- /dev/null
+++ b/devtools/shared/specs/style-rule.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";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+// Load the shared types for style actors
+require("resource://devtools/shared/specs/style/style-types.js");
+
+types.addDictType("domstylerule.queryContainerForNodeReturn", {
+ node: "nullable:domnode",
+ containerType: "nullable:string",
+ blockSize: "nullable:string",
+ inlineSize: "nullable:string",
+});
+
+const styleRuleSpec = generateActorSpec({
+ typeName: "domstylerule",
+
+ events: {
+ "location-changed": {
+ type: "locationChanged",
+ line: Arg(0, "number"),
+ column: Arg(1, "number"),
+ },
+ "rule-updated": {
+ type: "ruleUpdated",
+ rule: Arg(0, "domstylerule"),
+ },
+ },
+
+ methods: {
+ getRuleText: {
+ response: {
+ text: RetVal("string"),
+ },
+ },
+ setRuleText: {
+ request: {
+ newText: Arg(0, "string"),
+ modifications: Arg(1, "array:json"),
+ },
+ response: { rule: RetVal("domstylerule") },
+ },
+ modifyProperties: {
+ request: { modifications: Arg(0, "array:json") },
+ response: { rule: RetVal("domstylerule") },
+ },
+ modifySelector: {
+ request: {
+ node: Arg(0, "domnode"),
+ value: Arg(1, "string"),
+ editAuthored: Arg(2, "boolean"),
+ },
+ response: RetVal("modifiedStylesReturn"),
+ },
+ getQueryContainerForNode: {
+ request: {
+ ancestorRuleIndex: Arg(0, "number"),
+ node: Arg(1, "domnode"),
+ },
+ response: RetVal("domstylerule.queryContainerForNodeReturn"),
+ },
+ },
+});
+
+exports.styleRuleSpec = styleRuleSpec;
diff --git a/devtools/shared/specs/style-sheets.js b/devtools/shared/specs/style-sheets.js
new file mode 100644
index 0000000000..94d3e72f5f
--- /dev/null
+++ b/devtools/shared/specs/style-sheets.js
@@ -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/. */
+"use strict";
+
+const {
+ Arg,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const styleSheetsSpec = generateActorSpec({
+ typeName: "stylesheets",
+
+ events: {},
+
+ methods: {
+ getTraits: {
+ request: {},
+ response: { traits: RetVal("json") },
+ },
+ addStyleSheet: {
+ request: {
+ text: Arg(0, "string"),
+ fileName: Arg(1, "nullable:string"),
+ },
+ response: {},
+ },
+ toggleDisabled: {
+ request: { resourceId: Arg(0, "string") },
+ response: { disabled: RetVal("boolean") },
+ },
+ getText: {
+ request: { resourceId: Arg(0, "string") },
+ response: { text: RetVal("longstring") },
+ },
+ update: {
+ request: {
+ resourceId: Arg(0, "string"),
+ text: Arg(1, "string"),
+ transition: Arg(2, "boolean"),
+ cause: Arg(3, "nullable:string"),
+ },
+ response: {},
+ },
+ },
+});
+
+exports.styleSheetsSpec = styleSheetsSpec;
diff --git a/devtools/shared/specs/style/moz.build b/devtools/shared/specs/style/moz.build
new file mode 100644
index 0000000000..56883cc51a
--- /dev/null
+++ b/devtools/shared/specs/style/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ "style-types.js",
+)
diff --git a/devtools/shared/specs/style/style-types.js b/devtools/shared/specs/style/style-types.js
new file mode 100644
index 0000000000..b752073b43
--- /dev/null
+++ b/devtools/shared/specs/style/style-types.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";
+
+const { RetVal, types } = require("resource://devtools/shared/protocol.js");
+
+// Predeclare the domstylerule actor type
+types.addActorType("domstylerule");
+
+/**
+ * When asking for the styles applied to a node, we return a list of
+ * appliedstyle json objects that lists the rules that apply to the node
+ * and which element they were inherited from (if any).
+ */
+types.addDictType("appliedstyle", {
+ rule: "domstylerule",
+ inherited: "nullable:domnode#actorid",
+ keyframes: "nullable:domstylerule",
+});
+
+types.addDictType("matchedselector", {
+ rule: "domstylerule#actorid",
+ selector: "string",
+ value: "string",
+ status: "number",
+});
+
+types.addDictType("appliedStylesReturn", {
+ entries: "array:appliedstyle",
+});
+
+types.addDictType("modifiedStylesReturn", {
+ isMatching: RetVal("boolean"),
+ ruleProps: RetVal("nullable:appliedStylesReturn"),
+});
+
+types.addDictType("fontpreview", {
+ data: "nullable:longstring",
+ size: "json",
+});
+
+types.addDictType("fontvariationaxis", {
+ tag: "string",
+ name: "string",
+ minValue: "number",
+ maxValue: "number",
+ defaultValue: "number",
+});
+
+types.addDictType("fontvariationinstancevalue", {
+ axis: "string",
+ value: "number",
+});
+
+types.addDictType("fontvariationinstance", {
+ name: "string",
+ values: "array:fontvariationinstancevalue",
+});
+
+types.addDictType("fontface", {
+ name: "string",
+ CSSFamilyName: "string",
+ rule: "nullable:domstylerule",
+ srcIndex: "number",
+ URI: "string",
+ format: "string",
+ preview: "nullable:fontpreview",
+ localName: "string",
+ metadata: "string",
+ variationAxes: "array:fontvariationaxis",
+ variationInstances: "array:fontvariationinstance",
+});
diff --git a/devtools/shared/specs/symbol-iterator.js b/devtools/shared/specs/symbol-iterator.js
new file mode 100644
index 0000000000..c3345bcdd3
--- /dev/null
+++ b/devtools/shared/specs/symbol-iterator.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Option,
+ RetVal,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("symboliterator.data", {
+ ownSymbols: "array:symboliterator.ownsymbols",
+});
+
+types.addDictType("symboliterator.ownsymbols", {
+ name: "string",
+ descriptor: "nullable:object.descriptor",
+});
+
+const symbolIteratorSpec = generateActorSpec({
+ typeName: "symbolIterator",
+
+ methods: {
+ slice: {
+ request: {
+ start: Option(0, "number"),
+ count: Option(0, "number"),
+ },
+ response: RetVal("symboliterator.data"),
+ },
+ all: {
+ request: {},
+ response: RetVal("symboliterator.data"),
+ },
+ release: { release: true },
+ },
+});
+
+exports.symbolIteratorSpec = symbolIteratorSpec;
diff --git a/devtools/shared/specs/symbol.js b/devtools/shared/specs/symbol.js
new file mode 100644
index 0000000000..77e44ce1e4
--- /dev/null
+++ b/devtools/shared/specs/symbol.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/. */
+
+"use strict";
+
+const { generateActorSpec } = require("resource://devtools/shared/protocol.js");
+
+const symbolSpec = generateActorSpec({
+ typeName: "symbol",
+
+ methods: {
+ release: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+exports.symbolSpec = symbolSpec;
diff --git a/devtools/shared/specs/target-configuration.js b/devtools/shared/specs/target-configuration.js
new file mode 100644
index 0000000000..8db15ff77d
--- /dev/null
+++ b/devtools/shared/specs/target-configuration.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 {
+ generateActorSpec,
+ Arg,
+ RetVal,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("target-configuration.configuration", {
+ cacheDisabled: "nullable:boolean",
+ colorSchemeSimulation: "nullable:string",
+ customFormatters: "nullable:boolean",
+ customUserAgent: "nullable:string",
+ javascriptEnabled: "nullable:boolean",
+ overrideDPPX: "nullable:number",
+ printSimulationEnabled: "nullable:boolean",
+ rdmPaneOrientation: "nullable:json",
+ reloadOnTouchSimulationToggle: "nullable:boolean",
+ restoreFocus: "nullable:boolean",
+ serviceWorkersTestingEnabled: "nullable:boolean",
+ setTabOffline: "nullable:boolean",
+ touchEventsOverride: "nullable:string",
+});
+
+const targetConfigurationSpec = generateActorSpec({
+ typeName: "target-configuration",
+
+ methods: {
+ updateConfiguration: {
+ request: {
+ configuration: Arg(0, "target-configuration.configuration"),
+ },
+ response: {
+ configuration: RetVal("target-configuration.configuration"),
+ },
+ },
+ isJavascriptEnabled: {
+ request: {},
+ response: {
+ javascriptEnabled: RetVal("boolean"),
+ },
+ },
+ },
+});
+
+exports.targetConfigurationSpec = targetConfigurationSpec;
diff --git a/devtools/shared/specs/targets/content-process.js b/devtools/shared/specs/targets/content-process.js
new file mode 100644
index 0000000000..3c7edef4a2
--- /dev/null
+++ b/devtools/shared/specs/targets/content-process.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";
+
+const {
+ types,
+ Arg,
+ Option,
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("contentProcessTarget.workers", {
+ error: "nullable:string",
+ workers: "nullable:array:workerDescriptor",
+});
+
+const contentProcessTargetSpec = generateActorSpec({
+ typeName: "contentProcessTarget",
+
+ methods: {
+ listWorkers: {
+ request: {},
+ response: RetVal("contentProcessTarget.workers"),
+ },
+
+ pauseMatchingServiceWorkers: {
+ request: {
+ origin: Option(0, "string"),
+ },
+ response: {},
+ },
+ },
+
+ events: {
+ workerListChanged: {
+ type: "workerListChanged",
+ },
+ "resource-available-form": {
+ type: "resource-available-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-destroyed-form": {
+ type: "resource-destroyed-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-updated-form": {
+ type: "resource-updated-form",
+ resources: Arg(0, "array:json"),
+ },
+ },
+});
+
+exports.contentProcessTargetSpec = contentProcessTargetSpec;
diff --git a/devtools/shared/specs/targets/moz.build b/devtools/shared/specs/targets/moz.build
new file mode 100644
index 0000000000..9c22a1a241
--- /dev/null
+++ b/devtools/shared/specs/targets/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(
+ "content-process.js",
+ "parent-process.js",
+ "webextension.js",
+ "window-global.js",
+ "worker.js",
+)
diff --git a/devtools/shared/specs/targets/parent-process.js b/devtools/shared/specs/targets/parent-process.js
new file mode 100644
index 0000000000..dca2c777fa
--- /dev/null
+++ b/devtools/shared/specs/targets/parent-process.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { generateActorSpec } = require("resource://devtools/shared/protocol.js");
+const { extend } = require("resource://devtools/shared/extend.js");
+const {
+ windowGlobalTargetSpecPrototype,
+} = require("resource://devtools/shared/specs/targets/window-global.js");
+
+const parentProcessTargetSpecPrototype = extend(
+ windowGlobalTargetSpecPrototype,
+ {
+ typeName: "parentProcessTarget",
+ }
+);
+
+const parentProcessTargetSpec = generateActorSpec(
+ parentProcessTargetSpecPrototype
+);
+
+exports.parentProcessTargetSpecPrototype = parentProcessTargetSpecPrototype;
+exports.parentProcessTargetSpec = parentProcessTargetSpec;
diff --git a/devtools/shared/specs/targets/webextension.js b/devtools/shared/specs/targets/webextension.js
new file mode 100644
index 0000000000..4eeffb3da7
--- /dev/null
+++ b/devtools/shared/specs/targets/webextension.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { generateActorSpec } = require("resource://devtools/shared/protocol.js");
+const { extend } = require("resource://devtools/shared/extend.js");
+const {
+ parentProcessTargetSpecPrototype,
+} = require("resource://devtools/shared/specs/targets/parent-process.js");
+
+const webExtensionTargetSpec = generateActorSpec(
+ extend(parentProcessTargetSpecPrototype, {
+ typeName: "webExtensionTarget",
+ })
+);
+
+exports.webExtensionTargetSpec = webExtensionTargetSpec;
diff --git a/devtools/shared/specs/targets/window-global.js b/devtools/shared/specs/targets/window-global.js
new file mode 100644
index 0000000000..597e0eb0e8
--- /dev/null
+++ b/devtools/shared/specs/targets/window-global.js
@@ -0,0 +1,156 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ types,
+ generateActorSpec,
+ RetVal,
+ Option,
+ Arg,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("windowGlobalTarget.switchtoframe", {
+ message: "string",
+});
+
+types.addDictType("windowGlobalTarget.listframes", {
+ frames: "array:windowGlobalTarget.window",
+});
+
+types.addDictType("windowGlobalTarget.window", {
+ id: "string",
+ parentID: "nullable:string",
+ url: "nullable:string", // should be present if not destroying
+ title: "nullable:string", // should be present if not destroying
+ destroy: "nullable:boolean", // not present if not destroying
+});
+
+types.addDictType("windowGlobalTarget.workers", {
+ workers: "array:workerDescriptor",
+});
+
+// @backward-compat { legacy }
+// reload is preserved for third party tools. See Bug 1717837.
+// DevTools should use Descriptor::reloadDescriptor instead.
+types.addDictType("windowGlobalTarget.reload", {
+ force: "boolean",
+});
+
+// @backward-compat { version 87 } See backward-compat note for `reconfigure`.
+types.addDictType("windowGlobalTarget.reconfigure", {
+ cacheDisabled: "nullable:boolean",
+ colorSchemeSimulation: "nullable:string",
+ printSimulationEnabled: "nullable:boolean",
+ restoreFocus: "nullable:boolean",
+ serviceWorkersTestingEnabled: "nullable:boolean",
+});
+
+const windowGlobalTargetSpecPrototype = {
+ typeName: "windowGlobalTarget",
+
+ methods: {
+ detach: {
+ request: {},
+ response: {},
+ },
+ focus: {
+ request: {},
+ response: {},
+ },
+ goForward: {
+ request: {},
+ response: {},
+ },
+ goBack: {
+ request: {},
+ response: {},
+ },
+ // @backward-compat { legacy }
+ // reload is preserved for third party tools. See Bug 1717837.
+ // DevTools should use Descriptor::reloadDescriptor instead.
+ reload: {
+ request: {
+ options: Option(0, "windowGlobalTarget.reload"),
+ },
+ response: {},
+ },
+ navigateTo: {
+ request: {
+ url: Option(0, "string"),
+ },
+ response: {},
+ },
+ // @backward-compat { version 87 } Starting with version 87, targets which
+ // support the watcher will rely on the configuration actor to update their
+ // configuration flags. However we need to keep this request until all
+ // browsing context targets support the watcher (eg webextensions).
+ reconfigure: {
+ request: {
+ options: Option(0, "windowGlobalTarget.reconfigure"),
+ },
+ response: {},
+ },
+ switchToFrame: {
+ request: {
+ windowId: Option(0, "string"),
+ },
+ response: RetVal("windowGlobalTarget.switchtoframe"),
+ },
+ listFrames: {
+ request: {},
+ response: RetVal("windowGlobalTarget.listframes"),
+ },
+ listWorkers: {
+ request: {},
+ response: RetVal("windowGlobalTarget.workers"),
+ },
+ logInPage: {
+ request: {
+ text: Option(0, "string"),
+ category: Option(0, "string"),
+ flags: Option(0, "string"),
+ },
+ response: {},
+ },
+ },
+ events: {
+ tabNavigated: {
+ type: "tabNavigated",
+ url: Option(0, "string"),
+ title: Option(0, "string"),
+ state: Option(0, "string"),
+ isFrameSwitching: Option(0, "boolean"),
+ },
+ frameUpdate: {
+ type: "frameUpdate",
+ frames: Option(0, "nullable:array:windowGlobalTarget.window"),
+ selected: Option(0, "nullable:number"),
+ destroyAll: Option(0, "nullable:boolean"),
+ },
+ workerListChanged: {
+ type: "workerListChanged",
+ },
+
+ "resource-available-form": {
+ type: "resource-available-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-destroyed-form": {
+ type: "resource-destroyed-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-updated-form": {
+ type: "resource-updated-form",
+ resources: Arg(0, "array:json"),
+ },
+ },
+};
+
+const windowGlobalTargetSpec = generateActorSpec(
+ windowGlobalTargetSpecPrototype
+);
+
+exports.windowGlobalTargetSpecPrototype = windowGlobalTargetSpecPrototype;
+exports.windowGlobalTargetSpec = windowGlobalTargetSpec;
diff --git a/devtools/shared/specs/targets/worker.js b/devtools/shared/specs/targets/worker.js
new file mode 100644
index 0000000000..d329f5b9dd
--- /dev/null
+++ b/devtools/shared/specs/targets/worker.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const workerTargetSpec = generateActorSpec({
+ typeName: "workerTarget",
+ methods: {},
+ events: {
+ "resource-available-form": {
+ type: "resource-available-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-destroyed-form": {
+ type: "resource-destroyed-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-updated-form": {
+ type: "resource-updated-form",
+ resources: Arg(0, "array:json"),
+ },
+ },
+});
+
+exports.workerTargetSpec = workerTargetSpec;
diff --git a/devtools/shared/specs/thread-configuration.js b/devtools/shared/specs/thread-configuration.js
new file mode 100644
index 0000000000..6d412097fe
--- /dev/null
+++ b/devtools/shared/specs/thread-configuration.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";
+
+const {
+ generateActorSpec,
+ Arg,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("thread-configuration.configuration", {
+ pauseOnExceptions: "nullable:boolean",
+ ignoreCaughtExceptions: "nullable:boolean",
+});
+
+const threadConfigurationSpec = generateActorSpec({
+ typeName: "thread-configuration",
+
+ methods: {
+ updateConfiguration: {
+ request: {
+ configuration: Arg(0, "thread-configuration.configuration"),
+ },
+ response: {},
+ },
+ },
+});
+
+exports.threadConfigurationSpec = threadConfigurationSpec;
diff --git a/devtools/shared/specs/thread.js b/devtools/shared/specs/thread.js
new file mode 100644
index 0000000000..1a4f862fb1
--- /dev/null
+++ b/devtools/shared/specs/thread.js
@@ -0,0 +1,189 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ Option,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("available-breakpoint-group", {
+ name: "string",
+ events: "array:available-breakpoint-event",
+});
+
+types.addDictType("available-breakpoint-event", {
+ id: "string",
+ name: "string",
+});
+
+types.addDictType("thread.frames", {
+ frames: "array:frame",
+});
+
+types.addDictType("thread.breakpoint-options", {
+ condition: "nullable:string",
+ logValue: "nullable:string",
+});
+
+types.addDictType("paused-reason", {
+ type: "string",
+
+ // Used for any pause type that wants to describe why the pause happened.
+ message: "nullable:string",
+
+ // Used for the stepping pause types.
+ frameFinished: "nullable:json",
+
+ // Used for the "exception" pause type.
+ exception: "nullable:json",
+
+ // Used for the "interrupted" pause type.
+ onNext: "nullable:boolean",
+
+ // Used for the "eventBreakpoint" pause type.
+ breakpoint: "nullable:json",
+
+ // Used for the "mutationBreakpoint" pause type.
+ mutationType: "nullable:string",
+});
+
+const threadSpec = generateActorSpec({
+ typeName: "thread",
+
+ events: {
+ paused: {
+ actor: Option(0, "nullable:string"),
+ frame: Option(0, "frame"),
+ why: Option(0, "paused-reason"),
+ },
+ resumed: {},
+ newSource: {
+ source: Option(0, "json"),
+ },
+ },
+
+ methods: {
+ attach: {
+ request: {
+ options: Arg(0, "json"),
+ },
+ response: {},
+ },
+ reconfigure: {
+ request: {
+ options: Arg(0, "json"),
+ },
+ response: {},
+ },
+ resume: {
+ request: {
+ resumeLimit: Arg(0, "nullable:json"),
+ frameActorID: Arg(1, "nullable:string"),
+ },
+ response: RetVal("nullable:json"),
+ },
+ frames: {
+ request: {
+ start: Arg(0, "number"),
+ count: Arg(1, "number"),
+ },
+ response: RetVal("thread.frames"),
+ },
+ interrupt: {
+ request: {
+ when: Arg(0, "json"),
+ },
+ },
+ sources: {
+ response: {
+ sources: RetVal("array:json"),
+ },
+ },
+ skipBreakpoints: {
+ request: {
+ skip: Arg(0, "json"),
+ },
+ response: {
+ skip: Arg(0, "json"),
+ },
+ },
+ dumpThread: {
+ request: {},
+ response: RetVal("json"),
+ },
+ dumpPools: {
+ request: {},
+ response: RetVal("json"),
+ },
+ setBreakpoint: {
+ request: {
+ location: Arg(0, "json"),
+ options: Arg(1, "thread.breakpoint-options"),
+ },
+ },
+ removeBreakpoint: {
+ request: {
+ location: Arg(0, "json"),
+ },
+ },
+ setXHRBreakpoint: {
+ request: {
+ path: Arg(0, "string"),
+ method: Arg(1, "string"),
+ },
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ removeXHRBreakpoint: {
+ request: {
+ path: Arg(0, "string"),
+ method: Arg(1, "string"),
+ },
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ getAvailableEventBreakpoints: {
+ response: {
+ value: RetVal("array:available-breakpoint-group"),
+ },
+ },
+ getActiveEventBreakpoints: {
+ response: {
+ ids: RetVal("array:string"),
+ },
+ },
+ setActiveEventBreakpoints: {
+ request: {
+ ids: Arg(0, "array:string"),
+ },
+ },
+ pauseOnExceptions: {
+ request: {
+ pauseOnExceptions: Arg(0, "string"),
+ ignoreCaughtExceptions: Arg(1, "string"),
+ },
+ },
+
+ toggleEventLogging: {
+ request: {
+ logEventBreakpoints: Arg(0, "string"),
+ },
+ },
+
+ isAttached: {
+ request: {},
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ },
+});
+
+exports.threadSpec = threadSpec;
diff --git a/devtools/shared/specs/tracer.js b/devtools/shared/specs/tracer.js
new file mode 100644
index 0000000000..604987db84
--- /dev/null
+++ b/devtools/shared/specs/tracer.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Arg,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("tracer.start.options", {
+ logMethod: "string",
+ traceValues: "boolean",
+ traceOnNextInteraction: "boolean",
+ traceOnNextLoad: "boolean",
+});
+
+const tracerSpec = generateActorSpec({
+ typeName: "tracer",
+
+ methods: {
+ startTracing: {
+ request: {
+ options: Arg(0, "tracer.start.options"),
+ },
+ },
+ stopTracing: {
+ request: {},
+ },
+ },
+});
+
+exports.tracerSpec = tracerSpec;
diff --git a/devtools/shared/specs/walker.js b/devtools/shared/specs/walker.js
new file mode 100644
index 0000000000..396415965a
--- /dev/null
+++ b/devtools/shared/specs/walker.js
@@ -0,0 +1,393 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Arg,
+ Option,
+ RetVal,
+ generateActorSpec,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("dommutation", {});
+
+types.addDictType("searchresult", {
+ list: "domnodelist",
+ // Right now there is isn't anything required for metadata,
+ // but it's json so it can be extended with extra data.
+ metadata: "array:json",
+});
+
+// Some common request/response templates for the dom walker
+
+var nodeArrayMethod = {
+ request: {
+ node: Arg(0, "domnode"),
+ maxNodes: Option(1),
+ center: Option(1, "domnode"),
+ start: Option(1, "domnode"),
+ },
+ response: RetVal(
+ types.addDictType("domtraversalarray", {
+ nodes: "array:domnode",
+ })
+ ),
+};
+
+var traversalMethod = {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ node: RetVal("nullable:domnode"),
+ },
+};
+
+const walkerSpec = generateActorSpec({
+ typeName: "domwalker",
+
+ events: {
+ "new-mutations": {
+ type: "newMutations",
+ },
+ "root-available": {
+ type: "root-available",
+ node: Arg(0, "nullable:domnode"),
+ },
+ "root-destroyed": {
+ type: "root-destroyed",
+ node: Arg(0, "nullable:domnode"),
+ },
+ "picker-node-picked": {
+ type: "pickerNodePicked",
+ node: Arg(0, "disconnectedNode"),
+ },
+ "picker-node-previewed": {
+ type: "pickerNodePreviewed",
+ node: Arg(0, "disconnectedNode"),
+ },
+ "picker-node-hovered": {
+ type: "pickerNodeHovered",
+ node: Arg(0, "disconnectedNode"),
+ },
+ "picker-node-canceled": {
+ type: "pickerNodeCanceled",
+ },
+ "display-change": {
+ type: "display-change",
+ nodes: Arg(0, "array:domnode"),
+ },
+ "scrollable-change": {
+ type: "scrollable-change",
+ nodes: Arg(0, "array:domnode"),
+ },
+ "overflow-change": {
+ type: "overflow-change",
+ nodes: Arg(0, "array:domnode"),
+ },
+ "container-type-change": {
+ type: "container-type-change",
+ nodes: Arg(0, "array:domnode"),
+ },
+ // The walker actor emits a useful "resize" event to its front to let
+ // clients know when the browser window gets resized. This may be useful
+ // for refreshing a DOM node's styles for example, since those may depend on
+ // media-queries.
+ resize: {
+ type: "resize",
+ },
+ },
+
+ methods: {
+ release: {
+ release: true,
+ },
+ document: {
+ request: { node: Arg(0, "nullable:domnode") },
+ response: { node: RetVal("domnode") },
+ },
+ documentElement: {
+ request: { node: Arg(0, "nullable:domnode") },
+ response: { node: RetVal("domnode") },
+ },
+ retainNode: {
+ request: { node: Arg(0, "domnode") },
+ response: {},
+ },
+ unretainNode: {
+ request: { node: Arg(0, "domnode") },
+ response: {},
+ },
+ releaseNode: {
+ request: {
+ node: Arg(0, "domnode"),
+ force: Option(1),
+ },
+ },
+ children: nodeArrayMethod,
+ nextSibling: traversalMethod,
+ previousSibling: traversalMethod,
+ findInspectingNode: {
+ request: {},
+ response: RetVal("disconnectedNode"),
+ },
+ querySelector: {
+ request: {
+ node: Arg(0, "domnode"),
+ selector: Arg(1),
+ },
+ response: RetVal("disconnectedNode"),
+ },
+ querySelectorAll: {
+ request: {
+ node: Arg(0, "domnode"),
+ selector: Arg(1),
+ },
+ response: {
+ list: RetVal("domnodelist"),
+ },
+ },
+ search: {
+ request: {
+ query: Arg(0),
+ },
+ response: {
+ list: RetVal("searchresult"),
+ },
+ },
+ getSuggestionsForQuery: {
+ request: {
+ query: Arg(0),
+ completing: Arg(1),
+ selectorState: Arg(2),
+ },
+ response: {
+ list: RetVal("array:array:string"),
+ },
+ },
+ addPseudoClassLock: {
+ request: {
+ node: Arg(0, "domnode"),
+ pseudoClass: Arg(1),
+ parents: Option(2),
+ enabled: Option(2, "boolean"),
+ },
+ response: {},
+ },
+ hideNode: {
+ request: { node: Arg(0, "domnode") },
+ },
+ unhideNode: {
+ request: { node: Arg(0, "domnode") },
+ },
+ removePseudoClassLock: {
+ request: {
+ node: Arg(0, "domnode"),
+ pseudoClass: Arg(1),
+ parents: Option(2),
+ },
+ response: {},
+ },
+ clearPseudoClassLocks: {
+ request: {
+ node: Arg(0, "nullable:domnode"),
+ },
+ response: {},
+ },
+ innerHTML: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ value: RetVal("longstring"),
+ },
+ },
+ setInnerHTML: {
+ request: {
+ node: Arg(0, "domnode"),
+ value: Arg(1, "string"),
+ },
+ response: {},
+ },
+ outerHTML: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ value: RetVal("longstring"),
+ },
+ },
+ setOuterHTML: {
+ request: {
+ node: Arg(0, "domnode"),
+ value: Arg(1, "string"),
+ },
+ response: {},
+ },
+ insertAdjacentHTML: {
+ request: {
+ node: Arg(0, "domnode"),
+ position: Arg(1, "string"),
+ value: Arg(2, "string"),
+ },
+ response: RetVal("disconnectedNodeArray"),
+ },
+ duplicateNode: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {},
+ },
+ removeNode: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ nextSibling: RetVal("nullable:domnode"),
+ },
+ },
+ removeNodes: {
+ request: {
+ node: Arg(0, "array:domnode"),
+ },
+ response: {},
+ },
+ insertBefore: {
+ request: {
+ node: Arg(0, "domnode"),
+ parent: Arg(1, "domnode"),
+ sibling: Arg(2, "nullable:domnode"),
+ },
+ response: {},
+ },
+ editTagName: {
+ request: {
+ node: Arg(0, "domnode"),
+ tagName: Arg(1, "string"),
+ },
+ response: {},
+ },
+ getMutations: {
+ request: {
+ cleanup: Option(0),
+ },
+ response: {
+ mutations: RetVal("array:dommutation"),
+ },
+ },
+ isInDOMTree: {
+ request: { node: Arg(0, "domnode") },
+ response: { attached: RetVal("boolean") },
+ },
+ getNodeActorFromWindowID: {
+ request: {
+ windowID: Arg(0, "string"),
+ },
+ response: {
+ nodeFront: RetVal("nullable:disconnectedNode"),
+ },
+ },
+ getNodeActorFromContentDomReference: {
+ request: {
+ contentDomReference: Arg(0, "json"),
+ },
+ response: {
+ nodeFront: RetVal("nullable:disconnectedNode"),
+ },
+ },
+ getStyleSheetOwnerNode: {
+ request: {
+ styleSheetActorID: Arg(0, "string"),
+ },
+ response: {
+ ownerNode: RetVal("nullable:disconnectedNode"),
+ },
+ },
+ getNodeFromActor: {
+ request: {
+ actorID: Arg(0, "string"),
+ path: Arg(1, "array:string"),
+ },
+ response: {
+ node: RetVal("nullable:disconnectedNode"),
+ },
+ },
+ getLayoutInspector: {
+ request: {},
+ response: {
+ actor: RetVal("layout"),
+ },
+ },
+ getParentGridNode: {
+ request: {
+ node: Arg(0, "nullable:domnode"),
+ },
+ response: {
+ node: RetVal("nullable:domnode"),
+ },
+ },
+ getOffsetParent: {
+ request: {
+ node: Arg(0, "nullable:domnode"),
+ },
+ response: {
+ node: RetVal("nullable:domnode"),
+ },
+ },
+ setMutationBreakpoints: {
+ request: {
+ node: Arg(0, "nullable:domnode"),
+ subtree: Option(1, "nullable:boolean"),
+ removal: Option(1, "nullable:boolean"),
+ attribute: Option(1, "nullable:boolean"),
+ },
+ response: {},
+ },
+ getEmbedderElement: {
+ request: {
+ browsingContextID: Arg(0, "string"),
+ },
+ response: {
+ nodeFront: RetVal("disconnectedNode"),
+ },
+ },
+ pick: {
+ request: {
+ doFocus: Arg(0, "nullable:boolean"),
+ isLocalTab: Arg(1, "nullable:boolean"),
+ },
+ },
+ cancelPick: {
+ request: {},
+ response: {},
+ },
+ clearPicker: {
+ request: {},
+ oneway: true,
+ },
+ watchRootNode: {
+ request: {},
+ response: {},
+ },
+ getOverflowCausingElements: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ list: RetVal("disconnectedNodeArray"),
+ },
+ },
+ getScrollableAncestorNode: {
+ request: {
+ node: Arg(0, "domnode"),
+ },
+ response: {
+ node: RetVal("nullable:domnode"),
+ },
+ },
+ },
+});
+
+exports.walkerSpec = walkerSpec;
diff --git a/devtools/shared/specs/watcher.js b/devtools/shared/specs/watcher.js
new file mode 100644
index 0000000000..9ba19900eb
--- /dev/null
+++ b/devtools/shared/specs/watcher.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ generateActorSpec,
+ Arg,
+ RetVal,
+} = require("resource://devtools/shared/protocol.js");
+
+const watcherSpecPrototype = {
+ typeName: "watcher",
+
+ methods: {
+ watchTargets: {
+ request: {
+ targetType: Arg(0, "string"),
+ },
+ response: {},
+ },
+
+ unwatchTargets: {
+ request: {
+ targetType: Arg(0, "string"),
+ options: Arg(1, "nullable:json"),
+ },
+ oneway: true,
+ },
+
+ getParentBrowsingContextID: {
+ request: {
+ browsingContextID: Arg(0, "number"),
+ },
+ response: {
+ browsingContextID: RetVal("nullable:number"),
+ },
+ },
+
+ watchResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ response: {},
+ },
+
+ unwatchResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ oneway: true,
+ },
+
+ clearResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ oneway: true,
+ },
+
+ getNetworkParentActor: {
+ request: {},
+ response: {
+ network: RetVal("networkParent"),
+ },
+ },
+
+ getBlackboxingActor: {
+ request: {},
+ response: {
+ blackboxing: RetVal("blackboxing"),
+ },
+ },
+
+ getBreakpointListActor: {
+ request: {},
+ response: {
+ breakpointList: RetVal("breakpoint-list"),
+ },
+ },
+
+ getTargetConfigurationActor: {
+ request: {},
+ response: {
+ configuration: RetVal("target-configuration"),
+ },
+ },
+
+ getThreadConfigurationActor: {
+ request: {},
+ response: {
+ configuration: RetVal("thread-configuration"),
+ },
+ },
+ },
+
+ events: {
+ "target-available-form": {
+ type: "target-available-form",
+ target: Arg(0, "json"),
+ },
+ "target-destroyed-form": {
+ type: "target-destroyed-form",
+ target: Arg(0, "json"),
+ options: Arg(1, "nullable:json"),
+ },
+
+ "resource-available-form": {
+ type: "resource-available-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-destroyed-form": {
+ type: "resource-destroyed-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-updated-form": {
+ type: "resource-updated-form",
+ resources: Arg(0, "array:json"),
+ },
+ },
+};
+
+exports.watcherSpec = generateActorSpec(watcherSpecPrototype);
diff --git a/devtools/shared/specs/webconsole.js b/devtools/shared/specs/webconsole.js
new file mode 100644
index 0000000000..36c6d3595e
--- /dev/null
+++ b/devtools/shared/specs/webconsole.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ types,
+ generateActorSpec,
+ RetVal,
+ Option,
+ Arg,
+} = require("resource://devtools/shared/protocol.js");
+
+types.addDictType("console.startlisteners", {
+ startedListeners: "array:string",
+});
+
+types.addDictType("console.stoplisteners", {
+ stoppedListeners: "array:string",
+});
+
+types.addDictType("console.autocomplete", {
+ matches: "array:string",
+ matchProp: "string",
+});
+
+types.addDictType("console.evaluatejsasync", {
+ resultID: "string",
+});
+
+types.addDictType("console.cachedmessages", {
+ // this type is a union of two potential return types:
+ // { error, message } and { _type, message, timeStamp }
+ error: "nullable:string",
+ message: "longstring",
+ _type: "nullable:string",
+ timeStamp: "nullable:string",
+});
+
+const webconsoleSpecPrototype = {
+ typeName: "console",
+
+ events: {
+ evaluationResult: {
+ resultID: Option(0, "string"),
+ awaitResult: Option(0, "nullable:boolean"),
+ errorMessageName: Option(0, "nullable:string"),
+ exception: Option(0, "nullable:json"),
+ exceptionMessage: Option(0, "nullable:string"),
+ exceptionDocURL: Option(0, "nullable:string"),
+ exceptionStack: Option(0, "nullable:json"),
+ hasException: Option(0, "nullable:boolean"),
+ frame: Option(0, "nullable:json"),
+ helperResult: Option(0, "nullable:json"),
+ input: Option(0, "nullable:string"),
+ notes: Option(0, "nullable:string"),
+ result: Option(0, "nullable:json"),
+ startTime: Option(0, "number"),
+ timestamp: Option(0, "number"),
+ topLevelAwaitRejected: Option(0, "nullable:boolean"),
+ },
+ fileActivity: {
+ uri: Option(0, "string"),
+ },
+ pageError: {
+ pageError: Option(0, "json"),
+ },
+ logMessage: {
+ message: Option(0, "json"),
+ timeStamp: Option(0, "string"),
+ },
+ consoleAPICall: {
+ message: Option(0, "json"),
+ clonedFromContentProcess: Option(0, "nullable:boolean"),
+ },
+ reflowActivity: {
+ interruptible: Option(0, "boolean"),
+ start: Option(0, "number"),
+ end: Option(0, "number"),
+ sourceURL: Option(0, "nullable:string"),
+ sourceLine: Option(0, "nullable:number"),
+ functionName: Option(0, "nullable:string"),
+ },
+ // This event is modified re-emitted on the client as "networkEvent".
+ // In order to avoid a naming collision, we rename the server event.
+ serverNetworkEvent: {
+ type: "networkEvent",
+ eventActor: Option(0, "json"),
+ },
+ inspectObject: {
+ objectActor: Option(0, "json"),
+ },
+ documentEvent: {
+ name: Option(0, "string"),
+ time: Option(0, "string"),
+ hasNativeConsoleAPI: Option(0, "boolean"),
+ },
+ },
+
+ methods: {
+ /**
+ * Start the given Web Console listeners.
+ *
+ * @see webconsoleFront LISTENERS
+ * @Arg array events
+ * Array of events you want to start. See this.LISTENERS for
+ * known events.
+ */
+ startListeners: {
+ request: {
+ listeners: Arg(0, "array:string"),
+ },
+ response: RetVal("console.startlisteners"),
+ },
+ /**
+ * Stop the given Web Console listeners.
+ *
+ * @see webconsoleFront LISTENERS
+ * @Arg array events
+ * Array of events you want to stop. See this.LISTENERS for
+ * known events.
+ * @Arg function onResponse
+ * Function to invoke when the server response is received.
+ */
+ stopListeners: {
+ request: {
+ listeners: Arg(0, "nullable:array:string"),
+ },
+ response: RetVal("console.stoplisteners"),
+ },
+ /**
+ * Retrieve the cached messages from the server.
+ *
+ * @see webconsoleFront CACHED_MESSAGES
+ * @Arg array types
+ * The array of message types you want from the server. See
+ * this.CACHED_MESSAGES for known types.
+ */
+ getCachedMessages: {
+ request: {
+ messageTypes: Arg(0, "array:string"),
+ },
+ // the return value here has a field "string" which can either be a longStringActor
+ // or a plain string. Since we do not have union types, we cannot fully type this
+ // response
+ response: RetVal("console.cachedmessages"),
+ },
+ evaluateJSAsync: {
+ request: {
+ text: Option(0, "string"),
+ frameActor: Option(0, "string"),
+ url: Option(0, "string"),
+ selectedNodeActor: Option(0, "string"),
+ selectedObjectActor: Option(0, "string"),
+ innerWindowID: Option(0, "number"),
+ mapped: Option(0, "nullable:json"),
+ eager: Option(0, "nullable:boolean"),
+ disableBreaks: Option(0, "nullable:boolean"),
+ preferConsoleCommandsOverLocalSymbols: Option(0, "nullable:boolean"),
+ },
+ response: RetVal("console.evaluatejsasync"),
+ },
+ /**
+ * Autocomplete a JavaScript expression.
+ *
+ * @Arg {String} string
+ * The code you want to autocomplete.
+ * @Arg {Number} cursor
+ * Cursor location inside the string. Index starts from 0.
+ * @Arg {String} frameActor
+ * The id of the frame actor that made the call.
+ * @Arg {String} selectedNodeActor: Actor id of the selected node in the inspector.
+ * @Arg {Array} authorizedEvaluations
+ * Array of the properties access which can be executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "anotherGetter"]] to
+ * retrieve properties of `x.myGetter.` and `x.myGetter.y.anotherGetter`.
+ */
+ autocomplete: {
+ request: {
+ text: Arg(0, "string"),
+ cursor: Arg(1, "nullable:number"),
+ frameActor: Arg(2, "nullable:string"),
+ selectedNodeActor: Arg(3, "nullable:string"),
+ authorizedEvaluations: Arg(4, "nullable:json"),
+ expressionVars: Arg(5, "nullable:json"),
+ },
+ response: RetVal("console.autocomplete"),
+ },
+
+ /**
+ * Same as clearMessagesCache, but wait for the server response.
+ */
+ clearMessagesCacheAsync: {
+ request: {},
+ },
+ },
+};
+
+const webconsoleSpec = generateActorSpec(webconsoleSpecPrototype);
+
+exports.webconsoleSpecPrototype = webconsoleSpecPrototype;
+exports.webconsoleSpec = webconsoleSpec;
diff --git a/devtools/shared/specs/worker/moz.build b/devtools/shared/specs/worker/moz.build
new file mode 100644
index 0000000000..dae0e2d606
--- /dev/null
+++ b/devtools/shared/specs/worker/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(
+ "push-subscription.js",
+ "service-worker-registration.js",
+ "service-worker.js",
+)
diff --git a/devtools/shared/specs/worker/push-subscription.js b/devtools/shared/specs/worker/push-subscription.js
new file mode 100644
index 0000000000..f68a74eb00
--- /dev/null
+++ b/devtools/shared/specs/worker/push-subscription.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { generateActorSpec } = require("resource://devtools/shared/protocol.js");
+
+const pushSubscriptionSpec = generateActorSpec({
+ typeName: "pushSubscription",
+});
+
+exports.pushSubscriptionSpec = pushSubscriptionSpec;
diff --git a/devtools/shared/specs/worker/service-worker-registration.js b/devtools/shared/specs/worker/service-worker-registration.js
new file mode 100644
index 0000000000..b6b0fd5286
--- /dev/null
+++ b/devtools/shared/specs/worker/service-worker-registration.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ RetVal,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const serviceWorkerRegistrationSpec = generateActorSpec({
+ typeName: "serviceWorkerRegistration",
+
+ events: {
+ "push-subscription-modified": {
+ type: "push-subscription-modified",
+ },
+ "registration-changed": {
+ type: "registration-changed",
+ },
+ },
+
+ methods: {
+ allowShutdown: {
+ request: {},
+ },
+ preventShutdown: {
+ request: {},
+ },
+ push: {
+ request: {},
+ },
+ start: {
+ request: {},
+ },
+ unregister: {
+ request: {},
+ },
+ getPushSubscription: {
+ request: {},
+ response: {
+ subscription: RetVal("nullable:pushSubscription"),
+ },
+ },
+ },
+});
+
+exports.serviceWorkerRegistrationSpec = serviceWorkerRegistrationSpec;
diff --git a/devtools/shared/specs/worker/service-worker.js b/devtools/shared/specs/worker/service-worker.js
new file mode 100644
index 0000000000..8ddf151890
--- /dev/null
+++ b/devtools/shared/specs/worker/service-worker.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { generateActorSpec } = require("resource://devtools/shared/protocol.js");
+
+const serviceWorkerSpec = generateActorSpec({
+ typeName: "serviceWorker",
+});
+
+exports.serviceWorkerSpec = serviceWorkerSpec;
diff --git a/devtools/shared/sprintfjs/UPGRADING.md b/devtools/shared/sprintfjs/UPGRADING.md
new file mode 100644
index 0000000000..e0db44df67
--- /dev/null
+++ b/devtools/shared/sprintfjs/UPGRADING.md
@@ -0,0 +1,17 @@
+SPRINTF JS UPGRADING
+
+Original library at https://github.com/alexei/sprintf.js
+
+This library should no longer be upgraded from upstream. We added performance improvements
+in https://bugzilla.mozilla.org/show_bug.cgi?id=1406311. Most importantly removing the
+usage of the get_type() method as well as prioritizing the %S use case.
+
+If for some reason, updating from upstream becomes necessary, please refer to the bug
+mentioned above to reimplement the performance fixes in the new version.
+
+By default the library only supports string placeholders using %s (lowercase) while we use
+%S (uppercase). The library also has to be manually patched in order to support it.
+
+- grab the unminified version at https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js
+- update the re.placeholder regexp to allow "S" as well as "s"
+- update the switch statement in the format() method to make case "S" equivalent to case "s"
diff --git a/devtools/shared/sprintfjs/moz.build b/devtools/shared/sprintfjs/moz.build
new file mode 100644
index 0000000000..6f4f1f8734
--- /dev/null
+++ b/devtools/shared/sprintfjs/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ 'sprintf.js'
+)
diff --git a/devtools/shared/sprintfjs/sprintf.js b/devtools/shared/sprintfjs/sprintf.js
new file mode 100644
index 0000000000..fd53cd3ce9
--- /dev/null
+++ b/devtools/shared/sprintfjs/sprintf.js
@@ -0,0 +1,283 @@
+/**
+ * Copyright (c) 2007-2016, Alexandru Marasteanu <hello [at) alexei (dot] ro>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of this software nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+/* eslint-disable */
+/* globals window, exports, define */
+
+(function(window) {
+ 'use strict'
+
+ var re = {
+ not_string: /[^s]/,
+ not_bool: /[^t]/,
+ not_type: /[^T]/,
+ not_primitive: /[^v]/,
+ number: /[diefg]/,
+ numeric_arg: /bcdiefguxX/,
+ json: /[j]/,
+ not_json: /[^j]/,
+ text: /^[^\x25]+/,
+ modulo: /^\x25{2}/,
+ placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosStTuvxX])/,
+ key: /^([a-z_][a-z_\d]*)/i,
+ key_access: /^\.([a-z_][a-z_\d]*)/i,
+ index_access: /^\[(\d+)\]/,
+ sign: /^[\+\-]/
+ }
+
+ function sprintf() {
+ var key = arguments[0], cache = sprintf.cache
+ if (!(cache[key] && cache.hasOwnProperty(key))) {
+ cache[key] = sprintf.parse(key)
+ }
+ return sprintf.format.call(null, cache[key], arguments)
+ }
+
+ sprintf.format = function(parse_tree, argv) {
+ var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length, is_positive = true, sign = ''
+ for (i = 0; i < tree_length; i++) {
+ node_type = typeof parse_tree[i]
+ // The items of parse tree are either strings or results of a match() call.
+ if (node_type === 'string') {
+ // this is not a placeholder, this is just a string.
+ output[output.length] = parse_tree[i]
+ }
+ else {
+ // this is a placeholder, need to identify its type, options and replace
+ // it with the appropriate argument.
+ match = parse_tree[i] // convenience purposes only
+ if (match[2]) { // keyword argument
+ arg = argv[cursor]
+ for (k = 0; k < match[2].length; k++) {
+ if (!arg.hasOwnProperty(match[2][k])) {
+ throw new Error(sprintf('[sprintf] property "%s" does not exist', match[2][k]))
+ }
+ arg = arg[match[2][k]]
+ }
+ }
+ else if (match[1]) { // positional argument (explicit)
+ arg = argv[match[1]]
+ }
+ else { // positional argument (implicit)
+ arg = argv[cursor++]
+ }
+
+ // The most commonly used placeholder in DevTools is the string (%S or %s).
+ // We check it first to avoid unnecessary verifications.
+ let hasPadding = match[6];
+ let patternType = match[8];
+ if (!hasPadding && (patternType === "S" || patternType === "s")) {
+ if (typeof arg === "function") {
+ arg = arg();
+ }
+ if (typeof arg !== "string") {
+ arg = String(arg);
+ }
+ output[output.length] = match[7] ? arg.substring(0, match[7]) : arg;
+ continue;
+ }
+
+ if (re.not_type.test(match[8]) && re.not_primitive.test(match[8]) && typeof arg == 'function') {
+ arg = arg()
+ }
+
+ if (re.numeric_arg.test(match[8]) && (typeof arg != 'number' && isNaN(arg))) {
+ throw new TypeError(sprintf("[sprintf] expecting number but found %s", typeof arg))
+ }
+
+ if (re.number.test(match[8])) {
+ is_positive = arg >= 0
+ }
+
+ switch (match[8]) {
+ case 'b':
+ arg = parseInt(arg, 10).toString(2)
+ break
+ case 'c':
+ arg = String.fromCharCode(parseInt(arg, 10))
+ break
+ case 'd':
+ case 'i':
+ arg = parseInt(arg, 10)
+ break
+ case 'j':
+ arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0)
+ break
+ case 'e':
+ arg = match[7] ? parseFloat(arg).toExponential(match[7]) : parseFloat(arg).toExponential()
+ break
+ case 'f':
+ arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg)
+ break
+ case 'g':
+ arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg)
+ break
+ case 'o':
+ arg = arg.toString(8)
+ break
+ case 's':
+ case 'S':
+ arg = String(arg)
+ arg = (match[7] ? arg.substring(0, match[7]) : arg)
+ break
+ case 't':
+ arg = String(!!arg)
+ arg = (match[7] ? arg.substring(0, match[7]) : arg)
+ break
+ case 'T':
+ arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
+ arg = (match[7] ? arg.substring(0, match[7]) : arg)
+ break
+ case 'u':
+ arg = parseInt(arg, 10) >>> 0
+ break
+ case 'v':
+ arg = arg.valueOf()
+ arg = (match[7] ? arg.substring(0, match[7]) : arg)
+ break
+ case 'x':
+ arg = parseInt(arg, 10).toString(16)
+ break
+ case 'X':
+ arg = parseInt(arg, 10).toString(16).toUpperCase()
+ break
+ }
+ if (re.json.test(match[8])) {
+ output[output.length] = arg
+ }
+ else {
+ if (re.number.test(match[8]) && (!is_positive || match[3])) {
+ sign = is_positive ? '+' : '-'
+ arg = arg.toString().replace(re.sign, '')
+ }
+ else {
+ sign = ''
+ }
+ pad_character = match[4] ? match[4] === '0' ? '0' : match[4].charAt(1) : ' '
+ pad_length = match[6] - (sign + arg).length
+ pad = match[6] ? (pad_length > 0 ? str_repeat(pad_character, pad_length) : '') : ''
+ output[output.length] = match[5] ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
+ }
+ }
+ }
+ return output.join('')
+ }
+
+ sprintf.cache = {}
+
+ sprintf.parse = function(fmt) {
+ var _fmt = fmt, match = [], parse_tree = [], arg_names = 0
+ while (_fmt) {
+ if ((match = re.text.exec(_fmt)) !== null) {
+ parse_tree[parse_tree.length] = match[0]
+ }
+ else if ((match = re.modulo.exec(_fmt)) !== null) {
+ parse_tree[parse_tree.length] = '%'
+ }
+ else if ((match = re.placeholder.exec(_fmt)) !== null) {
+ if (match[2]) {
+ arg_names |= 1
+ var field_list = [], replacement_field = match[2], field_match = []
+ if ((field_match = re.key.exec(replacement_field)) !== null) {
+ field_list[field_list.length] = field_match[1]
+ while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+ if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+ field_list[field_list.length] = field_match[1]
+ }
+ else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+ field_list[field_list.length] = field_match[1]
+ }
+ else {
+ throw new SyntaxError("[sprintf] failed to parse named argument key")
+ }
+ }
+ }
+ else {
+ throw new SyntaxError("[sprintf] failed to parse named argument key")
+ }
+ match[2] = field_list
+ }
+ else {
+ arg_names |= 2
+ }
+ if (arg_names === 3) {
+ throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported")
+ }
+ parse_tree[parse_tree.length] = match
+ }
+ else {
+ throw new SyntaxError("[sprintf] unexpected placeholder")
+ }
+ _fmt = _fmt.substring(match[0].length)
+ }
+ return parse_tree
+ }
+
+ var vsprintf = function(fmt, argv, _argv) {
+ _argv = (argv || []).slice(0)
+ _argv.splice(0, 0, fmt)
+ return sprintf.apply(null, _argv)
+ }
+
+ /**
+ * helpers
+ */
+
+ var preformattedPadding = {
+ '0': ['', '0', '00', '000', '0000', '00000', '000000', '0000000'],
+ ' ': ['', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
+ '_': ['', '_', '__', '___', '____', '_____', '______', '_______'],
+ }
+ function str_repeat(input, multiplier) {
+ if (multiplier >= 0 && multiplier <= 7 && preformattedPadding[input]) {
+ return preformattedPadding[input][multiplier]
+ }
+ return Array(multiplier + 1).join(input)
+ }
+
+ /**
+ * export to either browser or node.js
+ */
+ if (typeof exports !== 'undefined') {
+ exports.sprintf = sprintf
+ exports.vsprintf = vsprintf
+ }
+ else {
+ window.sprintf = sprintf
+ window.vsprintf = vsprintf
+
+ if (typeof define === 'function' && define.amd) {
+ define(function() {
+ return {
+ sprintf: sprintf,
+ vsprintf: vsprintf
+ }
+ })
+ }
+ }
+})(typeof window === 'undefined' ? this : window);
diff --git a/devtools/shared/storage/moz.build b/devtools/shared/storage/moz.build
new file mode 100644
index 0000000000..95cf2857de
--- /dev/null
+++ b/devtools/shared/storage/moz.build
@@ -0,0 +1,9 @@
+# -*- 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 += ["vendor"]
+
+DevToolsModules("utils.js")
diff --git a/devtools/shared/storage/utils.js b/devtools/shared/storage/utils.js
new file mode 100644
index 0000000000..2c31a4ae88
--- /dev/null
+++ b/devtools/shared/storage/utils.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";
+
+loader.lazyRequireGetter(
+ this,
+ "validator",
+ "resource://devtools/shared/storage/vendor/stringvalidator/validator.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "JSON5",
+ "resource://devtools/shared/storage/vendor/json5.js"
+);
+
+const MATH_REGEX =
+ /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/;
+
+/**
+ * Tries to parse a string into an object on the basis of key-value pairs,
+ * separated by various separators. If failed, tries to parse for single
+ * separator separated values to form an array.
+ *
+ * @param {string} value
+ * The string to be parsed into an object or array
+ */
+function _extractKeyValPairs(value) {
+ const makeObject = (keySep, pairSep) => {
+ const object = {};
+ for (const pair of value.split(pairSep)) {
+ const [key, val] = pair.split(keySep);
+ object[key] = val;
+ }
+ return object;
+ };
+
+ // Possible separators.
+ const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."];
+ // Testing for object
+ for (let i = 0; i < separators.length; i++) {
+ const kv = separators[i];
+ for (let j = 0; j < separators.length; j++) {
+ if (i == j) {
+ continue;
+ }
+ const p = separators[j];
+ const word = `[^${kv}${p}]*`;
+ const keyValue = `${word}${kv}${word}`;
+ const keyValueList = `${keyValue}(${p}${keyValue})*`;
+ const regex = new RegExp(`^${keyValueList}$`);
+ if (
+ value.match &&
+ value.match(regex) &&
+ value.includes(kv) &&
+ (value.includes(p) || value.split(kv).length == 2)
+ ) {
+ return makeObject(kv, p);
+ }
+ }
+ }
+ // Testing for array
+ for (const p of separators) {
+ const word = `[^${p}]*`;
+ const wordList = `(${word}${p})+${word}`;
+ const regex = new RegExp(`^${wordList}$`);
+
+ if (regex.test(value)) {
+ const pNoBackslash = p.replace(/\\*/g, "");
+ return value.split(pNoBackslash);
+ }
+ }
+ return null;
+}
+
+/**
+ * Check whether the value string represents something that should be
+ * displayed as text. If so then it shouldn't be parsed into a tree.
+ *
+ * @param {String} value
+ * The value to be parsed.
+ */
+function _shouldParse(value) {
+ const validators = [
+ "isBase64",
+ "isBoolean",
+ "isCurrency",
+ "isDataURI",
+ "isEmail",
+ "isFQDN",
+ "isHexColor",
+ "isIP",
+ "isISO8601",
+ "isMACAddress",
+ "isSemVer",
+ "isURL",
+ ];
+
+ // Check for minus calculations e.g. 8-3 because otherwise 5 will be displayed.
+ if (MATH_REGEX.test(value)) {
+ return false;
+ }
+
+ // Check for any other types that shouldn't be parsed.
+ for (const test of validators) {
+ if (validator[test](value)) {
+ return false;
+ }
+ }
+
+ // Seems like this is data that should be parsed.
+ return true;
+}
+
+/**
+ * Tries to parse a string value into either a json or a key-value separated
+ * object. The value can also be a key separated array.
+ *
+ * @param {string} originalValue
+ * The string to be parsed into an object
+ */
+function parseItemValue(originalValue) {
+ // Find if value is URLEncoded ie
+ let decodedValue = "";
+ try {
+ decodedValue = decodeURIComponent(originalValue);
+ } catch (e) {
+ // Unable to decode, nothing to do
+ }
+ const value =
+ decodedValue && decodedValue !== originalValue
+ ? decodedValue
+ : originalValue;
+
+ if (!_shouldParse(value)) {
+ return value;
+ }
+
+ let obj = null;
+ try {
+ obj = JSON5.parse(value);
+ } catch (ex) {
+ obj = null;
+ }
+
+ if (!obj && value) {
+ obj = _extractKeyValPairs(value);
+ }
+
+ // return if obj is null, or same as value, or just a string.
+ if (!obj || obj === value || typeof obj === "string") {
+ return value;
+ }
+
+ // If we got this far, originalValue is an object literal or array,
+ // and we have successfully parsed it
+ return obj;
+}
+
+exports.parseItemValue = parseItemValue;
diff --git a/devtools/shared/storage/vendor/JSON5_LICENSE b/devtools/shared/storage/vendor/JSON5_LICENSE
new file mode 100644
index 0000000000..2171aca5a8
--- /dev/null
+++ b/devtools/shared/storage/vendor/JSON5_LICENSE
@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) 2012-2018 Aseem Kishore, and [others].
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+[others]: https://github.com/json5/json5/contributors
diff --git a/devtools/shared/storage/vendor/JSON5_UPGRADING.md b/devtools/shared/storage/vendor/JSON5_UPGRADING.md
new file mode 100644
index 0000000000..631d73ee46
--- /dev/null
+++ b/devtools/shared/storage/vendor/JSON5_UPGRADING.md
@@ -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/.
+)
+
+# Upgrading json5
+
+## Getting the Source
+
+```bash
+git clone https://github.com/json5/json5
+cd json5
+git checkout v2.2.1 # checkout the right version tag
+```
+
+## Building
+
+```bash
+npm install
+npm run build
+cp dist/index.js <gecko-dev>/devtools/shared/storage/vendor/json5.js
+```
+
+## Patching json5
+
+- open `json5.js`
+- Add the version number to the top of the file:
+ ```
+ /**
+ * json5 v2.2.1
+ */
+ ```
+- Replace instances of `Function('return this')()` with `globalThis`. See Bug 1473549.
+
+## Update the version:
+
+The current version is 2.2.1. Update this version number everywhere in this file.
diff --git a/devtools/shared/storage/vendor/json5.js b/devtools/shared/storage/vendor/json5.js
new file mode 100644
index 0000000000..667d78ffdd
--- /dev/null
+++ b/devtools/shared/storage/vendor/json5.js
@@ -0,0 +1,1713 @@
+/**
+ * json5 v2.2.1
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.JSON5 = factory());
+}(this, (function () { 'use strict';
+
+ function createCommonjsModule(fn, module) {
+ return module = { exports: {} }, fn(module, module.exports), module.exports;
+ }
+
+ var _global = createCommonjsModule(function (module) {
+ // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
+ var global = module.exports = typeof window != 'undefined' && window.Math == Math
+ ? window : typeof self != 'undefined' && self.Math == Math ? self
+ : globalThis;
+ if (typeof __g == 'number') { __g = global; } // eslint-disable-line no-undef
+ });
+
+ var _core = createCommonjsModule(function (module) {
+ var core = module.exports = { version: '2.6.5' };
+ if (typeof __e == 'number') { __e = core; } // eslint-disable-line no-undef
+ });
+ var _core_1 = _core.version;
+
+ var _isObject = function (it) {
+ return typeof it === 'object' ? it !== null : typeof it === 'function';
+ };
+
+ var _anObject = function (it) {
+ if (!_isObject(it)) { throw TypeError(it + ' is not an object!'); }
+ return it;
+ };
+
+ var _fails = function (exec) {
+ try {
+ return !!exec();
+ } catch (e) {
+ return true;
+ }
+ };
+
+ // Thank's IE8 for his funny defineProperty
+ var _descriptors = !_fails(function () {
+ return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7;
+ });
+
+ var document = _global.document;
+ // typeof document.createElement is 'object' in old IE
+ var is = _isObject(document) && _isObject(document.createElement);
+ var _domCreate = function (it) {
+ return is ? document.createElement(it) : {};
+ };
+
+ var _ie8DomDefine = !_descriptors && !_fails(function () {
+ return Object.defineProperty(_domCreate('div'), 'a', { get: function () { return 7; } }).a != 7;
+ });
+
+ // 7.1.1 ToPrimitive(input [, PreferredType])
+
+ // instead of the ES6 spec version, we didn't implement @@toPrimitive case
+ // and the second argument - flag - preferred type is a string
+ var _toPrimitive = function (it, S) {
+ if (!_isObject(it)) { return it; }
+ var fn, val;
+ if (S && typeof (fn = it.toString) == 'function' && !_isObject(val = fn.call(it))) { return val; }
+ if (typeof (fn = it.valueOf) == 'function' && !_isObject(val = fn.call(it))) { return val; }
+ if (!S && typeof (fn = it.toString) == 'function' && !_isObject(val = fn.call(it))) { return val; }
+ throw TypeError("Can't convert object to primitive value");
+ };
+
+ var dP = Object.defineProperty;
+
+ var f = _descriptors ? Object.defineProperty : function defineProperty(O, P, Attributes) {
+ _anObject(O);
+ P = _toPrimitive(P, true);
+ _anObject(Attributes);
+ if (_ie8DomDefine) { try {
+ return dP(O, P, Attributes);
+ } catch (e) { /* empty */ } }
+ if ('get' in Attributes || 'set' in Attributes) { throw TypeError('Accessors not supported!'); }
+ if ('value' in Attributes) { O[P] = Attributes.value; }
+ return O;
+ };
+
+ var _objectDp = {
+ f: f
+ };
+
+ var _propertyDesc = function (bitmap, value) {
+ return {
+ enumerable: !(bitmap & 1),
+ configurable: !(bitmap & 2),
+ writable: !(bitmap & 4),
+ value: value
+ };
+ };
+
+ var _hide = _descriptors ? function (object, key, value) {
+ return _objectDp.f(object, key, _propertyDesc(1, value));
+ } : function (object, key, value) {
+ object[key] = value;
+ return object;
+ };
+
+ var hasOwnProperty = {}.hasOwnProperty;
+ var _has = function (it, key) {
+ return hasOwnProperty.call(it, key);
+ };
+
+ var id = 0;
+ var px = Math.random();
+ var _uid = function (key) {
+ return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36));
+ };
+
+ var _library = false;
+
+ var _shared = createCommonjsModule(function (module) {
+ var SHARED = '__core-js_shared__';
+ var store = _global[SHARED] || (_global[SHARED] = {});
+
+ (module.exports = function (key, value) {
+ return store[key] || (store[key] = value !== undefined ? value : {});
+ })('versions', []).push({
+ version: _core.version,
+ mode: _library ? 'pure' : 'global',
+ copyright: '© 2019 Denis Pushkarev (zloirock.ru)'
+ });
+ });
+
+ var _functionToString = _shared('native-function-to-string', Function.toString);
+
+ var _redefine = createCommonjsModule(function (module) {
+ var SRC = _uid('src');
+
+ var TO_STRING = 'toString';
+ var TPL = ('' + _functionToString).split(TO_STRING);
+
+ _core.inspectSource = function (it) {
+ return _functionToString.call(it);
+ };
+
+ (module.exports = function (O, key, val, safe) {
+ var isFunction = typeof val == 'function';
+ if (isFunction) { _has(val, 'name') || _hide(val, 'name', key); }
+ if (O[key] === val) { return; }
+ if (isFunction) { _has(val, SRC) || _hide(val, SRC, O[key] ? '' + O[key] : TPL.join(String(key))); }
+ if (O === _global) {
+ O[key] = val;
+ } else if (!safe) {
+ delete O[key];
+ _hide(O, key, val);
+ } else if (O[key]) {
+ O[key] = val;
+ } else {
+ _hide(O, key, val);
+ }
+ // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative
+ })(Function.prototype, TO_STRING, function toString() {
+ return typeof this == 'function' && this[SRC] || _functionToString.call(this);
+ });
+ });
+
+ var _aFunction = function (it) {
+ if (typeof it != 'function') { throw TypeError(it + ' is not a function!'); }
+ return it;
+ };
+
+ // optional / simple context binding
+
+ var _ctx = function (fn, that, length) {
+ _aFunction(fn);
+ if (that === undefined) { return fn; }
+ switch (length) {
+ case 1: return function (a) {
+ return fn.call(that, a);
+ };
+ case 2: return function (a, b) {
+ return fn.call(that, a, b);
+ };
+ case 3: return function (a, b, c) {
+ return fn.call(that, a, b, c);
+ };
+ }
+ return function (/* ...args */) {
+ return fn.apply(that, arguments);
+ };
+ };
+
+ var PROTOTYPE = 'prototype';
+
+ var $export = function (type, name, source) {
+ var IS_FORCED = type & $export.F;
+ var IS_GLOBAL = type & $export.G;
+ var IS_STATIC = type & $export.S;
+ var IS_PROTO = type & $export.P;
+ var IS_BIND = type & $export.B;
+ var target = IS_GLOBAL ? _global : IS_STATIC ? _global[name] || (_global[name] = {}) : (_global[name] || {})[PROTOTYPE];
+ var exports = IS_GLOBAL ? _core : _core[name] || (_core[name] = {});
+ var expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {});
+ var key, own, out, exp;
+ if (IS_GLOBAL) { source = name; }
+ for (key in source) {
+ // contains in native
+ own = !IS_FORCED && target && target[key] !== undefined;
+ // export native or passed
+ out = (own ? target : source)[key];
+ // bind timers to global for call from export context
+ exp = IS_BIND && own ? _ctx(out, _global) : IS_PROTO && typeof out == 'function' ? _ctx(Function.call, out) : out;
+ // extend global
+ if (target) { _redefine(target, key, out, type & $export.U); }
+ // export
+ if (exports[key] != out) { _hide(exports, key, exp); }
+ if (IS_PROTO && expProto[key] != out) { expProto[key] = out; }
+ }
+ };
+ _global.core = _core;
+ // type bitmap
+ $export.F = 1; // forced
+ $export.G = 2; // global
+ $export.S = 4; // static
+ $export.P = 8; // proto
+ $export.B = 16; // bind
+ $export.W = 32; // wrap
+ $export.U = 64; // safe
+ $export.R = 128; // real proto method for `library`
+ var _export = $export;
+
+ // 7.1.4 ToInteger
+ var ceil = Math.ceil;
+ var floor = Math.floor;
+ var _toInteger = function (it) {
+ return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
+ };
+
+ // 7.2.1 RequireObjectCoercible(argument)
+ var _defined = function (it) {
+ if (it == undefined) { throw TypeError("Can't call method on " + it); }
+ return it;
+ };
+
+ // true -> String#at
+ // false -> String#codePointAt
+ var _stringAt = function (TO_STRING) {
+ return function (that, pos) {
+ var s = String(_defined(that));
+ var i = _toInteger(pos);
+ var l = s.length;
+ var a, b;
+ if (i < 0 || i >= l) { return TO_STRING ? '' : undefined; }
+ a = s.charCodeAt(i);
+ return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff
+ ? TO_STRING ? s.charAt(i) : a
+ : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000;
+ };
+ };
+
+ var $at = _stringAt(false);
+ _export(_export.P, 'String', {
+ // 21.1.3.3 String.prototype.codePointAt(pos)
+ codePointAt: function codePointAt(pos) {
+ return $at(this, pos);
+ }
+ });
+
+ var codePointAt = _core.String.codePointAt;
+
+ var max = Math.max;
+ var min = Math.min;
+ var _toAbsoluteIndex = function (index, length) {
+ index = _toInteger(index);
+ return index < 0 ? max(index + length, 0) : min(index, length);
+ };
+
+ var fromCharCode = String.fromCharCode;
+ var $fromCodePoint = String.fromCodePoint;
+
+ // length should be 1, old FF problem
+ _export(_export.S + _export.F * (!!$fromCodePoint && $fromCodePoint.length != 1), 'String', {
+ // 21.1.2.2 String.fromCodePoint(...codePoints)
+ fromCodePoint: function fromCodePoint(x) {
+ var arguments$1 = arguments;
+ // eslint-disable-line no-unused-vars
+ var res = [];
+ var aLen = arguments.length;
+ var i = 0;
+ var code;
+ while (aLen > i) {
+ code = +arguments$1[i++];
+ if (_toAbsoluteIndex(code, 0x10ffff) !== code) { throw RangeError(code + ' is not a valid code point'); }
+ res.push(code < 0x10000
+ ? fromCharCode(code)
+ : fromCharCode(((code -= 0x10000) >> 10) + 0xd800, code % 0x400 + 0xdc00)
+ );
+ } return res.join('');
+ }
+ });
+
+ var fromCodePoint = _core.String.fromCodePoint;
+
+ // This is a generated file. Do not edit.
+ var Space_Separator = /[\u1680\u2000-\u200A\u202F\u205F\u3000]/;
+ var ID_Start = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/;
+ var ID_Continue = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/;
+
+ var unicode = {
+ Space_Separator: Space_Separator,
+ ID_Start: ID_Start,
+ ID_Continue: ID_Continue
+ };
+
+ var util = {
+ isSpaceSeparator: function isSpaceSeparator (c) {
+ return typeof c === 'string' && unicode.Space_Separator.test(c)
+ },
+
+ isIdStartChar: function isIdStartChar (c) {
+ return typeof c === 'string' && (
+ (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c === '$') || (c === '_') ||
+ unicode.ID_Start.test(c)
+ )
+ },
+
+ isIdContinueChar: function isIdContinueChar (c) {
+ return typeof c === 'string' && (
+ (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ (c === '$') || (c === '_') ||
+ (c === '\u200C') || (c === '\u200D') ||
+ unicode.ID_Continue.test(c)
+ )
+ },
+
+ isDigit: function isDigit (c) {
+ return typeof c === 'string' && /[0-9]/.test(c)
+ },
+
+ isHexDigit: function isHexDigit (c) {
+ return typeof c === 'string' && /[0-9A-Fa-f]/.test(c)
+ },
+ };
+
+ var source;
+ var parseState;
+ var stack;
+ var pos;
+ var line;
+ var column;
+ var token;
+ var key;
+ var root;
+
+ var parse = function parse (text, reviver) {
+ source = String(text);
+ parseState = 'start';
+ stack = [];
+ pos = 0;
+ line = 1;
+ column = 0;
+ token = undefined;
+ key = undefined;
+ root = undefined;
+
+ do {
+ token = lex();
+
+ // This code is unreachable.
+ // if (!parseStates[parseState]) {
+ // throw invalidParseState()
+ // }
+
+ parseStates[parseState]();
+ } while (token.type !== 'eof')
+
+ if (typeof reviver === 'function') {
+ return internalize({'': root}, '', reviver)
+ }
+
+ return root
+ };
+
+ function internalize (holder, name, reviver) {
+ var value = holder[name];
+ if (value != null && typeof value === 'object') {
+ for (var key in value) {
+ var replacement = internalize(value, key, reviver);
+ if (replacement === undefined) {
+ delete value[key];
+ } else {
+ value[key] = replacement;
+ }
+ }
+ }
+
+ return reviver.call(holder, name, value)
+ }
+
+ var lexState;
+ var buffer;
+ var doubleQuote;
+ var sign;
+ var c;
+
+ function lex () {
+ lexState = 'default';
+ buffer = '';
+ doubleQuote = false;
+ sign = 1;
+
+ for (;;) {
+ c = peek();
+
+ // This code is unreachable.
+ // if (!lexStates[lexState]) {
+ // throw invalidLexState(lexState)
+ // }
+
+ var token = lexStates[lexState]();
+ if (token) {
+ return token
+ }
+ }
+ }
+
+ function peek () {
+ if (source[pos]) {
+ return String.fromCodePoint(source.codePointAt(pos))
+ }
+ }
+
+ function read () {
+ var c = peek();
+
+ if (c === '\n') {
+ line++;
+ column = 0;
+ } else if (c) {
+ column += c.length;
+ } else {
+ column++;
+ }
+
+ if (c) {
+ pos += c.length;
+ }
+
+ return c
+ }
+
+ var lexStates = {
+ default: function default$1 () {
+ switch (c) {
+ case '\t':
+ case '\v':
+ case '\f':
+ case ' ':
+ case '\u00A0':
+ case '\uFEFF':
+ case '\n':
+ case '\r':
+ case '\u2028':
+ case '\u2029':
+ read();
+ return
+
+ case '/':
+ read();
+ lexState = 'comment';
+ return
+
+ case undefined:
+ read();
+ return newToken('eof')
+ }
+
+ if (util.isSpaceSeparator(c)) {
+ read();
+ return
+ }
+
+ // This code is unreachable.
+ // if (!lexStates[parseState]) {
+ // throw invalidLexState(parseState)
+ // }
+
+ return lexStates[parseState]()
+ },
+
+ comment: function comment () {
+ switch (c) {
+ case '*':
+ read();
+ lexState = 'multiLineComment';
+ return
+
+ case '/':
+ read();
+ lexState = 'singleLineComment';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ multiLineComment: function multiLineComment () {
+ switch (c) {
+ case '*':
+ read();
+ lexState = 'multiLineCommentAsterisk';
+ return
+
+ case undefined:
+ throw invalidChar(read())
+ }
+
+ read();
+ },
+
+ multiLineCommentAsterisk: function multiLineCommentAsterisk () {
+ switch (c) {
+ case '*':
+ read();
+ return
+
+ case '/':
+ read();
+ lexState = 'default';
+ return
+
+ case undefined:
+ throw invalidChar(read())
+ }
+
+ read();
+ lexState = 'multiLineComment';
+ },
+
+ singleLineComment: function singleLineComment () {
+ switch (c) {
+ case '\n':
+ case '\r':
+ case '\u2028':
+ case '\u2029':
+ read();
+ lexState = 'default';
+ return
+
+ case undefined:
+ read();
+ return newToken('eof')
+ }
+
+ read();
+ },
+
+ value: function value () {
+ switch (c) {
+ case '{':
+ case '[':
+ return newToken('punctuator', read())
+
+ case 'n':
+ read();
+ literal('ull');
+ return newToken('null', null)
+
+ case 't':
+ read();
+ literal('rue');
+ return newToken('boolean', true)
+
+ case 'f':
+ read();
+ literal('alse');
+ return newToken('boolean', false)
+
+ case '-':
+ case '+':
+ if (read() === '-') {
+ sign = -1;
+ }
+
+ lexState = 'sign';
+ return
+
+ case '.':
+ buffer = read();
+ lexState = 'decimalPointLeading';
+ return
+
+ case '0':
+ buffer = read();
+ lexState = 'zero';
+ return
+
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ buffer = read();
+ lexState = 'decimalInteger';
+ return
+
+ case 'I':
+ read();
+ literal('nfinity');
+ return newToken('numeric', Infinity)
+
+ case 'N':
+ read();
+ literal('aN');
+ return newToken('numeric', NaN)
+
+ case '"':
+ case "'":
+ doubleQuote = (read() === '"');
+ buffer = '';
+ lexState = 'string';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ identifierNameStartEscape: function identifierNameStartEscape () {
+ if (c !== 'u') {
+ throw invalidChar(read())
+ }
+
+ read();
+ var u = unicodeEscape();
+ switch (u) {
+ case '$':
+ case '_':
+ break
+
+ default:
+ if (!util.isIdStartChar(u)) {
+ throw invalidIdentifier()
+ }
+
+ break
+ }
+
+ buffer += u;
+ lexState = 'identifierName';
+ },
+
+ identifierName: function identifierName () {
+ switch (c) {
+ case '$':
+ case '_':
+ case '\u200C':
+ case '\u200D':
+ buffer += read();
+ return
+
+ case '\\':
+ read();
+ lexState = 'identifierNameEscape';
+ return
+ }
+
+ if (util.isIdContinueChar(c)) {
+ buffer += read();
+ return
+ }
+
+ return newToken('identifier', buffer)
+ },
+
+ identifierNameEscape: function identifierNameEscape () {
+ if (c !== 'u') {
+ throw invalidChar(read())
+ }
+
+ read();
+ var u = unicodeEscape();
+ switch (u) {
+ case '$':
+ case '_':
+ case '\u200C':
+ case '\u200D':
+ break
+
+ default:
+ if (!util.isIdContinueChar(u)) {
+ throw invalidIdentifier()
+ }
+
+ break
+ }
+
+ buffer += u;
+ lexState = 'identifierName';
+ },
+
+ sign: function sign$1 () {
+ switch (c) {
+ case '.':
+ buffer = read();
+ lexState = 'decimalPointLeading';
+ return
+
+ case '0':
+ buffer = read();
+ lexState = 'zero';
+ return
+
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ buffer = read();
+ lexState = 'decimalInteger';
+ return
+
+ case 'I':
+ read();
+ literal('nfinity');
+ return newToken('numeric', sign * Infinity)
+
+ case 'N':
+ read();
+ literal('aN');
+ return newToken('numeric', NaN)
+ }
+
+ throw invalidChar(read())
+ },
+
+ zero: function zero () {
+ switch (c) {
+ case '.':
+ buffer += read();
+ lexState = 'decimalPoint';
+ return
+
+ case 'e':
+ case 'E':
+ buffer += read();
+ lexState = 'decimalExponent';
+ return
+
+ case 'x':
+ case 'X':
+ buffer += read();
+ lexState = 'hexadecimal';
+ return
+ }
+
+ return newToken('numeric', sign * 0)
+ },
+
+ decimalInteger: function decimalInteger () {
+ switch (c) {
+ case '.':
+ buffer += read();
+ lexState = 'decimalPoint';
+ return
+
+ case 'e':
+ case 'E':
+ buffer += read();
+ lexState = 'decimalExponent';
+ return
+ }
+
+ if (util.isDigit(c)) {
+ buffer += read();
+ return
+ }
+
+ return newToken('numeric', sign * Number(buffer))
+ },
+
+ decimalPointLeading: function decimalPointLeading () {
+ if (util.isDigit(c)) {
+ buffer += read();
+ lexState = 'decimalFraction';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ decimalPoint: function decimalPoint () {
+ switch (c) {
+ case 'e':
+ case 'E':
+ buffer += read();
+ lexState = 'decimalExponent';
+ return
+ }
+
+ if (util.isDigit(c)) {
+ buffer += read();
+ lexState = 'decimalFraction';
+ return
+ }
+
+ return newToken('numeric', sign * Number(buffer))
+ },
+
+ decimalFraction: function decimalFraction () {
+ switch (c) {
+ case 'e':
+ case 'E':
+ buffer += read();
+ lexState = 'decimalExponent';
+ return
+ }
+
+ if (util.isDigit(c)) {
+ buffer += read();
+ return
+ }
+
+ return newToken('numeric', sign * Number(buffer))
+ },
+
+ decimalExponent: function decimalExponent () {
+ switch (c) {
+ case '+':
+ case '-':
+ buffer += read();
+ lexState = 'decimalExponentSign';
+ return
+ }
+
+ if (util.isDigit(c)) {
+ buffer += read();
+ lexState = 'decimalExponentInteger';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ decimalExponentSign: function decimalExponentSign () {
+ if (util.isDigit(c)) {
+ buffer += read();
+ lexState = 'decimalExponentInteger';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ decimalExponentInteger: function decimalExponentInteger () {
+ if (util.isDigit(c)) {
+ buffer += read();
+ return
+ }
+
+ return newToken('numeric', sign * Number(buffer))
+ },
+
+ hexadecimal: function hexadecimal () {
+ if (util.isHexDigit(c)) {
+ buffer += read();
+ lexState = 'hexadecimalInteger';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ hexadecimalInteger: function hexadecimalInteger () {
+ if (util.isHexDigit(c)) {
+ buffer += read();
+ return
+ }
+
+ return newToken('numeric', sign * Number(buffer))
+ },
+
+ string: function string () {
+ switch (c) {
+ case '\\':
+ read();
+ buffer += escape();
+ return
+
+ case '"':
+ if (doubleQuote) {
+ read();
+ return newToken('string', buffer)
+ }
+
+ buffer += read();
+ return
+
+ case "'":
+ if (!doubleQuote) {
+ read();
+ return newToken('string', buffer)
+ }
+
+ buffer += read();
+ return
+
+ case '\n':
+ case '\r':
+ throw invalidChar(read())
+
+ case '\u2028':
+ case '\u2029':
+ separatorChar(c);
+ break
+
+ case undefined:
+ throw invalidChar(read())
+ }
+
+ buffer += read();
+ },
+
+ start: function start () {
+ switch (c) {
+ case '{':
+ case '[':
+ return newToken('punctuator', read())
+
+ // This code is unreachable since the default lexState handles eof.
+ // case undefined:
+ // return newToken('eof')
+ }
+
+ lexState = 'value';
+ },
+
+ beforePropertyName: function beforePropertyName () {
+ switch (c) {
+ case '$':
+ case '_':
+ buffer = read();
+ lexState = 'identifierName';
+ return
+
+ case '\\':
+ read();
+ lexState = 'identifierNameStartEscape';
+ return
+
+ case '}':
+ return newToken('punctuator', read())
+
+ case '"':
+ case "'":
+ doubleQuote = (read() === '"');
+ lexState = 'string';
+ return
+ }
+
+ if (util.isIdStartChar(c)) {
+ buffer += read();
+ lexState = 'identifierName';
+ return
+ }
+
+ throw invalidChar(read())
+ },
+
+ afterPropertyName: function afterPropertyName () {
+ if (c === ':') {
+ return newToken('punctuator', read())
+ }
+
+ throw invalidChar(read())
+ },
+
+ beforePropertyValue: function beforePropertyValue () {
+ lexState = 'value';
+ },
+
+ afterPropertyValue: function afterPropertyValue () {
+ switch (c) {
+ case ',':
+ case '}':
+ return newToken('punctuator', read())
+ }
+
+ throw invalidChar(read())
+ },
+
+ beforeArrayValue: function beforeArrayValue () {
+ if (c === ']') {
+ return newToken('punctuator', read())
+ }
+
+ lexState = 'value';
+ },
+
+ afterArrayValue: function afterArrayValue () {
+ switch (c) {
+ case ',':
+ case ']':
+ return newToken('punctuator', read())
+ }
+
+ throw invalidChar(read())
+ },
+
+ end: function end () {
+ // This code is unreachable since it's handled by the default lexState.
+ // if (c === undefined) {
+ // read()
+ // return newToken('eof')
+ // }
+
+ throw invalidChar(read())
+ },
+ };
+
+ function newToken (type, value) {
+ return {
+ type: type,
+ value: value,
+ line: line,
+ column: column,
+ }
+ }
+
+ function literal (s) {
+ for (var i = 0, list = s; i < list.length; i += 1) {
+ var c = list[i];
+
+ var p = peek();
+
+ if (p !== c) {
+ throw invalidChar(read())
+ }
+
+ read();
+ }
+ }
+
+ function escape () {
+ var c = peek();
+ switch (c) {
+ case 'b':
+ read();
+ return '\b'
+
+ case 'f':
+ read();
+ return '\f'
+
+ case 'n':
+ read();
+ return '\n'
+
+ case 'r':
+ read();
+ return '\r'
+
+ case 't':
+ read();
+ return '\t'
+
+ case 'v':
+ read();
+ return '\v'
+
+ case '0':
+ read();
+ if (util.isDigit(peek())) {
+ throw invalidChar(read())
+ }
+
+ return '\0'
+
+ case 'x':
+ read();
+ return hexEscape()
+
+ case 'u':
+ read();
+ return unicodeEscape()
+
+ case '\n':
+ case '\u2028':
+ case '\u2029':
+ read();
+ return ''
+
+ case '\r':
+ read();
+ if (peek() === '\n') {
+ read();
+ }
+
+ return ''
+
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ throw invalidChar(read())
+
+ case undefined:
+ throw invalidChar(read())
+ }
+
+ return read()
+ }
+
+ function hexEscape () {
+ var buffer = '';
+ var c = peek();
+
+ if (!util.isHexDigit(c)) {
+ throw invalidChar(read())
+ }
+
+ buffer += read();
+
+ c = peek();
+ if (!util.isHexDigit(c)) {
+ throw invalidChar(read())
+ }
+
+ buffer += read();
+
+ return String.fromCodePoint(parseInt(buffer, 16))
+ }
+
+ function unicodeEscape () {
+ var buffer = '';
+ var count = 4;
+
+ while (count-- > 0) {
+ var c = peek();
+ if (!util.isHexDigit(c)) {
+ throw invalidChar(read())
+ }
+
+ buffer += read();
+ }
+
+ return String.fromCodePoint(parseInt(buffer, 16))
+ }
+
+ var parseStates = {
+ start: function start () {
+ if (token.type === 'eof') {
+ throw invalidEOF()
+ }
+
+ push();
+ },
+
+ beforePropertyName: function beforePropertyName () {
+ switch (token.type) {
+ case 'identifier':
+ case 'string':
+ key = token.value;
+ parseState = 'afterPropertyName';
+ return
+
+ case 'punctuator':
+ // This code is unreachable since it's handled by the lexState.
+ // if (token.value !== '}') {
+ // throw invalidToken()
+ // }
+
+ pop();
+ return
+
+ case 'eof':
+ throw invalidEOF()
+ }
+
+ // This code is unreachable since it's handled by the lexState.
+ // throw invalidToken()
+ },
+
+ afterPropertyName: function afterPropertyName () {
+ // This code is unreachable since it's handled by the lexState.
+ // if (token.type !== 'punctuator' || token.value !== ':') {
+ // throw invalidToken()
+ // }
+
+ if (token.type === 'eof') {
+ throw invalidEOF()
+ }
+
+ parseState = 'beforePropertyValue';
+ },
+
+ beforePropertyValue: function beforePropertyValue () {
+ if (token.type === 'eof') {
+ throw invalidEOF()
+ }
+
+ push();
+ },
+
+ beforeArrayValue: function beforeArrayValue () {
+ if (token.type === 'eof') {
+ throw invalidEOF()
+ }
+
+ if (token.type === 'punctuator' && token.value === ']') {
+ pop();
+ return
+ }
+
+ push();
+ },
+
+ afterPropertyValue: function afterPropertyValue () {
+ // This code is unreachable since it's handled by the lexState.
+ // if (token.type !== 'punctuator') {
+ // throw invalidToken()
+ // }
+
+ if (token.type === 'eof') {
+ throw invalidEOF()
+ }
+
+ switch (token.value) {
+ case ',':
+ parseState = 'beforePropertyName';
+ return
+
+ case '}':
+ pop();
+ }
+
+ // This code is unreachable since it's handled by the lexState.
+ // throw invalidToken()
+ },
+
+ afterArrayValue: function afterArrayValue () {
+ // This code is unreachable since it's handled by the lexState.
+ // if (token.type !== 'punctuator') {
+ // throw invalidToken()
+ // }
+
+ if (token.type === 'eof') {
+ throw invalidEOF()
+ }
+
+ switch (token.value) {
+ case ',':
+ parseState = 'beforeArrayValue';
+ return
+
+ case ']':
+ pop();
+ }
+
+ // This code is unreachable since it's handled by the lexState.
+ // throw invalidToken()
+ },
+
+ end: function end () {
+ // This code is unreachable since it's handled by the lexState.
+ // if (token.type !== 'eof') {
+ // throw invalidToken()
+ // }
+ },
+ };
+
+ function push () {
+ var value;
+
+ switch (token.type) {
+ case 'punctuator':
+ switch (token.value) {
+ case '{':
+ value = {};
+ break
+
+ case '[':
+ value = [];
+ break
+ }
+
+ break
+
+ case 'null':
+ case 'boolean':
+ case 'numeric':
+ case 'string':
+ value = token.value;
+ break
+
+ // This code is unreachable.
+ // default:
+ // throw invalidToken()
+ }
+
+ if (root === undefined) {
+ root = value;
+ } else {
+ var parent = stack[stack.length - 1];
+ if (Array.isArray(parent)) {
+ parent.push(value);
+ } else {
+ parent[key] = value;
+ }
+ }
+
+ if (value !== null && typeof value === 'object') {
+ stack.push(value);
+
+ if (Array.isArray(value)) {
+ parseState = 'beforeArrayValue';
+ } else {
+ parseState = 'beforePropertyName';
+ }
+ } else {
+ var current = stack[stack.length - 1];
+ if (current == null) {
+ parseState = 'end';
+ } else if (Array.isArray(current)) {
+ parseState = 'afterArrayValue';
+ } else {
+ parseState = 'afterPropertyValue';
+ }
+ }
+ }
+
+ function pop () {
+ stack.pop();
+
+ var current = stack[stack.length - 1];
+ if (current == null) {
+ parseState = 'end';
+ } else if (Array.isArray(current)) {
+ parseState = 'afterArrayValue';
+ } else {
+ parseState = 'afterPropertyValue';
+ }
+ }
+
+ // This code is unreachable.
+ // function invalidParseState () {
+ // return new Error(`JSON5: invalid parse state '${parseState}'`)
+ // }
+
+ // This code is unreachable.
+ // function invalidLexState (state) {
+ // return new Error(`JSON5: invalid lex state '${state}'`)
+ // }
+
+ function invalidChar (c) {
+ if (c === undefined) {
+ return syntaxError(("JSON5: invalid end of input at " + line + ":" + column))
+ }
+
+ return syntaxError(("JSON5: invalid character '" + (formatChar(c)) + "' at " + line + ":" + column))
+ }
+
+ function invalidEOF () {
+ return syntaxError(("JSON5: invalid end of input at " + line + ":" + column))
+ }
+
+ // This code is unreachable.
+ // function invalidToken () {
+ // if (token.type === 'eof') {
+ // return syntaxError(`JSON5: invalid end of input at ${line}:${column}`)
+ // }
+
+ // const c = String.fromCodePoint(token.value.codePointAt(0))
+ // return syntaxError(`JSON5: invalid character '${formatChar(c)}' at ${line}:${column}`)
+ // }
+
+ function invalidIdentifier () {
+ column -= 5;
+ return syntaxError(("JSON5: invalid identifier character at " + line + ":" + column))
+ }
+
+ function separatorChar (c) {
+ console.warn(("JSON5: '" + (formatChar(c)) + "' in strings is not valid ECMAScript; consider escaping"));
+ }
+
+ function formatChar (c) {
+ var replacements = {
+ "'": "\\'",
+ '"': '\\"',
+ '\\': '\\\\',
+ '\b': '\\b',
+ '\f': '\\f',
+ '\n': '\\n',
+ '\r': '\\r',
+ '\t': '\\t',
+ '\v': '\\v',
+ '\0': '\\0',
+ '\u2028': '\\u2028',
+ '\u2029': '\\u2029',
+ };
+
+ if (replacements[c]) {
+ return replacements[c]
+ }
+
+ if (c < ' ') {
+ var hexString = c.charCodeAt(0).toString(16);
+ return '\\x' + ('00' + hexString).substring(hexString.length)
+ }
+
+ return c
+ }
+
+ function syntaxError (message) {
+ var err = new SyntaxError(message);
+ err.lineNumber = line;
+ err.columnNumber = column;
+ return err
+ }
+
+ var stringify = function stringify (value, replacer, space) {
+ var stack = [];
+ var indent = '';
+ var propertyList;
+ var replacerFunc;
+ var gap = '';
+ var quote;
+
+ if (
+ replacer != null &&
+ typeof replacer === 'object' &&
+ !Array.isArray(replacer)
+ ) {
+ space = replacer.space;
+ quote = replacer.quote;
+ replacer = replacer.replacer;
+ }
+
+ if (typeof replacer === 'function') {
+ replacerFunc = replacer;
+ } else if (Array.isArray(replacer)) {
+ propertyList = [];
+ for (var i = 0, list = replacer; i < list.length; i += 1) {
+ var v = list[i];
+
+ var item = (void 0);
+
+ if (typeof v === 'string') {
+ item = v;
+ } else if (
+ typeof v === 'number' ||
+ v instanceof String ||
+ v instanceof Number
+ ) {
+ item = String(v);
+ }
+
+ if (item !== undefined && propertyList.indexOf(item) < 0) {
+ propertyList.push(item);
+ }
+ }
+ }
+
+ if (space instanceof Number) {
+ space = Number(space);
+ } else if (space instanceof String) {
+ space = String(space);
+ }
+
+ if (typeof space === 'number') {
+ if (space > 0) {
+ space = Math.min(10, Math.floor(space));
+ gap = ' '.substr(0, space);
+ }
+ } else if (typeof space === 'string') {
+ gap = space.substr(0, 10);
+ }
+
+ return serializeProperty('', {'': value})
+
+ function serializeProperty (key, holder) {
+ var value = holder[key];
+ if (value != null) {
+ if (typeof value.toJSON5 === 'function') {
+ value = value.toJSON5(key);
+ } else if (typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+ }
+
+ if (replacerFunc) {
+ value = replacerFunc.call(holder, key, value);
+ }
+
+ if (value instanceof Number) {
+ value = Number(value);
+ } else if (value instanceof String) {
+ value = String(value);
+ } else if (value instanceof Boolean) {
+ value = value.valueOf();
+ }
+
+ switch (value) {
+ case null: return 'null'
+ case true: return 'true'
+ case false: return 'false'
+ }
+
+ if (typeof value === 'string') {
+ return quoteString(value, false)
+ }
+
+ if (typeof value === 'number') {
+ return String(value)
+ }
+
+ if (typeof value === 'object') {
+ return Array.isArray(value) ? serializeArray(value) : serializeObject(value)
+ }
+
+ return undefined
+ }
+
+ function quoteString (value) {
+ var quotes = {
+ "'": 0.1,
+ '"': 0.2,
+ };
+
+ var replacements = {
+ "'": "\\'",
+ '"': '\\"',
+ '\\': '\\\\',
+ '\b': '\\b',
+ '\f': '\\f',
+ '\n': '\\n',
+ '\r': '\\r',
+ '\t': '\\t',
+ '\v': '\\v',
+ '\0': '\\0',
+ '\u2028': '\\u2028',
+ '\u2029': '\\u2029',
+ };
+
+ var product = '';
+
+ for (var i = 0; i < value.length; i++) {
+ var c = value[i];
+ switch (c) {
+ case "'":
+ case '"':
+ quotes[c]++;
+ product += c;
+ continue
+
+ case '\0':
+ if (util.isDigit(value[i + 1])) {
+ product += '\\x00';
+ continue
+ }
+ }
+
+ if (replacements[c]) {
+ product += replacements[c];
+ continue
+ }
+
+ if (c < ' ') {
+ var hexString = c.charCodeAt(0).toString(16);
+ product += '\\x' + ('00' + hexString).substring(hexString.length);
+ continue
+ }
+
+ product += c;
+ }
+
+ var quoteChar = quote || Object.keys(quotes).reduce(function (a, b) { return (quotes[a] < quotes[b]) ? a : b; });
+
+ product = product.replace(new RegExp(quoteChar, 'g'), replacements[quoteChar]);
+
+ return quoteChar + product + quoteChar
+ }
+
+ function serializeObject (value) {
+ if (stack.indexOf(value) >= 0) {
+ throw TypeError('Converting circular structure to JSON5')
+ }
+
+ stack.push(value);
+
+ var stepback = indent;
+ indent = indent + gap;
+
+ var keys = propertyList || Object.keys(value);
+ var partial = [];
+ for (var i = 0, list = keys; i < list.length; i += 1) {
+ var key = list[i];
+
+ var propertyString = serializeProperty(key, value);
+ if (propertyString !== undefined) {
+ var member = serializeKey(key) + ':';
+ if (gap !== '') {
+ member += ' ';
+ }
+ member += propertyString;
+ partial.push(member);
+ }
+ }
+
+ var final;
+ if (partial.length === 0) {
+ final = '{}';
+ } else {
+ var properties;
+ if (gap === '') {
+ properties = partial.join(',');
+ final = '{' + properties + '}';
+ } else {
+ var separator = ',\n' + indent;
+ properties = partial.join(separator);
+ final = '{\n' + indent + properties + ',\n' + stepback + '}';
+ }
+ }
+
+ stack.pop();
+ indent = stepback;
+ return final
+ }
+
+ function serializeKey (key) {
+ if (key.length === 0) {
+ return quoteString(key, true)
+ }
+
+ var firstChar = String.fromCodePoint(key.codePointAt(0));
+ if (!util.isIdStartChar(firstChar)) {
+ return quoteString(key, true)
+ }
+
+ for (var i = firstChar.length; i < key.length; i++) {
+ if (!util.isIdContinueChar(String.fromCodePoint(key.codePointAt(i)))) {
+ return quoteString(key, true)
+ }
+ }
+
+ return key
+ }
+
+ function serializeArray (value) {
+ if (stack.indexOf(value) >= 0) {
+ throw TypeError('Converting circular structure to JSON5')
+ }
+
+ stack.push(value);
+
+ var stepback = indent;
+ indent = indent + gap;
+
+ var partial = [];
+ for (var i = 0; i < value.length; i++) {
+ var propertyString = serializeProperty(String(i), value);
+ partial.push((propertyString !== undefined) ? propertyString : 'null');
+ }
+
+ var final;
+ if (partial.length === 0) {
+ final = '[]';
+ } else {
+ if (gap === '') {
+ var properties = partial.join(',');
+ final = '[' + properties + ']';
+ } else {
+ var separator = ',\n' + indent;
+ var properties$1 = partial.join(separator);
+ final = '[\n' + indent + properties$1 + ',\n' + stepback + ']';
+ }
+ }
+
+ stack.pop();
+ indent = stepback;
+ return final
+ }
+ };
+
+ var JSON5 = {
+ parse: parse,
+ stringify: stringify,
+ };
+
+ var lib = JSON5;
+
+ var es5 = lib;
+
+ return es5;
+
+})));
diff --git a/devtools/shared/storage/vendor/moz.build b/devtools/shared/storage/vendor/moz.build
new file mode 100644
index 0000000000..5e8a82199a
--- /dev/null
+++ b/devtools/shared/storage/vendor/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/.
+
+DIRS += [
+ 'stringvalidator',
+]
+
+DevToolsModules(
+ 'json5.js',
+) \ No newline at end of file
diff --git a/devtools/shared/storage/vendor/stringvalidator/UPDATING.md b/devtools/shared/storage/vendor/stringvalidator/UPDATING.md
new file mode 100644
index 0000000000..701715ed45
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/UPDATING.md
@@ -0,0 +1,142 @@
+# Updating this library
+
+1. Replace the contents of validator.js with the contents of https://github.com/chriso/validator.js/blob/master/validator.js.
+
+2. Add the following methods:
+ ```
+ // see http://isrc.ifpi.org/en/isrc-standard/code-syntax
+ var isrc = /^[A-Z]{2}[0-9A-Z]{3}\d{2}\d{5}$/;
+
+ function isISRC(str) {
+ assertString(str);
+ return isrc.test(str);
+ }
+ ```
+
+ ```
+ var cultureCodes = new Set(["ar", "bg", "ca", "zh-Hans", "cs", "da", "de",
+ "el", "en", "es", "fi", "fr", "he", "hu", "is", "it", "ja", "ko", "nl", "no",
+ "pl", "pt", "rm", "ro", "ru", "hr", "sk", "sq", "sv", "th", "tr", "ur", "id",
+ "uk", "be", "sl", "et", "lv", "lt", "tg", "fa", "vi", "hy", "az", "eu", "hsb",
+ "mk", "tn", "xh", "zu", "af", "ka", "fo", "hi", "mt", "se", "ga", "ms", "kk",
+ "ky", "sw", "tk", "uz", "tt", "bn", "pa", "gu", "or", "ta", "te", "kn", "ml",
+ "as", "mr", "sa", "mn", "bo", "cy", "km", "lo", "gl", "kok", "syr", "si", "iu",
+ "am", "tzm", "ne", "fy", "ps", "fil", "dv", "ha", "yo", "quz", "nso", "ba", "lb",
+ "kl", "ig", "ii", "arn", "moh", "br", "ug", "mi", "oc", "co", "gsw", "sah",
+ "qut", "rw", "wo", "prs", "gd", "ar-SA", "bg-BG", "ca-ES", "zh-TW", "cs-CZ",
+ "da-DK", "de-DE", "el-GR", "en-US", "fi-FI", "fr-FR", "he-IL", "hu-HU", "is-IS",
+ "it-IT", "ja-JP", "ko-KR", "nl-NL", "nb-NO", "pl-PL", "pt-BR", "rm-CH", "ro-RO",
+ "ru-RU", "hr-HR", "sk-SK", "sq-AL", "sv-SE", "th-TH", "tr-TR", "ur-PK", "id-ID",
+ "uk-UA", "be-BY", "sl-SI", "et-EE", "lv-LV", "lt-LT", "tg-Cyrl-TJ", "fa-IR",
+ "vi-VN", "hy-AM", "az-Latn-AZ", "eu-ES", "hsb-DE", "mk-MK", "tn-ZA", "xh-ZA",
+ "zu-ZA", "af-ZA", "ka-GE", "fo-FO", "hi-IN", "mt-MT", "se-NO", "ms-MY", "kk-KZ",
+ "ky-KG", "sw-KE", "tk-TM", "uz-Latn-UZ", "tt-RU", "bn-IN", "pa-IN", "gu-IN",
+ "or-IN", "ta-IN", "te-IN", "kn-IN", "ml-IN", "as-IN", "mr-IN", "sa-IN", "mn-MN",
+ "bo-CN", "cy-GB", "km-KH", "lo-LA", "gl-ES", "kok-IN", "syr-SY", "si-LK",
+ "iu-Cans-CA", "am-ET", "ne-NP", "fy-NL", "ps-AF", "fil-PH", "dv-MV",
+ "ha-Latn-NG", "yo-NG", "quz-BO", "nso-ZA", "ba-RU", "lb-LU", "kl-GL", "ig-NG",
+ "ii-CN", "arn-CL", "moh-CA", "br-FR", "ug-CN", "mi-NZ", "oc-FR", "co-FR",
+ "gsw-FR", "sah-RU", "qut-GT", "rw-RW", "wo-SN", "prs-AF", "gd-GB", "ar-IQ",
+ "zh-CN", "de-CH", "en-GB", "es-MX", "fr-BE", "it-CH", "nl-BE", "nn-NO", "pt-PT",
+ "sr-Latn-CS", "sv-FI", "az-Cyrl-AZ", "dsb-DE", "se-SE", "ga-IE", "ms-BN",
+ "uz-Cyrl-UZ", "bn-BD", "mn-Mong-CN", "iu-Latn-CA", "tzm-Latn-DZ", "quz-EC",
+ "ar-EG", "zh-HK", "de-AT", "en-AU", "es-ES", "fr-CA", "sr-Cyrl-CS", "se-FI",
+ "quz-PE", "ar-LY", "zh-SG", "de-LU", "en-CA", "es-GT", "fr-CH", "hr-BA",
+ "smj-NO", "ar-DZ", "zh-MO", "de-LI", "en-NZ", "es-CR", "fr-LU", "bs-Latn-BA",
+ "smj-SE", "ar-MA", "en-IE", "es-PA", "fr-MC", "sr-Latn-BA", "sma-NO", "ar-TN",
+ "en-ZA", "es-DO", "sr-Cyrl-BA", "sma-SE", "ar-OM", "en-JM", "es-VE",
+ "bs-Cyrl-BA", "sms-FI", "ar-YE", "en-029", "es-CO", "sr-Latn-RS", "smn-FI",
+ "ar-SY", "en-BZ", "es-PE", "sr-Cyrl-RS", "ar-JO", "en-TT", "es-AR", "sr-Latn-ME",
+ "ar-LB", "en-ZW", "es-EC", "sr-Cyrl-ME", "ar-KW", "en-PH", "es-CL", "ar-AE",
+ "es-UY", "ar-BH", "es-PY", "ar-QA", "en-IN", "es-BO", "en-MY", "es-SV", "en-SG",
+ "es-HN", "es-NI", "es-PR", "es-US", "bs-Cyrl", "bs-Latn", "sr-Cyrl", "sr-Latn",
+ "smn", "az-Cyrl", "sms", "zh", "nn", "bs", "az-Latn", "sma", "uz-Cyrl",
+ "mn-Cyrl", "iu-Cans", "zh-Hant", "nb", "sr", "tg-Cyrl", "dsb", "smj", "uz-Latn",
+ "mn-Mong", "iu-Latn", "tzm-Latn", "ha-Latn", "zh-CHS", "zh-CHT"]);
+
+ function isRFC5646(str) {
+ assertString(str);
+ // According to the spec these codes are case sensitive so we can check the
+ // string directly.
+ return cultureCodes.has(str);
+ }
+ ```
+
+ ```
+ var semver = /^v?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/i
+
+ function isSemVer(str) {
+ assertString(str);
+ return semver.test(str);
+ }
+ ```
+
+ ```
+ var rgbcolor = /^rgb?\(\s*(0|[1-9]\d?|1\d\d?|2[0-4]\d|25[0-5])\s*,\s*(0|[1-9]\d?|1\d\d?|2[0-4]\d|25[0-5])\s*,\s*(0|[1-9]\d?|1\d\d?|2[0-4]\d|25[0-5])\s*\)$/i
+
+ function isRGBColor(str) {
+ assertString(str);
+ return rgbcolor.test(str);
+ }
+ ```
+
+3. Add the following to the validator object towards the end of the file:
+ ```
+ isISRC: isISRC,
+ isRFC5646: isRFC5646,
+ isSemVer: isSemVer,
+ isRGBColor: isRGBColor,
+ ```
+
+4. Look for the phones array just above the isMobilePhone() method.
+
+ 1. Replace the en-HK regex with:
+ ```
+ // According to http://www.ofca.gov.hk/filemanager/ofca/en/content_311/no_plan.pdf
+ 'en-HK': /^(\+?852-?)?((4(04[01]|06\d|09[3-9]|20\d|2[2-9]\d|3[3-9]\d|[467]\d{2}|5[1-9]\d|81\d|82[1-9]|8[69]\d|92[3-9]|95[2-9]|98\d)|5([1-79]\d{2})|6(0[1-9]\d|[1-9]\d{2})|7(0[1-9]\d|10[4-79]|11[458]|1[24578]\d|13[24-9]|16[0-8]|19[24579]|21[02-79]|2[456]\d|27[13-6]|3[456]\d|37[4578]|39[0146])|8(1[58]\d|2[45]\d|267|27[5-9]|2[89]\d|3[15-9]\d|32[5-8]|[46-9]\d{2}|5[013-9]\d)|9(0[1-9]\d|1[02-9]\d|[2-8]\d{2}))-?\d{4}|7130-?[0124-8]\d{3}|8167-?2\d{3})$/,
+ ```
+ 2. Add:
+ ```
+ 'ko-KR': /^((\+?82)[ \-]?)?0?1([0|1|6|7|8|9]{1})[ \-]?\d{3,4}[ \-]?\d{4}$/,
+ 'lt-LT': /^(\+370|8)\d{8}$/,
+ ```
+
+5. Replace the isMobilePhone() method with:
+ ```
+ function isMobilePhone(str, locale) {
+ assertString(str);
+ if (locale in phones) {
+ return phones[locale].test(str);
+ } else if (locale === 'any') {
+ return !!Object.values(phones).find(phone => phone.test(str));
+ }
+ return false;
+ }
+ ```
+
+6. Delete the notBase64 regex and replace the isBase64 with:
+ ```
+ function isBase64(str) {
+ assertString(str);
+ // Value length must be divisible by 4.
+ var len = str.length;
+ if (!len || len % 4 !== 0) {
+ return false;
+ }
+
+ try {
+ if (atob(str)) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+ ```
+
+7. Do not replace the test files as they have been converted to xpcshell tests. If there are new methods then add their tests to the `test_sanitizers.js` or `test_validators.js` files as appropriate.
+
+8. To test the library please run the following:
+ ```
+ ./mach xpcshell-test devtools/client/shared/vendor/stringvalidator/
+ ```
diff --git a/devtools/shared/storage/vendor/stringvalidator/moz.build b/devtools/shared/storage/vendor/stringvalidator/moz.build
new file mode 100644
index 0000000000..416242f722
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/moz.build
@@ -0,0 +1,15 @@
+# -*- 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 += [
+ 'util'
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.toml']
+
+DevToolsModules(
+ 'validator.js',
+)
diff --git a/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/head_stringvalidator.js b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/head_stringvalidator.js
new file mode 100644
index 0000000000..f1e25fe2ec
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/head_stringvalidator.js
@@ -0,0 +1,15 @@
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+
+this.validator = require("resource://devtools/shared/storage/vendor/stringvalidator/validator.js");
+
+function describe(suite, testFunc) {
+ info(`\n Test suite: ${suite}`.toUpperCase());
+ testFunc();
+}
+
+function it(description, testFunc) {
+ info(`\n - ${description}:\n`.toUpperCase());
+ testFunc();
+}
diff --git a/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_sanitizers.js b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_sanitizers.js
new file mode 100644
index 0000000000..aa56d54c00
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_sanitizers.js
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+
+ "use strict";
+
+function test(options) {
+ var args = options.args || [];
+
+ args.unshift(null);
+
+ Object.keys(options.expect).forEach(function (input) {
+ args[0] = input;
+
+ var result = validator[options.sanitizer](...args);
+ var expected = options.expect[input];
+ let argsString = args.join(', ');
+
+ if (isNaN(result) && !result.length && isNaN(expected)) {
+ ok(true, `validator.${options.sanitizer}(${argsString}) returned "${result}"`);
+ } else {
+ equal(result, expected, `validator.${options.sanitizer}("${argsString}") ` +
+ `returned "${result}"`);
+ }
+ });
+}
+
+function run_test() {
+ describe('Sanitizers', function () {
+ it('should sanitize boolean strings', function () {
+ test({
+ sanitizer: 'toBoolean',
+ expect: {
+ '0': false,
+ '': false,
+ '1': true,
+ 'true': true,
+ 'foobar': true,
+ ' ': true,
+ },
+ });
+ test({
+ sanitizer: 'toBoolean',
+ args: [true], // strict
+ expect: {
+ '0': false,
+ '': false,
+ '1': true,
+ 'true': true,
+ 'foobar': false,
+ ' ': false,
+ },
+ });
+ });
+
+ it('should trim whitespace', function () {
+ test({
+ sanitizer: 'trim',
+ expect: {
+ ' \r\n\tfoo \r\n\t ': 'foo',
+ ' \r': '',
+ },
+ });
+
+ test({
+ sanitizer: 'ltrim',
+ expect: {
+ ' \r\n\tfoo \r\n\t ': 'foo \r\n\t ',
+ ' \t \n': '',
+ },
+ });
+
+ test({
+ sanitizer: 'rtrim',
+ expect: {
+ ' \r\n\tfoo \r\n\t ': ' \r\n\tfoo',
+ ' \r\n \t': '',
+ },
+ });
+ });
+
+ it('should trim custom characters', function () {
+ test({
+ sanitizer: 'trim',
+ args: ['01'],
+ expect: { '010100201000': '2' },
+ });
+
+ test({
+ sanitizer: 'ltrim',
+ args: ['01'],
+ expect: { '010100201000': '201000' },
+ });
+
+ test({
+ sanitizer: 'rtrim',
+ args: ['01'],
+ expect: { '010100201000': '0101002' },
+ });
+ });
+
+ it('should convert strings to integers', function () {
+ test({
+ sanitizer: 'toInt',
+ expect: {
+ '3': 3,
+ ' 3 ': 3,
+ '2.4': 2,
+ 'foo': NaN,
+ },
+ });
+
+ test({
+ sanitizer: 'toInt',
+ args: [16],
+ expect: { 'ff': 255 },
+ });
+ });
+
+ it('should convert strings to floats', function () {
+ test({
+ sanitizer: 'toFloat',
+ expect: {
+ '2': 2.0,
+ '2.': 2.0,
+ '-2.5': -2.5,
+ '.5': 0.5,
+ 'foo': NaN,
+ },
+ });
+ });
+
+ it('should escape HTML', function () {
+ test({
+ sanitizer: 'escape',
+ expect: {
+ '<script> alert("xss&fun"); </script>':
+ '&lt;script&gt; alert(&quot;xss&amp;fun&quot;); &lt;&#x2F;script&gt;',
+
+ "<script> alert('xss&fun'); </script>":
+ '&lt;script&gt; alert(&#x27;xss&amp;fun&#x27;); &lt;&#x2F;script&gt;',
+
+ 'Backtick: `':
+ 'Backtick: &#96;',
+
+ 'Backslash: \\':
+ 'Backslash: &#x5C;',
+ },
+ });
+ });
+
+ it('should unescape HTML', function () {
+ test({
+ sanitizer: 'unescape',
+ expect: {
+ '&lt;script&gt; alert(&quot;xss&amp;fun&quot;); &lt;&#x2F;script&gt;':
+ '<script> alert("xss&fun"); </script>',
+
+ '&lt;script&gt; alert(&#x27;xss&amp;fun&#x27;); &lt;&#x2F;script&gt;':
+ "<script> alert('xss&fun'); </script>",
+
+ 'Backtick: &#96;':
+ 'Backtick: `',
+ },
+ });
+ });
+
+ it('should remove control characters (<32 and 127)', function () {
+ // Check basic functionality
+ test({
+ sanitizer: 'stripLow',
+ expect: {
+ 'foo\x00': 'foo',
+ '\x7Ffoo\x02': 'foo',
+ '\x01\x09': '',
+ 'foo\x0A\x0D': 'foo',
+ },
+ });
+ // Unicode safety
+ test({
+ sanitizer: 'stripLow',
+ expect: {
+ 'perch\u00e9': 'perch\u00e9',
+ '\u20ac': '\u20ac',
+ '\u2206\x0A': '\u2206',
+ '\ud83d\ude04': '\ud83d\ude04',
+ },
+ });
+ // Preserve newlines
+ test({
+ sanitizer: 'stripLow',
+ args: [true], // keep_new_lines
+ expect: {
+ 'foo\x0A\x0D': 'foo\x0A\x0D',
+ '\x03foo\x0A\x0D': 'foo\x0A\x0D',
+ },
+ });
+ });
+
+ it('should sanitize a string based on a whitelist', function () {
+ test({
+ sanitizer: 'whitelist',
+ args: ['abc'],
+ expect: {
+ 'abcdef': 'abc',
+ 'aaaaaaaaaabbbbbbbbbb': 'aaaaaaaaaabbbbbbbbbb',
+ 'a1b2c3': 'abc',
+ ' ': '',
+ },
+ });
+ });
+
+ it('should sanitize a string based on a blacklist', function () {
+ test({
+ sanitizer: 'blacklist',
+ args: ['abc'],
+ expect: {
+ 'abcdef': 'def',
+ 'aaaaaaaaaabbbbbbbbbb': '',
+ 'a1b2c3': '123',
+ ' ': ' ',
+ },
+ });
+ });
+
+ it('should normalize an email based on domain', function () {
+ test({
+ sanitizer: 'normalizeEmail',
+ expect: {
+ 'test@me.com': 'test@me.com',
+ 'some.name@gmail.com': 'somename@gmail.com',
+ 'some.name@googleMail.com': 'somename@gmail.com',
+ 'some.name+extension@gmail.com': 'somename@gmail.com',
+ 'some.Name+extension@GoogleMail.com': 'somename@gmail.com',
+ 'some.name.middleName+extension@gmail.com': 'somenamemiddlename@gmail.com',
+ 'some.name.middleName+extension@GoogleMail.com': 'somenamemiddlename@gmail.com',
+ 'some.name.midd.leNa.me.+extension@gmail.com': 'somenamemiddlename@gmail.com',
+ 'some.name.midd.leNa.me.+extension@GoogleMail.com': 'somenamemiddlename@gmail.com',
+ 'some.name+extension@unknown.com': 'some.name+extension@unknown.com',
+ 'hans@m端ller.com': 'hans@m端ller.com',
+ 'an invalid email address': false,
+ '': false,
+ '+extension@gmail.com': false,
+ '...@gmail.com': false,
+ '.+extension@googlemail.com': false,
+ '+a@icloud.com': false,
+ '+a@outlook.com': false,
+ '-a@yahoo.com': false,
+ 'some.name.midd..leNa...me...+extension@GoogleMail.com': 'somenamemiddlename@gmail.com',
+ '"foo@bar"@baz.com': '"foo@bar"@baz.com',
+ },
+ });
+
+ // Testing all_lowercase switch, should apply to domains not known to be case-insensitive
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{ all_lowercase: false }],
+ expect: {
+ 'test@foo.com': 'test@foo.com',
+ 'hans@m端ller.com': 'hans@m端ller.com',
+ 'test@FOO.COM': 'test@foo.com', // Hostname is always lowercased
+ 'blAH@x.com': 'blAH@x.com',
+ // In case of domains that are known to be case-insensitive, there's a separate switch
+ 'TEST@me.com': 'test@me.com',
+ 'TEST@ME.COM': 'test@me.com',
+ 'SOME.name@GMAIL.com': 'somename@gmail.com',
+ 'SOME.name.middleName+extension@GoogleMail.com': 'somenamemiddlename@gmail.com',
+ 'SOME.name.midd.leNa.me.+extension@gmail.com': 'somenamemiddlename@gmail.com',
+ 'SOME.name@gmail.com': 'somename@gmail.com',
+ 'SOME.name@yahoo.ca': 'some.name@yahoo.ca',
+ 'SOME.name@outlook.ie': 'some.name@outlook.ie',
+ 'SOME.name@me.com': 'some.name@me.com',
+ },
+ });
+
+ // Testing *_lowercase
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ all_lowercase: false,
+ gmail_lowercase: false,
+ icloud_lowercase: false,
+ outlookdotcom_lowercase: false,
+ yahoo_lowercase: false,
+ }],
+ expect: {
+ 'TEST@FOO.COM': 'TEST@foo.com', // all_lowercase
+ 'ME@gMAil.com': 'ME@gmail.com', // gmail_lowercase
+ 'ME@me.COM': 'ME@me.com', // icloud_lowercase
+ 'ME@icloud.COM': 'ME@icloud.com', // icloud_lowercase
+ 'ME@outlook.COM': 'ME@outlook.com', // outlookdotcom_lowercase
+ 'JOHN@live.CA': 'JOHN@live.ca', // outlookdotcom_lowercase
+ 'ME@ymail.COM': 'ME@ymail.com', // yahoo_lowercase
+ },
+ });
+
+ // Testing all_lowercase
+ // Should overwrite all the *_lowercase options
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ all_lowercase: true,
+ gmail_lowercase: false, // Overruled
+ icloud_lowercase: false, // Overruled
+ outlookdotcom_lowercase: false, // Overruled
+ yahoo_lowercase: false, // Overruled
+ }],
+ expect: {
+ 'TEST@FOO.COM': 'test@foo.com', // all_lowercase
+ 'ME@gMAil.com': 'me@gmail.com', // gmail_lowercase
+ 'ME@me.COM': 'me@me.com', // icloud_lowercase
+ 'ME@icloud.COM': 'me@icloud.com', // icloud_lowercase
+ 'ME@outlook.COM': 'me@outlook.com', // outlookdotcom_lowercase
+ 'JOHN@live.CA': 'john@live.ca', // outlookdotcom_lowercase
+ 'ME@ymail.COM': 'me@ymail.com', // yahoo_lowercase
+ },
+ });
+
+ // Testing *_remove_dots
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ gmail_remove_dots: false,
+ }],
+ expect: {
+ 'SOME.name@GMAIL.com': 'some.name@gmail.com',
+ 'SOME.name+me@GMAIL.com': 'some.name@gmail.com',
+ 'my.self@foo.com': 'my.self@foo.com',
+ },
+ });
+
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ gmail_remove_dots: true,
+ }],
+ expect: {
+ 'SOME.name@GMAIL.com': 'somename@gmail.com',
+ 'SOME.name+me@GMAIL.com': 'somename@gmail.com',
+ 'my.self@foo.com': 'my.self@foo.com',
+ },
+ });
+
+ // Testing *_remove_subaddress
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ gmail_remove_subaddress: false,
+ icloud_remove_subaddress: false,
+ outlookdotcom_remove_subaddress: false,
+ yahoo_remove_subaddress: false, // Note Yahoo uses "-"
+ }],
+ expect: {
+ 'foo+bar@unknown.com': 'foo+bar@unknown.com',
+ 'foo+bar@gmail.com': 'foo+bar@gmail.com', // gmail_remove_subaddress
+ 'foo+bar@me.com': 'foo+bar@me.com', // icloud_remove_subaddress
+ 'foo+bar@icloud.com': 'foo+bar@icloud.com', // icloud_remove_subaddress
+ 'foo+bar@live.fr': 'foo+bar@live.fr', // outlookdotcom_remove_subaddress
+ 'foo+bar@hotmail.co.uk': 'foo+bar@hotmail.co.uk', // outlookdotcom_remove_subaddress
+ 'foo-bar@yahoo.com': 'foo-bar@yahoo.com', // yahoo_remove_subaddress
+ 'foo+bar@yahoo.com': 'foo+bar@yahoo.com', // yahoo_remove_subaddress
+ },
+ });
+
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ gmail_remove_subaddress: true,
+ icloud_remove_subaddress: true,
+ outlookdotcom_remove_subaddress: true,
+ yahoo_remove_subaddress: true, // Note Yahoo uses "-"
+ }],
+ expect: {
+ 'foo+bar@unknown.com': 'foo+bar@unknown.com',
+ 'foo+bar@gmail.com': 'foo@gmail.com', // gmail_remove_subaddress
+ 'foo+bar@me.com': 'foo@me.com', // icloud_remove_subaddress
+ 'foo+bar@icloud.com': 'foo@icloud.com', // icloud_remove_subaddress
+ 'foo+bar@live.fr': 'foo@live.fr', // outlookdotcom_remove_subaddress
+ 'foo+bar@hotmail.co.uk': 'foo@hotmail.co.uk', // outlookdotcom_remove_subaddress
+ 'foo-bar@yahoo.com': 'foo@yahoo.com', // yahoo_remove_subaddress
+ 'foo+bar@yahoo.com': 'foo+bar@yahoo.com', // yahoo_remove_subaddress
+ },
+ });
+
+ // Testing gmail_convert_googlemaildotcom
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ gmail_convert_googlemaildotcom: false,
+ }],
+ expect: {
+ 'SOME.name@GMAIL.com': 'somename@gmail.com',
+ 'SOME.name+me@GMAIL.com': 'somename@gmail.com',
+ 'SOME.name+me@googlemail.com': 'somename@googlemail.com',
+ 'SOME.name+me@googlemail.COM': 'somename@googlemail.com',
+ 'SOME.name+me@googlEmail.com': 'somename@googlemail.com',
+ 'my.self@foo.com': 'my.self@foo.com',
+ },
+ });
+
+ test({
+ sanitizer: 'normalizeEmail',
+ args: [{
+ gmail_convert_googlemaildotcom: true,
+ }],
+ expect: {
+ 'SOME.name@GMAIL.com': 'somename@gmail.com',
+ 'SOME.name+me@GMAIL.com': 'somename@gmail.com',
+ 'SOME.name+me@googlemail.com': 'somename@gmail.com',
+ 'SOME.name+me@googlemail.COM': 'somename@gmail.com',
+ 'SOME.name+me@googlEmail.com': 'somename@gmail.com',
+ 'my.self@foo.com': 'my.self@foo.com',
+ },
+ });
+ });
+ });
+}
diff --git a/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_validators.js b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_validators.js
new file mode 100644
index 0000000000..eaf86bc7dd
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/test_validators.js
@@ -0,0 +1,3762 @@
+/*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+
+ "use strict";
+
+var assert = require("resource://devtools/shared/storage/vendor/stringvalidator/util/assert.js").assert;
+
+function test(options) {
+ var args = options.args || [];
+ args.unshift(null);
+ if (options.valid) {
+ options.valid.forEach(function (valid) {
+ args[0] = valid;
+
+ let argsString = args.join(', ');
+ ok(validator[options.validator](...args), `validator.${options.validator}(${argsString}) == true`);
+ });
+ }
+ if (options.invalid) {
+ options.invalid.forEach(function (invalid) {
+ args[0] = invalid;
+
+ let argsString = args.join(', ');
+ ok(!validator[options.validator](...args), `validator.${options.validator}(${argsString}) == false`);
+ });
+ }
+}
+
+function repeat(str, count) {
+ var result = '';
+ while (count--) {
+ result += str;
+ }
+ return result;
+}
+
+function random4digit() {
+ return Math.floor(1000 + (Math.random() * 9000));
+}
+
+function run_test() {
+ describe('Validators', function () {
+ it('should validate email addresses', function () {
+ test({
+ validator: 'isEmail',
+ valid: [
+ 'foo@bar.com',
+ 'x@x.au',
+ 'foo@bar.com.au',
+ 'foo+bar@bar.com',
+ 'hans.m端ller@test.com',
+ 'hans@m端ller.com',
+ 'test|123@m端ller.com',
+ 'test+ext@gmail.com',
+ 'some.name.midd.leNa.me.+extension@GoogleMail.com',
+ 'gmail...ignores...dots...@gmail.com',
+ '"foobar"@example.com',
+ '" foo m端ller "@example.com',
+ '"foo\\@bar"@example.com',
+ `${repeat('a', 64)}@${repeat('a', 252)}.com`,
+ ],
+ invalid: [
+ 'invalidemail@',
+ 'invalid.com',
+ '@invalid.com',
+ 'foo@bar.com.',
+ 'somename@gmail.com',
+ 'foo@bar.co.uk.',
+ 'z@co.c',
+ 'gmailgmailgmailgmailgmail@gmail.com',
+ `${repeat('a', 64)}@${repeat('a', 253)}.com`,
+ `${repeat('a', 65)}@${repeat('a', 252)}.com`,
+ ],
+ });
+ });
+
+ it('should validate email addresses without UTF8 characters in local part', function () {
+ test({
+ validator: 'isEmail',
+ args: [{ allow_utf8_local_part: false }],
+ valid: [
+ 'foo@bar.com',
+ 'x@x.au',
+ 'foo@bar.com.au',
+ 'foo+bar@bar.com',
+ 'hans@m端ller.com',
+ 'test|123@m端ller.com',
+ 'test+ext@gmail.com',
+ 'some.name.midd.leNa.me.+extension@GoogleMail.com',
+ '"foobar"@example.com',
+ '"foo\\@bar"@example.com',
+ '" foo bar "@example.com',
+ ],
+ invalid: [
+ 'invalidemail@',
+ 'invalid.com',
+ '@invalid.com',
+ 'foo@bar.com.',
+ 'foo@bar.co.uk.',
+ 'somename@gmail.com',
+ 'hans.m端ller@test.com',
+ 'z@co.c',
+ ],
+ });
+ });
+
+ it('should validate email addresses with display names', function () {
+ test({
+ validator: 'isEmail',
+ args: [{ allow_display_name: true }],
+ valid: [
+ 'foo@bar.com',
+ 'x@x.au',
+ 'foo@bar.com.au',
+ 'foo+bar@bar.com',
+ 'hans.m端ller@test.com',
+ 'hans@m端ller.com',
+ 'test|123@m端ller.com',
+ 'test+ext@gmail.com',
+ 'some.name.midd.leNa.me.+extension@GoogleMail.com',
+ 'Some Name <foo@bar.com>',
+ 'Some Name <x@x.au>',
+ 'Some Name <foo@bar.com.au>',
+ 'Some Name <foo+bar@bar.com>',
+ 'Some Name <hans.m端ller@test.com>',
+ 'Some Name <hans@m端ller.com>',
+ 'Some Name <test|123@m端ller.com>',
+ 'Some Name <test+ext@gmail.com>',
+ 'Some Name <some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ 'Some Middle Name <some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ 'Name <some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ 'Name<some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ ],
+ invalid: [
+ 'invalidemail@',
+ 'invalid.com',
+ '@invalid.com',
+ 'foo@bar.com.',
+ 'foo@bar.co.uk.',
+ 'Some Name <invalidemail@>',
+ 'Some Name <invalid.com>',
+ 'Some Name <@invalid.com>',
+ 'Some Name <foo@bar.com.>',
+ 'Some Name <foo@bar.co.uk.>',
+ 'Some Name foo@bar.co.uk.>',
+ 'Some Name <foo@bar.co.uk.',
+ 'Some Name < foo@bar.co.uk >',
+ 'Name foo@bar.co.uk',
+ ],
+ });
+ });
+
+ it('should validate email addresses with required display names', function () {
+ test({
+ validator: 'isEmail',
+ args: [{ require_display_name: true }],
+ valid: [
+ 'Some Name <foo@bar.com>',
+ 'Some Name <x@x.au>',
+ 'Some Name <foo@bar.com.au>',
+ 'Some Name <foo+bar@bar.com>',
+ 'Some Name <hans.m端ller@test.com>',
+ 'Some Name <hans@m端ller.com>',
+ 'Some Name <test|123@m端ller.com>',
+ 'Some Name <test+ext@gmail.com>',
+ 'Some Name <some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ 'Some Middle Name <some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ 'Name <some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ 'Name<some.name.midd.leNa.me.+extension@GoogleMail.com>',
+ ],
+ invalid: [
+ 'some.name.midd.leNa.me.+extension@GoogleMail.com',
+ 'foo@bar.com',
+ 'x@x.au',
+ 'foo@bar.com.au',
+ 'foo+bar@bar.com',
+ 'hans.m端ller@test.com',
+ 'hans@m端ller.com',
+ 'test|123@m端ller.com',
+ 'test+ext@gmail.com',
+ 'invalidemail@',
+ 'invalid.com',
+ '@invalid.com',
+ 'foo@bar.com.',
+ 'foo@bar.co.uk.',
+ 'Some Name <invalidemail@>',
+ 'Some Name <invalid.com>',
+ 'Some Name <@invalid.com>',
+ 'Some Name <foo@bar.com.>',
+ 'Some Name <foo@bar.co.uk.>',
+ 'Some Name foo@bar.co.uk.>',
+ 'Some Name <foo@bar.co.uk.',
+ 'Some Name < foo@bar.co.uk >',
+ 'Name foo@bar.co.uk',
+ ],
+ });
+ });
+
+
+ it('should validate URLs', function () {
+ test({
+ validator: 'isURL',
+ valid: [
+ 'foobar.com',
+ 'www.foobar.com',
+ 'foobar.com/',
+ 'valid.au',
+ 'http://www.foobar.com/',
+ 'http://www.foobar.com:23/',
+ 'http://www.foobar.com:65535/',
+ 'http://www.foobar.com:5/',
+ 'https://www.foobar.com/',
+ 'ftp://www.foobar.com/',
+ 'http://www.foobar.com/~foobar',
+ 'http://user:pass@www.foobar.com/',
+ 'http://user:@www.foobar.com/',
+ 'http://127.0.0.1/',
+ 'http://10.0.0.0/',
+ 'http://189.123.14.13/',
+ 'http://duckduckgo.com/?q=%2F',
+ 'http://foobar.com/t$-_.+!*\'(),',
+ 'http://localhost:3000/',
+ 'http://foobar.com/?foo=bar#baz=qux',
+ 'http://foobar.com?foo=bar',
+ 'http://foobar.com#baz=qux',
+ 'http://www.xn--froschgrn-x9a.net/',
+ 'http://xn--froschgrn-x9a.com/',
+ 'http://foo--bar.com',
+ 'http://høyfjellet.no',
+ 'http://xn--j1aac5a4g.xn--j1amh',
+ 'http://xn------eddceddeftq7bvv7c4ke4c.xn--p1ai',
+ 'http://кулік.укр',
+ 'test.com?ref=http://test2.com',
+ 'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html',
+ 'http://[1080:0:0:0:8:800:200C:417A]/index.html',
+ 'http://[3ffe:2a00:100:7031::1]',
+ 'http://[1080::8:800:200C:417A]/foo',
+ 'http://[::192.9.5.5]/ipng',
+ 'http://[::FFFF:129.144.52.38]:80/index.html',
+ 'http://[2010:836B:4179::836B:4179]',
+ ],
+ invalid: [
+ 'xyz://foobar.com',
+ 'invalid/',
+ 'invalid.x',
+ 'invalid.',
+ '.com',
+ 'http://com/',
+ 'http://',
+ 'http://300.0.0.1/',
+ 'mailto:foo@bar.com',
+ 'rtmp://foobar.com',
+ 'http://www.xn--.com/',
+ 'http://xn--.com/',
+ 'http://www.foobar.com:0/',
+ 'http://www.foobar.com:70000/',
+ 'http://www.foobar.com:99999/',
+ 'http://www.-foobar.com/',
+ 'http://www.foobar-.com/',
+ 'http://foobar/# lol',
+ 'http://foobar/? lol',
+ 'http://foobar/ lol/',
+ 'http://lol @foobar.com/',
+ 'http://lol:lol @foobar.com/',
+ 'http://lol:lol:lol@foobar.com/',
+ 'http://lol: @foobar.com/',
+ 'http://www.foo_bar.com/',
+ 'http://www.foobar.com/\t',
+ 'http://\n@www.foobar.com/',
+ '',
+ `http://foobar.com/${new Array(2083).join('f')}`,
+ 'http://*.foo.com',
+ '*.foo.com',
+ '!.foo.com',
+ 'http://example.com.',
+ 'http://localhost:61500this is an invalid url!!!!',
+ '////foobar.com',
+ 'http:////foobar.com',
+ 'https://',
+ 'https://example.com/foo/<script>alert(\'XSS\')</script>/',
+ ],
+ });
+ });
+
+ it('should validate URLs with custom protocols', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ protocols: ['rtmp'],
+ }],
+ valid: [
+ 'rtmp://foobar.com',
+ ],
+ invalid: [
+ 'http://foobar.com',
+ ],
+ });
+ });
+
+ it('should validate file URLs without a host', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ protocols: ['file'],
+ require_host: false,
+ }],
+ valid: [
+ 'file://localhost/foo.txt',
+ 'file:///foo.txt',
+ 'file:///',
+ ],
+ invalid: [
+ 'http://foobar.com',
+ ],
+ });
+ });
+
+ it('should validate URLs with any protocol', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ require_valid_protocol: false,
+ }],
+ valid: [
+ 'rtmp://foobar.com',
+ 'http://foobar.com',
+ 'test://foobar.com',
+ ],
+ invalid: [
+ 'mailto:test@example.com',
+ ],
+ });
+ });
+
+ it('should validate URLs with underscores', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ allow_underscores: true,
+ }],
+ valid: [
+ 'http://foo_bar.com',
+ 'http://pr.example_com.294.example.com/',
+ 'http://foo__bar.com',
+ ],
+ invalid: [],
+ });
+ });
+
+ it('should validate URLs that do not have a TLD', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ require_tld: false,
+ }],
+ valid: [
+ 'http://foobar.com/',
+ 'http://foobar/',
+ 'foobar/',
+ 'foobar',
+ ],
+ invalid: [],
+ });
+ });
+
+ it('should validate URLs with a trailing dot option', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ allow_trailing_dot: true,
+ require_tld: false,
+ }],
+ valid: [
+ 'http://example.com.',
+ 'foobar.',
+ ],
+ });
+ });
+
+ it('should validate protocol relative URLs', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ allow_protocol_relative_urls: true,
+ }],
+ valid: [
+ '//foobar.com',
+ 'http://foobar.com',
+ 'foobar.com',
+ ],
+ invalid: [
+ '://foobar.com',
+ '/foobar.com',
+ '////foobar.com',
+ 'http:////foobar.com',
+ ],
+ });
+ });
+
+ it('should not validate protocol relative URLs when require protocol is true', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ allow_protocol_relative_urls: true,
+ require_protocol: true,
+ }],
+ valid: [
+ 'http://foobar.com',
+ ],
+ invalid: [
+ '//foobar.com',
+ '://foobar.com',
+ '/foobar.com',
+ 'foobar.com',
+ ],
+ });
+ });
+
+ it('should let users specify whether URLs require a protocol', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ require_protocol: true,
+ }],
+ valid: [
+ 'http://foobar.com/',
+ 'http://localhost/',
+ ],
+ invalid: [
+ 'foobar.com',
+ 'foobar',
+ ],
+ });
+ });
+
+ it('should let users specify a host whitelist', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ host_whitelist: ['foo.com', 'bar.com'],
+ }],
+ valid: [
+ 'http://bar.com/',
+ 'http://foo.com/',
+ ],
+ invalid: [
+ 'http://foobar.com',
+ 'http://foo.bar.com/',
+ 'http://qux.com',
+ ],
+ });
+ });
+
+ it('should allow regular expressions in the host whitelist', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ host_whitelist: ['bar.com', 'foo.com', /\.foo\.com$/],
+ }],
+ valid: [
+ 'http://bar.com/',
+ 'http://foo.com/',
+ 'http://images.foo.com/',
+ 'http://cdn.foo.com/',
+ 'http://a.b.c.foo.com/',
+ ],
+ invalid: [
+ 'http://foobar.com',
+ 'http://foo.bar.com/',
+ 'http://qux.com',
+ ],
+ });
+ });
+
+ it('should let users specify a host blacklist', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ host_blacklist: ['foo.com', 'bar.com'],
+ }],
+ valid: [
+ 'http://foobar.com',
+ 'http://foo.bar.com/',
+ 'http://qux.com',
+ ],
+ invalid: [
+ 'http://bar.com/',
+ 'http://foo.com/',
+ ],
+ });
+ });
+
+ it('should allow regular expressions in the host blacklist', function () {
+ test({
+ validator: 'isURL',
+ args: [{
+ host_blacklist: ['bar.com', 'foo.com', /\.foo\.com$/],
+ }],
+ valid: [
+ 'http://foobar.com',
+ 'http://foo.bar.com/',
+ 'http://qux.com',
+ ],
+ invalid: [
+ 'http://bar.com/',
+ 'http://foo.com/',
+ 'http://images.foo.com/',
+ 'http://cdn.foo.com/',
+ 'http://a.b.c.foo.com/',
+ ],
+ });
+ });
+
+ it('should validate MAC addresses', function () {
+ test({
+ validator: 'isMACAddress',
+ valid: [
+ 'ab:ab:ab:ab:ab:ab',
+ 'FF:FF:FF:FF:FF:FF',
+ '01:02:03:04:05:ab',
+ '01:AB:03:04:05:06',
+ ],
+ invalid: [
+ 'abc',
+ '01:02:03:04:05',
+ '01:02:03:04::ab',
+ '1:2:3:4:5:6',
+ 'AB:CD:EF:GH:01:02',
+ ],
+ });
+ });
+
+ it('should validate IP addresses', function () {
+ test({
+ validator: 'isIP',
+ valid: [
+ '127.0.0.1',
+ '0.0.0.0',
+ '255.255.255.255',
+ '1.2.3.4',
+ '::1',
+ '2001:db8:0000:1:1:1:1:1',
+ '2001:41d0:2:a141::1',
+ '::ffff:127.0.0.1',
+ '::0000',
+ '0000::',
+ '1::',
+ '1111:1:1:1:1:1:1:1',
+ 'fe80::a6db:30ff:fe98:e946',
+ '::',
+ '::ffff:127.0.0.1',
+ '0:0:0:0:0:ffff:127.0.0.1',
+ ],
+ invalid: [
+ 'abc',
+ '256.0.0.0',
+ '0.0.0.256',
+ '26.0.0.256',
+ '0200.200.200.200',
+ '200.0200.200.200',
+ '200.200.0200.200',
+ '200.200.200.0200',
+ '::banana',
+ 'banana::',
+ '::1banana',
+ '::1::',
+ '1:',
+ ':1',
+ ':1:1:1::2',
+ '1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1',
+ '::11111',
+ '11111:1:1:1:1:1:1:1',
+ '2001:db8:0000:1:1:1:1::1',
+ '0:0:0:0:0:0:ffff:127.0.0.1',
+ '0:0:0:0:ffff:127.0.0.1',
+ ],
+ });
+ test({
+ validator: 'isIP',
+ args: [4],
+ valid: [
+ '127.0.0.1',
+ '0.0.0.0',
+ '255.255.255.255',
+ '1.2.3.4',
+ ],
+ invalid: [
+ '::1',
+ '2001:db8:0000:1:1:1:1:1',
+ '::ffff:127.0.0.1',
+ ],
+ });
+ test({
+ validator: 'isIP',
+ args: [6],
+ valid: [
+ '::1',
+ '2001:db8:0000:1:1:1:1:1',
+ '::ffff:127.0.0.1',
+ ],
+ invalid: [
+ '127.0.0.1',
+ '0.0.0.0',
+ '255.255.255.255',
+ '1.2.3.4',
+ '::ffff:287.0.0.1',
+ ],
+ });
+ test({
+ validator: 'isIP',
+ args: [10],
+ valid: [],
+ invalid: [
+ '127.0.0.1',
+ '0.0.0.0',
+ '255.255.255.255',
+ '1.2.3.4',
+ '::1',
+ '2001:db8:0000:1:1:1:1:1',
+ ],
+ });
+ });
+
+ it('should validate FQDN', function () {
+ test({
+ validator: 'isFQDN',
+ valid: [
+ 'domain.com',
+ 'dom.plato',
+ 'a.domain.co',
+ 'foo--bar.com',
+ 'xn--froschgrn-x9a.com',
+ 'rebecca.blackfriday',
+ ],
+ invalid: [
+ 'abc',
+ '256.0.0.0',
+ '_.com',
+ '*.some.com',
+ 's!ome.com',
+ 'domain.com/',
+ '/more.com',
+ ],
+ });
+ });
+ it('should validate FQDN with trailing dot option', function () {
+ test({
+ validator: 'isFQDN',
+ args: [
+ { allow_trailing_dot: true },
+ ],
+ valid: [
+ 'example.com.',
+ ],
+ });
+ });
+
+ it('should validate alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ valid: [
+ 'abc',
+ 'ABC',
+ 'FoObar',
+ ],
+ invalid: [
+ 'abc1',
+ ' foo ',
+ '',
+ 'ÄBC',
+ 'FÜübar',
+ 'Jön',
+ 'Heiß',
+ ],
+ });
+ });
+
+ it('should validate czech alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['cs-CZ'],
+ valid: [
+ 'žluťoučký',
+ 'KŮŇ',
+ 'Pěl',
+ 'Ďábelské',
+ 'ódy',
+ ],
+ invalid: [
+ 'ábc1',
+ ' fůj ',
+ '',
+ ],
+ });
+ });
+
+ it('should validate german alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['de-DE'],
+ valid: [
+ 'äbc',
+ 'ÄBC',
+ 'FöÖbär',
+ 'Heiß',
+ ],
+ invalid: [
+ 'äbc1',
+ ' föö ',
+ '',
+ ],
+ });
+ });
+
+ it('should validate hungarian alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['hu-HU'],
+ valid: [
+ 'árvíztűrőtükörfúrógép',
+ 'ÁRVÍZTŰRŐTÜKÖRFÚRÓGÉP',
+ ],
+ invalid: [
+ 'äbc1',
+ ' fäö ',
+ 'Heiß',
+ '',
+ ],
+ });
+ });
+
+ it('should validate arabic alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['ar'],
+ valid: [
+ 'أبت',
+ 'اَبِتَثّجً',
+ ],
+ invalid: [
+ '١٢٣أبت',
+ '١٢٣',
+ 'abc1',
+ ' foo ',
+ '',
+ 'ÄBC',
+ 'FÜübar',
+ 'Jön',
+ 'Heiß',
+ ],
+ });
+ });
+
+ it('should validate serbian cyrillic alpha strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['sr-RS'],
+ valid: [
+ 'ШћжЂљЕ',
+ 'ЧПСТЋЏ',
+ ],
+ invalid: [
+ 'řiď ',
+ 'blé33!!',
+ 'föö!!',
+ ],
+ });
+ });
+
+ it('should validate serbian latin alpha strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['sr-RS@latin'],
+ valid: [
+ 'ŠAabčšđćž',
+ 'ŠATROĆčđš',
+ ],
+ invalid: [
+ '12řiď ',
+ 'blé!!',
+ 'föö!2!',
+ ],
+ });
+ });
+
+
+ it('should validate defined arabic locales alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['ar-SY'],
+ valid: [
+ 'أبت',
+ 'اَبِتَثّجً',
+ ],
+ invalid: [
+ '١٢٣أبت',
+ '١٢٣',
+ 'abc1',
+ ' foo ',
+ '',
+ 'ÄBC',
+ 'FÜübar',
+ 'Jön',
+ 'Heiß',
+ ],
+ });
+ });
+
+ it('should validate turkish alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['tr-TR'],
+ valid: [
+ 'AİıÖöÇ窺ĞğÜüZ',
+ ],
+ invalid: [
+ '0AİıÖöÇ窺ĞğÜüZ1',
+ ' AİıÖöÇ窺ĞğÜüZ ',
+ 'abc1',
+ ' foo ',
+ '',
+ 'ÄBC',
+ 'Heiß',
+ ],
+ });
+ });
+
+ it('should validate urkrainian alpha strings', function () {
+ test({
+ validator: 'isAlpha',
+ args: ['uk-UA'],
+ valid: [
+ 'АБВГҐДЕЄЖЗИIЇЙКЛМНОПРСТУФХЦШЩЬЮЯ',
+ ],
+ invalid: [
+ '0AİıÖöÇ窺ĞğÜüZ1',
+ ' AİıÖöÇ窺ĞğÜüZ ',
+ 'abc1',
+ ' foo ',
+ '',
+ 'ÄBC',
+ 'Heiß',
+ 'ЫыЪъЭэ',
+ ],
+ });
+ });
+
+ it('should validate alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ valid: [
+ 'abc123',
+ 'ABC11',
+ ],
+ invalid: [
+ 'abc ',
+ 'foo!!',
+ 'ÄBC',
+ 'FÜübar',
+ 'Jön',
+ ],
+ });
+ });
+
+ it('should validate defined english aliases', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['en-GB'],
+ valid: [
+ 'abc123',
+ 'ABC11',
+ ],
+ invalid: [
+ 'abc ',
+ 'foo!!',
+ 'ÄBC',
+ 'FÜübar',
+ 'Jön',
+ ],
+ });
+ });
+
+ it('should validate czech alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['cs-CZ'],
+ valid: [
+ 'řiť123',
+ 'KŮŇ11',
+ ],
+ invalid: [
+ 'řiď ',
+ 'blé!!',
+ ],
+ });
+ });
+
+ it('should validate german alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['de-DE'],
+ valid: [
+ 'äbc123',
+ 'ÄBC11',
+ ],
+ invalid: [
+ 'äca ',
+ 'föö!!',
+ ],
+ });
+ });
+
+ it('should validate hungarian alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['hu-HU'],
+ valid: [
+ '0árvíztűrőtükörfúrógép123',
+ '0ÁRVÍZTŰRŐTÜKÖRFÚRÓGÉP123',
+ ],
+ invalid: [
+ '1időúr!',
+ 'äbc1',
+ ' fäö ',
+ 'Heiß!',
+ '',
+ ],
+ });
+ });
+
+ it('should validate spanish alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['es-ES'],
+ valid: [
+ 'ábcó123',
+ 'ÁBCÓ11',
+ ],
+ invalid: [
+ 'äca ',
+ 'abcß',
+ 'föö!!',
+ ],
+ });
+ });
+
+ it('should validate arabic alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['ar'],
+ valid: [
+ 'أبت123',
+ 'أبتَُِ١٢٣',
+ ],
+ invalid: [
+ 'äca ',
+ 'abcß',
+ 'föö!!',
+ ],
+ });
+ });
+
+ it('should validate defined arabic aliases', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['ar-SY'],
+ valid: [
+ 'أبت123',
+ 'أبتَُِ١٢٣',
+ ],
+ invalid: [
+ 'abc ',
+ 'foo!!',
+ 'ÄBC',
+ 'FÜübar',
+ 'Jön',
+ ],
+ });
+ });
+
+ it('should validate serbian cyrillic alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['sr-RS'],
+ valid: [
+ 'ШћжЂљЕ123',
+ 'ЧПСТ132ЋЏ',
+ ],
+ invalid: [
+ 'řiď ',
+ 'blé!!',
+ 'föö!!',
+ ],
+ });
+ });
+
+ it('should validate serbian latin alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['sr-RS@latin'],
+ valid: [
+ 'ŠAabčšđćž123',
+ 'ŠATRO11Ćčđš',
+ ],
+ invalid: [
+ 'řiď ',
+ 'blé!!',
+ 'föö!!',
+ ],
+ });
+ });
+
+ it('should validate turkish alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['tr-TR'],
+ valid: [
+ 'AİıÖöÇ窺ĞğÜüZ123',
+ ],
+ invalid: [
+ 'AİıÖöÇ窺ĞğÜüZ ',
+ 'foo!!',
+ 'ÄBC',
+ ],
+ });
+ });
+
+ it('should validate urkrainian alphanumeric strings', function () {
+ test({
+ validator: 'isAlphanumeric',
+ args: ['uk-UA'],
+ valid: [
+ 'АБВГҐДЕЄЖЗИIЇЙКЛМНОПРСТУФХЦШЩЬЮЯ123',
+ ],
+ invalid: [
+ 'éeoc ',
+ 'foo!!',
+ 'ÄBC',
+ 'ЫыЪъЭэ',
+ ],
+ });
+ });
+
+ it('should error on invalid locale', function () {
+ try {
+ validator.isAlphanumeric('abc123', 'in-INVALID');
+ assert(false);
+ } catch (err) {
+ assert(true);
+ }
+ });
+
+ it('should validate numeric strings', function () {
+ test({
+ validator: 'isNumeric',
+ valid: [
+ '123',
+ '00123',
+ '-00123',
+ '0',
+ '-0',
+ '+123',
+ ],
+ invalid: [
+ '123.123',
+ ' ',
+ '.',
+ ],
+ });
+ });
+
+ it('should validate RFC5646 strings', function () {
+ test({
+ validator: 'isRFC5646',
+ valid: [
+ 'en-US',
+ 'en-US',
+ ],
+ invalid: [
+ 'enus',
+ 'en-us',
+ 'xxx',
+ '',
+ ],
+ });
+ });
+
+ it('should validate RGBColor strings', function () {
+ test({
+ validator: 'isRGBColor',
+ valid: [
+ "rgb(0,31,255)",
+ "rgb(0, 31, 255)",
+ ],
+ invalid: [
+ "",
+ "rgb(1,349,275)",
+ "rgb(01,31,255)",
+ "rgb(0.6,31,255)",
+ "rgba(0,31,255)",
+ ],
+ });
+ });
+
+ it('should validate SemVer strings', function () {
+ test({
+ validator: 'isSemVer',
+ valid: [
+ "v1.0.0",
+ "1.0.0",
+ "1.0.0-alpha",
+ "1.0.0-alpha.1",
+ "1.0.0-0.3.7",
+ "1.0.0-x.7.z.92",
+ "1.0.0-alpha+001",
+ "1.0.0+20130313144700",
+ "1.0.0-beta+exp.sha.5114f85",
+ "1.0.0-beta+exp.sha.05114f85",
+ ],
+ invalid: [
+ "1.1.01",
+ "1.01.0",
+ "01.1.0",
+ "v1.1.01",
+ "v1.01.0",
+ "v01.1.0",
+ "1.0.0-0.03.7",
+ "1.0.0-00.3.7",
+ "1.0.0-+beta",
+ "1.0.0-b+-9+eta",
+ "v+1.8.0-b+-9+eta",
+ ],
+ });
+ });
+
+ it('should validate decimal numbers', function () {
+ test({
+ validator: 'isDecimal',
+ valid: [
+ '123',
+ '00123',
+ '-00123',
+ '0',
+ '-0',
+ '+123',
+ '0.01',
+ '.1',
+ '1.0',
+ '-.25',
+ '-0',
+ '0.0000000000001',
+ ],
+ invalid: [
+ '....',
+ ' ',
+ '',
+ '-',
+ '+',
+ '.',
+ '0.1a',
+ 'a',
+ '\n',
+ ],
+ });
+ });
+
+ it('should validate lowercase strings', function () {
+ test({
+ validator: 'isLowercase',
+ valid: [
+ 'abc',
+ 'abc123',
+ 'this is lowercase.',
+ 'tr竪s 端ber',
+ ],
+ invalid: [
+ 'fooBar',
+ '123A',
+ ],
+ });
+ });
+
+ it('should validate uppercase strings', function () {
+ test({
+ validator: 'isUppercase',
+ valid: [
+ 'ABC',
+ 'ABC123',
+ 'ALL CAPS IS FUN.',
+ ' .',
+ ],
+ invalid: [
+ 'fooBar',
+ '123abc',
+ ],
+ });
+ });
+
+ it('should validate integers', function () {
+ test({
+ validator: 'isInt',
+ valid: [
+ '13',
+ '123',
+ '0',
+ '123',
+ '-0',
+ '+1',
+ '01',
+ '-01',
+ '000',
+ ],
+ invalid: [
+ '100e10',
+ '123.123',
+ ' ',
+ '',
+ ],
+ });
+ test({
+ validator: 'isInt',
+ args: [{ allow_leading_zeroes: false }],
+ valid: [
+ '13',
+ '123',
+ '0',
+ '123',
+ '-0',
+ '+1',
+ ],
+ invalid: [
+ '01',
+ '-01',
+ '000',
+ '100e10',
+ '123.123',
+ ' ',
+ '',
+ ],
+ });
+ test({
+ validator: 'isInt',
+ args: [{ allow_leading_zeroes: true }],
+ valid: [
+ '13',
+ '123',
+ '0',
+ '123',
+ '-0',
+ '+1',
+ '01',
+ '-01',
+ '000',
+ '-000',
+ '+000',
+ ],
+ invalid: [
+ '100e10',
+ '123.123',
+ ' ',
+ '',
+ ],
+ });
+ test({
+ validator: 'isInt',
+ args: [{
+ min: 10,
+ }],
+ valid: [
+ '15',
+ '80',
+ '99',
+ ],
+ invalid: [
+ '9',
+ '6',
+ '3.2',
+ 'a',
+ ],
+ });
+ test({
+ validator: 'isInt',
+ args: [{
+ min: 10,
+ max: 15,
+ }],
+ valid: [
+ '15',
+ '11',
+ '13',
+ ],
+ invalid: [
+ '9',
+ '2',
+ '17',
+ '3.2',
+ '33',
+ 'a',
+ ],
+ });
+ test({
+ validator: 'isInt',
+ args: [{
+ gt: 10,
+ lt: 15,
+ }],
+ valid: [
+ '14',
+ '11',
+ '13',
+ ],
+ invalid: [
+ '10',
+ '15',
+ '17',
+ '3.2',
+ '33',
+ 'a',
+ ],
+ });
+ });
+
+ it('should validate floats', function () {
+ test({
+ validator: 'isFloat',
+ valid: [
+ '123',
+ '123.',
+ '123.123',
+ '-123.123',
+ '-0.123',
+ '+0.123',
+ '0.123',
+ '.0',
+ '-.123',
+ '+.123',
+ '01.123',
+ '-0.22250738585072011e-307',
+ ],
+ invalid: [
+ ' ',
+ '',
+ '.',
+ 'foo',
+ ],
+ });
+ test({
+ validator: 'isFloat',
+ args: [{
+ min: 3.7,
+ }],
+ valid: [
+ '3.888',
+ '3.92',
+ '4.5',
+ '50',
+ '3.7',
+ '3.71',
+ ],
+ invalid: [
+ '3.6',
+ '3.69',
+ '3',
+ '1.5',
+ 'a',
+ ],
+ });
+ test({
+ validator: 'isFloat',
+ args: [{
+ min: 0.1,
+ max: 1.0,
+ }],
+ valid: [
+ '0.1',
+ '1.0',
+ '0.15',
+ '0.33',
+ '0.57',
+ '0.7',
+ ],
+ invalid: [
+ '0',
+ '0.0',
+ 'a',
+ '1.3',
+ '0.05',
+ '5',
+ ],
+ });
+ test({
+ validator: 'isFloat',
+ args: [{
+ gt: -5.5,
+ lt: 10,
+ }],
+ valid: [
+ '9.9',
+ '1.0',
+ '0',
+ '-1',
+ '7',
+ '-5.4',
+ ],
+ invalid: [
+ '10',
+ '-5.5',
+ 'a',
+ '-20.3',
+ '20e3',
+ '10.00001',
+ ],
+ });
+ test({
+ validator: 'isFloat',
+ args: [{
+ min: -5.5,
+ max: 10,
+ gt: -5.5,
+ lt: 10,
+ }],
+ valid: [
+ '9.99999',
+ '-5.499999',
+ ],
+ invalid: [
+ '10',
+ '-5.5',
+ ],
+ });
+ });
+
+ it('should validate hexadecimal strings', function () {
+ test({
+ validator: 'isHexadecimal',
+ valid: [
+ 'deadBEEF',
+ 'ff0044',
+ ],
+ invalid: [
+ 'abcdefg',
+ '',
+ '..',
+ ],
+ });
+ });
+
+ it('should validate hexadecimal color strings', function () {
+ test({
+ validator: 'isHexColor',
+ valid: [
+ '#ff0034',
+ '#CCCCCC',
+ 'fff',
+ '#f00',
+ ],
+ invalid: [
+ '#ff',
+ 'fff0',
+ '#ff12FG',
+ ],
+ });
+ });
+
+ it('should validate ISRC code strings', function () {
+ test({
+ validator: 'isISRC',
+ valid: [
+ 'USAT29900609',
+ 'GBAYE6800011',
+ 'USRC15705223',
+ 'USCA29500702',
+ ],
+ invalid: [
+ 'USAT2990060',
+ 'SRC15705223',
+ 'US-CA29500702',
+ 'USARC15705223',
+ ],
+ });
+ });
+
+ it('should validate md5 strings', function () {
+ test({
+ validator: 'isMD5',
+ valid: [
+ 'd94f3f016ae679c3008de268209132f2',
+ '751adbc511ccbe8edf23d486fa4581cd',
+ '88dae00e614d8f24cfd5a8b3f8002e93',
+ '0bf1c35032a71a14c2f719e5a14c1e96',
+ ],
+ invalid: [
+ 'KYT0bf1c35032a71a14c2f719e5a14c1',
+ 'q94375dj93458w34',
+ '39485729348',
+ '%&FHKJFvk',
+ ],
+ });
+ });
+
+ it('should validate null strings', function () {
+ test({
+ validator: 'isEmpty',
+ valid: [
+ '',
+ ],
+ invalid: [
+ ' ',
+ 'foo',
+ '3',
+ ],
+ });
+ });
+
+ it('should validate strings against an expected value', function () {
+ test({ validator: 'equals', args: ['abc'], valid: ['abc'], invalid: ['Abc', '123'] });
+ });
+
+ it('should validate strings contain another string', function () {
+ test({
+ validator: 'contains',
+ args: ['foo'],
+ valid: ['foo', 'foobar', 'bazfoo'],
+ invalid: ['bar', 'fobar'],
+ });
+ });
+
+ it('should validate strings against a pattern', function () {
+ test({
+ validator: 'matches',
+ args: [/abc/],
+ valid: ['abc', 'abcdef', '123abc'],
+ invalid: ['acb', 'Abc'],
+ });
+ test({
+ validator: 'matches',
+ args: ['abc'],
+ valid: ['abc', 'abcdef', '123abc'],
+ invalid: ['acb', 'Abc'],
+ });
+ test({
+ validator: 'matches',
+ args: ['abc', 'i'],
+ valid: ['abc', 'abcdef', '123abc', 'AbC'],
+ invalid: ['acb'],
+ });
+ });
+
+ it('should validate strings by length (deprecated api)', function () {
+ test({
+ validator: 'isLength',
+ args: [2],
+ valid: ['abc', 'de', 'abcd'],
+ invalid: ['', 'a'],
+ });
+ test({
+ validator: 'isLength',
+ args: [2, 3],
+ valid: ['abc', 'de'],
+ invalid: ['', 'a', 'abcd'],
+ });
+ test({
+ validator: 'isLength',
+ args: [2, 3],
+ valid: ['干𩸽', '𠮷野家'],
+ invalid: ['', '𠀋', '千竈通り'],
+ });
+ test({
+ validator: 'isLength',
+ args: [0, 0],
+ valid: [''],
+ invalid: ['a', 'ab'],
+ });
+ });
+
+ it('should validate strings by byte length (deprecated api)', function () {
+ test({
+ validator: 'isByteLength',
+ args: [2],
+ valid: ['abc', 'de', 'abcd', 'gmail'],
+ invalid: ['', 'a'],
+ });
+ test({
+ validator: 'isByteLength',
+ args: [2, 3],
+ valid: ['abc', 'de', 'g'],
+ invalid: ['', 'a', 'abcd', 'gm'],
+ });
+ test({
+ validator: 'isByteLength',
+ args: [0, 0],
+ valid: [''],
+ invalid: ['g', 'a'],
+ });
+ });
+
+ it('should validate strings by length', function () {
+ test({
+ validator: 'isLength',
+ args: [{ min: 2 }],
+ valid: ['abc', 'de', 'abcd'],
+ invalid: ['', 'a'],
+ });
+ test({
+ validator: 'isLength',
+ args: [{ min: 2, max: 3 }],
+ valid: ['abc', 'de'],
+ invalid: ['', 'a', 'abcd'],
+ });
+ test({
+ validator: 'isLength',
+ args: [{ min: 2, max: 3 }],
+ valid: ['干𩸽', '𠮷野家'],
+ invalid: ['', '𠀋', '千竈通り'],
+ });
+ test({
+ validator: 'isLength',
+ args: [{ max: 3 }],
+ valid: ['abc', 'de', 'a', ''],
+ invalid: ['abcd'],
+ });
+ test({
+ validator: 'isLength',
+ args: [{ max: 0 }],
+ valid: [''],
+ invalid: ['a', 'ab'],
+ });
+ });
+
+ it('should validate strings by byte length', function () {
+ test({
+ validator: 'isByteLength',
+ args: [{ min: 2 }],
+ valid: ['abc', 'de', 'abcd', 'gmail'],
+ invalid: ['', 'a'],
+ });
+ test({
+ validator: 'isByteLength',
+ args: [{ min: 2, max: 3 }],
+ valid: ['abc', 'de', 'g'],
+ invalid: ['', 'a', 'abcd', 'gm'],
+ });
+ test({
+ validator: 'isByteLength',
+ args: [{ max: 3 }],
+ valid: ['abc', 'de', 'g', 'a', ''],
+ invalid: ['abcd', 'gm'],
+ });
+ test({
+ validator: 'isByteLength',
+ args: [{ max: 0 }],
+ valid: [''],
+ invalid: ['g', 'a'],
+ });
+ });
+
+ it('should validate UUIDs', function () {
+ test({
+ validator: 'isUUID',
+ valid: [
+ 'A987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ 'A987FBC9-4BED-4078-8F07-9141BA07C9F3',
+ 'A987FBC9-4BED-5078-AF07-9141BA07C9F3',
+ ],
+ invalid: [
+ '',
+ 'xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ 'A987FBC9-4BED-3078-CF07-9141BA07C9F3xxx',
+ 'A987FBC94BED3078CF079141BA07C9F3',
+ '934859',
+ '987FBC9-4BED-3078-CF07A-9141BA07C9F3',
+ 'AAAAAAAA-1111-1111-AAAG-111111111111',
+ ],
+ });
+ test({
+ validator: 'isUUID',
+ args: [3],
+ valid: [
+ 'A987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ ],
+ invalid: [
+ '',
+ 'xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ '934859',
+ 'AAAAAAAA-1111-1111-AAAG-111111111111',
+ 'A987FBC9-4BED-4078-8F07-9141BA07C9F3',
+ 'A987FBC9-4BED-5078-AF07-9141BA07C9F3',
+ ],
+ });
+ test({
+ validator: 'isUUID',
+ args: [4],
+ valid: [
+ '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1',
+ '625e63f3-58f5-40b7-83a1-a72ad31acffb',
+ '57b73598-8764-4ad0-a76a-679bb6640eb1',
+ '9c858901-8a57-4791-81fe-4c455b099bc9',
+ ],
+ invalid: [
+ '',
+ 'xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ '934859',
+ 'AAAAAAAA-1111-1111-AAAG-111111111111',
+ 'A987FBC9-4BED-5078-AF07-9141BA07C9F3',
+ 'A987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ ],
+ });
+ test({
+ validator: 'isUUID',
+ args: [5],
+ valid: [
+ '987FBC97-4BED-5078-AF07-9141BA07C9F3',
+ '987FBC97-4BED-5078-BF07-9141BA07C9F3',
+ '987FBC97-4BED-5078-8F07-9141BA07C9F3',
+ '987FBC97-4BED-5078-9F07-9141BA07C9F3',
+ ],
+ invalid: [
+ '',
+ 'xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ '934859',
+ 'AAAAAAAA-1111-1111-AAAG-111111111111',
+ '9c858901-8a57-4791-81fe-4c455b099bc9',
+ 'A987FBC9-4BED-3078-CF07-9141BA07C9F3',
+ ],
+ });
+ });
+
+ it('should validate a string that is in another string or array', function () {
+ test({
+ validator: 'isIn',
+ args: ['foobar'],
+ valid: ['foo', 'bar', 'foobar', ''],
+ invalid: ['foobarbaz', 'barfoo'],
+ });
+ test({
+ validator: 'isIn',
+ args: [['foo', 'bar']],
+ valid: ['foo', 'bar'],
+ invalid: ['foobar', 'barfoo', ''],
+ });
+ test({
+ validator: 'isIn',
+ args: [['1', '2', '3']],
+ valid: ['1', '2', '3'],
+ invalid: ['4', ''],
+ });
+ test({ validator: 'isIn', invalid: ['foo', ''] });
+ });
+
+ it('should validate a string that is in another object', function () {
+ test({
+ validator: 'isIn',
+ args: [{ 'foo': 1, 'bar': 2, 'foobar': 3 }],
+ valid: ['foo', 'bar', 'foobar'],
+ invalid: ['foobarbaz', 'barfoo', ''],
+ });
+ test({
+ validator: 'isIn',
+ args: [{ 1: 3, 2: 0, 3: 1 }],
+ valid: ['1', '2', '3'],
+ invalid: ['4', ''],
+ });
+ });
+
+ it('should validate dates against a start date', function () {
+ test({
+ validator: 'isAfter',
+ args: ['2011-08-03'],
+ valid: ['2011-08-04', new Date(2011, 8, 10).toString()],
+ invalid: ['2010-07-02', '2011-08-03', new Date(0).toString(), 'foo'],
+ });
+ test({
+ validator: 'isAfter',
+ valid: ['2100-08-04', new Date(Date.now() + 86400000).toString()],
+ invalid: ['2010-07-02', new Date(0).toString()],
+ });
+ test({
+ validator: 'isAfter',
+ args: ['2011-08-03'],
+ valid: ['2015-09-17'],
+ invalid: ['invalid date'],
+ });
+ test({
+ validator: 'isAfter',
+ args: ['invalid date'],
+ invalid: ['invalid date', '2015-09-17'],
+ });
+ });
+
+ it('should validate dates against an end date', function () {
+ test({
+ validator: 'isBefore',
+ args: ['08/04/2011'],
+ valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
+ invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
+ });
+ test({
+ validator: 'isBefore',
+ args: [new Date(2011, 7, 4).toString()],
+ valid: ['2010-07-02', '2010-08-04', new Date(0).toString()],
+ invalid: ['08/04/2011', new Date(2011, 9, 10).toString()],
+ });
+ test({
+ validator: 'isBefore',
+ valid: [
+ '2000-08-04',
+ new Date(0).toString(),
+ new Date(Date.now() - 86400000).toString(),
+ ],
+ invalid: ['2100-07-02', new Date(2097, 10, 10).toString()],
+ });
+ test({
+ validator: 'isBefore',
+ args: ['2011-08-03'],
+ valid: ['1999-12-31'],
+ invalid: ['invalid date'],
+ });
+ test({
+ validator: 'isBefore',
+ args: ['invalid date'],
+ invalid: ['invalid date', '1999-12-31'],
+ });
+ });
+
+ it('should validate that integer strings are divisible by a number', function () {
+ test({
+ validator: 'isDivisibleBy',
+ args: [2],
+ valid: ['2', '4', '100', '1000'],
+ invalid: [
+ '1',
+ '2.5',
+ '101',
+ 'foo',
+ '',
+ ],
+ });
+ });
+
+ it('should validate credit cards', function () {
+ test({
+ validator: 'isCreditCard',
+ valid: [
+ '375556917985515',
+ '36050234196908',
+ '4716461583322103',
+ '4716-2210-5188-5662',
+ '4929 7226 5379 7141',
+ '5398228707871527',
+ '6283875070985593',
+ '6263892624162870',
+ '6234917882863855',
+ '6234698580215388',
+ '6226050967750613',
+ '6246281879460688',
+ '2222155765072228',
+ '2225855203075256',
+ '2720428011723762',
+ '2718760626256570',
+ ],
+ invalid: [
+ 'foo',
+ 'foo',
+ '5398228707871528',
+ '2718760626256571',
+ '2721465526338453',
+ '2220175103860763',
+ ],
+ });
+ });
+
+ it('should validate ISINs', function () {
+ test({
+ validator: 'isISIN',
+ valid: [
+ 'AU0000XVGZA3',
+ 'DE000BAY0017',
+ 'BE0003796134',
+ 'SG1G55870362',
+ 'GB0001411924',
+ 'DE000WCH8881',
+ 'PLLWBGD00016',
+ ],
+ invalid: [
+ 'DE000BAY0018',
+ 'PLLWBGD00019',
+ 'foo',
+ '5398228707871528',
+ ],
+ });
+ });
+
+ it('should validate ISBNs', function () {
+ test({
+ validator: 'isISBN',
+ args: [10],
+ valid: [
+ '3836221195', '3-8362-2119-5', '3 8362 2119 5',
+ '1617290858', '1-61729-085-8', '1 61729 085-8',
+ '0007269706', '0-00-726970-6', '0 00 726970 6',
+ '3423214120', '3-423-21412-0', '3 423 21412 0',
+ '340101319X', '3-401-01319-X', '3 401 01319 X',
+ ],
+ invalid: [
+ '3423214121', '3-423-21412-1', '3 423 21412 1',
+ '978-3836221191', '9783836221191',
+ '123456789a', 'foo', '',
+ ],
+ });
+ test({
+ validator: 'isISBN',
+ args: [13],
+ valid: [
+ '9783836221191', '978-3-8362-2119-1', '978 3 8362 2119 1',
+ '9783401013190', '978-3401013190', '978 3401013190',
+ '9784873113685', '978-4-87311-368-5', '978 4 87311 368 5',
+ ],
+ invalid: [
+ '9783836221190', '978-3-8362-2119-0', '978 3 8362 2119 0',
+ '3836221195', '3-8362-2119-5', '3 8362 2119 5',
+ '01234567890ab', 'foo', '',
+ ],
+ });
+ test({
+ validator: 'isISBN',
+ valid: [
+ '340101319X',
+ '9784873113685',
+ ],
+ invalid: [
+ '3423214121',
+ '9783836221190',
+ ],
+ });
+ test({
+ validator: 'isISBN',
+ args: ['foo'],
+ invalid: [
+ '340101319X',
+ '9784873113685',
+ ],
+ });
+ });
+
+ it('should validate ISSNs', function () {
+ test({
+ validator: 'isISSN',
+ valid: [
+ '0378-5955',
+ '0000-0000',
+ '2434-561X',
+ '2434-561x',
+ '01896016',
+ '20905076',
+ ],
+ invalid: [
+ '0378-5954',
+ '0000-0001',
+ '0378-123',
+ '037-1234',
+ '0',
+ '2434-561c',
+ '1684-5370',
+ '19960791',
+ '',
+ ],
+ });
+ test({
+ validator: 'isISSN',
+ args: [{ case_sensitive: true }],
+ valid: [
+ '2434-561X',
+ '2434561X',
+ '0378-5955',
+ '03785955',
+ ],
+ invalid: [
+ '2434-561x',
+ '2434561x',
+ ],
+ });
+ test({
+ validator: 'isISSN',
+ args: [{ require_hyphen: true }],
+ valid: [
+ '2434-561X',
+ '2434-561x',
+ '0378-5955',
+ ],
+ invalid: [
+ '2434561X',
+ '2434561x',
+ '03785955',
+ ],
+ });
+ test({
+ validator: 'isISSN',
+ args: [{ case_sensitive: true, require_hyphen: true }],
+ valid: [
+ '2434-561X',
+ '0378-5955',
+ ],
+ invalid: [
+ '2434-561x',
+ '2434561X',
+ '2434561x',
+ '03785955',
+ ],
+ });
+ });
+
+ it('should validate JSON', function () {
+ test({
+ validator: 'isJSON',
+ valid: [
+ '{ "key": "value" }',
+ '{}',
+ ],
+ invalid: [
+ '{ key: "value" }',
+ '{ \'key\': \'value\' }',
+ 'null',
+ '1234',
+ 'false',
+ '"nope"',
+ ],
+ });
+ });
+
+ it('should validate multibyte strings', function () {
+ test({
+ validator: 'isMultibyte',
+ valid: [
+ 'ひらがな・カタカナ、.漢字',
+ 'あいうえお foobar',
+ 'test@example.com',
+ '1234abcDExyz',
+ 'カタカナ',
+ '中文',
+ ],
+ invalid: [
+ 'abc',
+ 'abc123',
+ '<>@" *.',
+ ],
+ });
+ });
+
+ it('should validate ascii strings', function () {
+ test({
+ validator: 'isAscii',
+ valid: [
+ 'foobar',
+ '0987654321',
+ 'test@example.com',
+ '1234abcDEF',
+ ],
+ invalid: [
+ 'foobar',
+ 'xyz098',
+ '123456',
+ 'カタカナ',
+ ],
+ });
+ });
+
+ it('should validate full-width strings', function () {
+ test({
+ validator: 'isFullWidth',
+ valid: [
+ 'ひらがな・カタカナ、.漢字',
+ '3ー0 a@com',
+ 'Fカタカナ゙ᆲ',
+ 'Good=Parts',
+ ],
+ invalid: [
+ 'abc',
+ 'abc123',
+ '!"#$%&()<>/+=-_? ~^|.,@`{}[]',
+ ],
+ });
+ });
+
+ it('should validate half-width strings', function () {
+ test({
+ validator: 'isHalfWidth',
+ valid: [
+ '!"#$%&()<>/+=-_? ~^|.,@`{}[]',
+ 'l-btn_02--active',
+ 'abc123い',
+ 'カタカナ゙ᆲ←',
+ ],
+ invalid: [
+ 'あいうえお',
+ '0011',
+ ],
+ });
+ });
+
+ it('should validate variable-width strings', function () {
+ test({
+ validator: 'isVariableWidth',
+ valid: [
+ 'ひらがなカタカナ漢字ABCDE',
+ '3ー0123',
+ 'Fカタカナ゙ᆲ',
+ 'Good=Parts',
+ ],
+ invalid: [
+ 'abc',
+ 'abc123',
+ '!"#$%&()<>/+=-_? ~^|.,@`{}[]',
+ 'ひらがな・カタカナ、.漢字',
+ '123456',
+ 'カタカナ゙ᆲ',
+ ],
+ });
+ });
+
+ it('should validate surrogate pair strings', function () {
+ test({
+ validator: 'isSurrogatePair',
+ valid: [
+ '𠮷野𠮷',
+ '𩸽',
+ 'ABC千𥧄1-2-3',
+ ],
+ invalid: [
+ '吉野竈',
+ '鮪',
+ 'ABC1-2-3',
+ ],
+ });
+ });
+
+ it('should validate base64 strings', function () {
+ test({
+ validator: 'isBase64',
+ valid: [
+ 'Zg==',
+ 'Zm8=',
+ 'Zm9v',
+ 'Zm9vYg==',
+ 'Zm9vYmE=',
+ 'Zm9vYmFy',
+ 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
+ 'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
+ 'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
+ 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
+ 'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
+ 'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
+ 'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
+ 'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
+ 'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
+ 'HQIDAQAB',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ 'Zg=',
+ 'Z===',
+ 'Zm=8',
+ '=m9vYg==',
+ 'Zm9vYmFy====',
+ ],
+ });
+ });
+
+ it('should validate hex-encoded MongoDB ObjectId', function () {
+ test({
+ validator: 'isMongoId',
+ valid: [
+ '507f1f77bcf86cd799439011',
+ ],
+ invalid: [
+ '507f1f77bcf86cd7994390',
+ '507f1f77bcf86cd79943901z',
+ '',
+ '507f1f77bcf86cd799439011 ',
+ ],
+ });
+ });
+
+ it('should validate mobile phone number', function () {
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0944549710',
+ '+963944549710',
+ '956654379',
+ '0944549710',
+ '0962655597',
+ ],
+ invalid: [
+ '12345',
+ '',
+ '+9639626626262',
+ '+963332210972',
+ '0114152198',
+ ],
+ args: ['ar-SY'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0556578654',
+ '+966556578654',
+ '966556578654',
+ '596578654',
+ '572655597',
+ ],
+ invalid: [
+ '12345',
+ '',
+ '+9665626626262',
+ '+96633221097',
+ '0114152198',
+ ],
+ args: ['ar-SA'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+420 123 456 789',
+ '+420 123456789',
+ '+420123456789',
+ '123 456 789',
+ '123456789',
+ ],
+ invalid: [
+ '',
+ '+42012345678',
+ ],
+ args: ['cs-CZ'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+49 (0) 123 456 789',
+ '+49 (0) 123 456789',
+ '0123/4567890',
+ '+49 01234567890',
+ '01234567890',
+ ],
+ invalid: [
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ ],
+ args: ['de-DE'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '55-17-3332-2155',
+ '55-15-25661234',
+ '551223456789',
+ '01523456987',
+ '022995678947',
+ '+55-12-996551215',
+ ],
+ invalid: [
+ '+017-123456789',
+ '5501599623874',
+ '+55012962308',
+ '+55-015-1234-3214',
+ ],
+ args: ['pt-BR'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '15323456787',
+ '13523333233',
+ '13898728332',
+ '+086-13238234822',
+ '08613487234567',
+ '8617823492338',
+ '86-17823492338',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '010-38238383',
+ ],
+ args: ['zh-CN'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0987123456',
+ '+886987123456',
+ '886987123456',
+ '+886-987123456',
+ '886-987123456',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '0-987123456',
+ ],
+ args: ['zh-TW'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ invalid: [
+ '15323456787',
+ '13523333233',
+ '13898728332',
+ '+086-13238234822',
+ '08613487234567',
+ '8617823492338',
+ '86-17823492338',
+ ],
+ args: ['en'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0821231234',
+ '+27821231234',
+ '27821231234',
+ ],
+ invalid: [
+ '082123',
+ '08212312345',
+ '21821231234',
+ '+21821231234',
+ '+0821231234',
+ ],
+ args: ['en-ZA'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '61404111222',
+ '+61411222333',
+ '0417123456',
+ ],
+ invalid: [
+ '082123',
+ '08212312345',
+ '21821231234',
+ '+21821231234',
+ '+0821231234',
+ '04123456789',
+ ],
+ args: ['en-AU'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '91234567',
+ '9123-4567',
+ '61234567',
+ '51234567',
+ '+85291234567',
+ '+852-91234567',
+ '+852-9123-4567',
+ '852-91234567',
+ `8384-${random4digit()}`, // Spare - starting from 1.7.2017
+ `8580-${random4digit()}`, // Spare - starting from 1.7.2017
+ `7109-${random4digit()}`, // Spare - starting from 1.7.2017
+ `7114-${random4digit()}`, // Spare - starting from 1.7.2017
+ `6920-${random4digit()}`, // Spare - starting from 1.7.2017
+ `6280-${random4digit()}`, // Spare - starting from 1.7.2017
+ `5908-${random4digit()}`, // Spare - starting from 1.7.2017
+ `5152-${random4digit()}`, // Spare - starting from 1.7.2017
+ `4923-${random4digit()}`, // Spare - starting from 1.7.2017
+ ],
+ invalid: [
+ '999',
+ '+852-912345678',
+ '123456789',
+ '+852-1234-56789',
+ `9998${random4digit()}`, // Emergency Service (999)
+ `9111-${random4digit()}`, // Others (911)
+ `8521-${random4digit()}`, // Others (Conflict with country code)
+ `8009-${random4digit()}`, // 800 Fixed Services
+ `7211-${random4digit()}`, // Paging Service
+ `7119-${random4digit()}`, // Paging Service
+ `5850-${random4digit()}`, // Fixed Services
+ `5003${random4digit()}`, // Others (Special Services)
+ `4300-${random4digit()}`, // Others (Network Number)
+ `3830-${random4digit()}`, // Fixed Services
+ `2230-${random4digit()}`, // Fixed Services
+ `1833-${random4digit()}`, // Short Code (High traffic volume services)
+ `0060-${random4digit()}`, // Access Code (ETS, Prime IDD for Voice)
+ ],
+ args: ['en-HK'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0612457898',
+ '+33612457898',
+ '33612457898',
+ '0712457898',
+ '+33712457898',
+ '33712457898',
+ ],
+ invalid: [
+ '061245789',
+ '06124578980',
+ '0112457898',
+ '0212457898',
+ '0312457898',
+ '0412457898',
+ '0512457898',
+ '0812457898',
+ '0912457898',
+ '+34612457898',
+ '+336124578980',
+ '+3361245789',
+ ],
+ args: ['fr-FR'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+306944848966',
+ '6944848966',
+ '306944848966',
+ ],
+ invalid: [
+ '2102323234',
+ '+302646041461',
+ '120000000',
+ '20000000000',
+ '68129485729',
+ '6589394827',
+ '298RI89572',
+ ],
+ args: ['el-GR'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '447789345856',
+ '+447861235675',
+ '07888814488',
+ ],
+ invalid: [
+ '67699567',
+ '0773894868',
+ '077389f8688',
+ '+07888814488',
+ '0152456999',
+ '442073456754',
+ '+443003434751',
+ '05073456754',
+ '08001123123',
+ ],
+ args: ['en-GB'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '19876543210',
+ '8005552222',
+ '+15673628910',
+ ],
+ invalid: [
+ '564785',
+ '0123456789',
+ '1437439210',
+ '8009112340',
+ '+10345672645',
+ '11435213543',
+ '2436119753',
+ '16532116190',
+ ],
+ args: ['en-US'],
+ });
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '19876543210',
+ '8005552222',
+ '+15673628910',
+ ],
+ invalid: [
+ '564785',
+ '0123456789',
+ '1437439210',
+ '8009112340',
+ '+10345672645',
+ '11435213543',
+ '2436119753',
+ '16532116190',
+ ],
+ args: ['en-CA'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0956684590',
+ '0966684590',
+ '0976684590',
+ '+260956684590',
+ '+260966684590',
+ '+260976684590',
+ '260976684590',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '010-38238383',
+ '966684590',
+ ],
+ args: ['en-ZM'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+79676338855',
+ '79676338855',
+ '89676338855',
+ '9676338855',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '010-38238383',
+ '+9676338855',
+ '19676338855',
+ '6676338855',
+ '+99676338855',
+ ],
+ args: ['ru-RU'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0640133338',
+ '063333133',
+ '0668888878',
+ '+381645678912',
+ '+381611314000',
+ '0655885010',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '010-38238383',
+ '+9676338855',
+ '19676338855',
+ '6676338855',
+ '+99676338855',
+ ],
+ args: ['sr-RS'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+6427987035',
+ '642240512347',
+ '0293981646',
+ '029968425',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '+642956696123566',
+ '+02119620856',
+ '+9676338855',
+ '19676338855',
+ '6676338855',
+ '+99676338855',
+ ],
+ args: ['en-NZ'],
+ });
+
+ var norwegian = {
+ valid: [
+ '+4796338855',
+ '+4746338855',
+ '4796338855',
+ '4746338855',
+ '46338855',
+ '96338855',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '+4676338855',
+ '19676338855',
+ '+4726338855',
+ '4736338855',
+ '66338855',
+ ],
+ };
+ test({
+ validator: 'isMobilePhone',
+ valid: norwegian.valid,
+ invalid: norwegian.invalid,
+ args: ['nb-NO'],
+ });
+ test({
+ validator: 'isMobilePhone',
+ valid: norwegian.valid,
+ invalid: norwegian.invalid,
+ args: ['nn-NO'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '01636012403',
+ '+841636012403',
+ '1636012403',
+ '841636012403',
+ '+84999999999',
+ '84999999999',
+ '0999999999',
+ '999999999',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '010-38238383',
+ '260976684590',
+ ],
+ args: ['vi-VN'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+34654789321',
+ '654789321',
+ '+34714789321',
+ '714789321',
+ '+34744789321',
+ '744789321',
+ ],
+ invalid: [
+ '12345',
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '+3465478932',
+ '65478932',
+ '+346547893210',
+ '6547893210',
+ '+34704789321',
+ '704789321',
+ '+34754789321',
+ '754789321',
+ ],
+ args: ['es-ES'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+48512689767',
+ '+48 56 376 87 47',
+ '56 566 78 46',
+ '657562855',
+ '+48657562855',
+ '+48 887472765',
+ '+48 56 6572724',
+ '+48 67 621 5461',
+ '48 67 621 5461',
+ ],
+ invalid: [
+ '+48 67 621 5461',
+ '+55657562855',
+ '3454535',
+ 'teststring',
+ '',
+ '1800-88-8687',
+ '+6019-5830837',
+ '357562855',
+ ],
+ args: ['pl-PL'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+358505557171',
+ '0455571',
+ '0505557171',
+ '358505557171',
+ '04412345',
+ '0457 123 45 67',
+ '+358457 123 45 67',
+ '+358 50 555 7171',
+ ],
+ invalid: [
+ '12345',
+ '',
+ '045557',
+ '045555717112312332423423421',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '010-38238383',
+ '+3-585-0555-7171',
+ '+9676338855',
+ '19676338855',
+ '6676338855',
+ '+99676338855',
+ '044123',
+ '019123456789012345678901',
+ ],
+ args: ['fi-FI'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+60128228789',
+ '+60195830837',
+ '+6019-5830837',
+ '+6019-5830837',
+ '0128737867',
+ '01468987837',
+ '016-2838768',
+ '016 2838768',
+ ],
+ invalid: [
+ '12345',
+ '601238788657',
+ '088387675',
+ '16-2838768',
+ '032551433',
+ '6088-387888',
+ '088-261987',
+ '1800-88-8687',
+ '088-320000',
+ ],
+ args: ['ms-MY'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0312345678',
+ '0721234567',
+ '09012345688',
+ '06 1234 5678',
+ '072 123 4567',
+ '0729 12 3456',
+ '07296 1 2345',
+ '072961 2345',
+ '090 1234 5678',
+ '03-1234-5678',
+ '+81312345678',
+ '+816-1234-5678',
+ '+8190-1234-5678',
+ ],
+ invalid: [
+ '12345',
+ '',
+ '045555717112312332423423421',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '+3-585-0555-7171',
+ '0 1234 5689',
+ '16 1234 5689',
+ '03_1234_5689',
+ ],
+ args: ['ja-JP'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '370 3175423',
+ '333202925',
+ '+39 310 7688449',
+ '+39 3339847632',
+ ],
+ invalid: [
+ '011 7387545',
+ '12345',
+ '+45 345 6782395',
+ ],
+ args: ['it-IT'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0470123456',
+ '+32470123456',
+ '32470123456',
+ '021234567',
+ '+3221234567',
+ '3221234567',
+ ],
+ invalid: [
+ '12345',
+ '+3212345',
+ '3212345',
+ '04701234567',
+ '+3204701234567',
+ '3204701234567',
+ '0212345678',
+ '+320212345678',
+ '320212345678',
+ ],
+ args: ['fr-BE'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0470123456',
+ '+32470123456',
+ '32470123456',
+ '021234567',
+ '+3221234567',
+ '3221234567',
+ ],
+ invalid: [
+ '12345',
+ '+3212345',
+ '3212345',
+ '04701234567',
+ '+3204701234567',
+ '3204701234567',
+ '0212345678',
+ '+320212345678',
+ '320212345678',
+ ],
+ args: ['nl-BE'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+40740123456',
+ '+40 740123456',
+ '+40740 123 456',
+ '+40740.123.456',
+ '+40740-123-456',
+ '40740123456',
+ '40 740123456',
+ '40740 123 456',
+ '40740.123.456',
+ '40740-123-456',
+ '0740123456',
+ '0740/123456',
+ '0740 123 456',
+ '0740.123.456',
+ '0740-123-456',
+ ],
+ invalid: [
+ '',
+ 'Vml2YW11cyBmZXJtZtesting123',
+ '123456',
+ '740123456',
+ '+40640123456',
+ '+40210123456',
+ ],
+ args: ['ro-RO'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '0217123456',
+ '0811 778 998',
+ '089931236181900',
+ '622178878890',
+ '62811 778 998',
+ '62811778998',
+ '6289931236181900',
+ '6221 740123456',
+ '62899 740123456',
+ '62899 7401 2346',
+ '0341 8123456',
+ '0778 89800910',
+ '0741 123456',
+ '+6221740123456',
+ '+62811 778 998',
+ '+62811778998',
+ ],
+ invalid: [
+ '+65740 123 456',
+ '',
+ 'ASDFGJKLmZXJtZtesting123',
+ '123456',
+ '740123456',
+ '+65640123456',
+ '+64210123456',
+ ],
+ args: ['id-ID'],
+ });
+
+ test({
+ validator: 'isMobilePhone',
+ valid: [
+ '+37051234567',
+ '851234567',
+ ],
+ invalid: [
+ '+65740 123 456',
+ '',
+ 'ASDFGJKLmZXJtZtesting123',
+ '123456',
+ '740123456',
+ '+65640123456',
+ '+64210123456',
+ ],
+ args: ['lt-LT'],
+ });
+ });
+
+ it('should validate currency', function () {
+ test({
+ validator: 'isCurrency',
+ args: [
+ { },
+ '-$##,###.## (en-US, en-CA, en-AU, en-NZ, en-HK)',
+ ],
+ valid: [
+ '-$10,123.45',
+ '$10,123.45',
+ '$10123.45',
+ '10,123.45',
+ '10123.45',
+ '10,123',
+ '1,123,456',
+ '1123456',
+ '1.39',
+ '.03',
+ '0.10',
+ '$0.10',
+ '-$0.01',
+ '-$.99',
+ '$100,234,567.89',
+ '$10,123',
+ '10,123',
+ '-10123',
+ ],
+ invalid: [
+ '1.234',
+ '$1.1',
+ '$ 32.50',
+ '500$',
+ '.0001',
+ '$.001',
+ '$0.001',
+ '12,34.56',
+ '123456,123,123456',
+ '123,4',
+ ',123',
+ '$-,123',
+ '$',
+ '.',
+ ',',
+ '00',
+ '$-',
+ '$-,.',
+ '-',
+ '-$',
+ '',
+ '- $',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ require_symbol: true,
+ },
+ '-$##,###.## with $ required (en-US, en-CA, en-AU, en-NZ, en-HK)',
+ ],
+ valid: [
+ '-$10,123.45',
+ '$10,123.45',
+ '$10123.45',
+ '$10,123.45',
+ '$10,123',
+ '$1,123,456',
+ '$1123456',
+ '$1.39',
+ '$.03',
+ '$0.10',
+ '$0.10',
+ '-$0.01',
+ '-$.99',
+ '$100,234,567.89',
+ '$10,123',
+ '-$10123',
+ ],
+ invalid: [
+ '1.234',
+ '$1.234',
+ '1.1',
+ '$1.1',
+ '$ 32.50',
+ ' 32.50',
+ '500',
+ '10,123,456',
+ '.0001',
+ '$.001',
+ '$0.001',
+ '1,234.56',
+ '123456,123,123456',
+ '$123456,123,123456',
+ '123.4',
+ '$123.4',
+ ',123',
+ '$,123',
+ '$-,123',
+ '$',
+ '.',
+ '$.',
+ ',',
+ '$,',
+ '00',
+ '$00',
+ '$-',
+ '$-,.',
+ '-',
+ '-$',
+ '',
+ '$ ',
+ '- $',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: '¥',
+ negative_sign_before_digits: true,
+ },
+ '¥-##,###.## (zh-CN)',
+ ],
+ valid: [
+ '123,456.78',
+ '-123,456.78',
+ '¥6,954,231',
+ '¥-6,954,231',
+ '¥10.03',
+ '¥-10.03',
+ '10.03',
+ '1.39',
+ '.03',
+ '0.10',
+ '¥-10567.01',
+ '¥0.01',
+ '¥1,234,567.89',
+ '¥10,123',
+ '¥-10,123',
+ '¥-10,123.45',
+ '10,123',
+ '10123',
+ '¥-100',
+ ],
+ invalid: [
+ '1.234',
+ '¥1.1',
+ '5,00',
+ '.0001',
+ '¥.001',
+ '¥0.001',
+ '12,34.56',
+ '123456,123,123456',
+ '123 456',
+ ',123',
+ '¥-,123',
+ '',
+ ' ',
+ '¥',
+ '¥-',
+ '¥-,.',
+ '-',
+ '- ¥',
+ '-¥',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: '¥',
+ allow_negatives: false,
+ },
+ '¥##,###.## with no negatives (zh-CN)',
+ ],
+ valid: [
+ '123,456.78',
+ '¥6,954,231',
+ '¥10.03',
+ '10.03',
+ '1.39',
+ '.03',
+ '0.10',
+ '¥0.01',
+ '¥1,234,567.89',
+ '¥10,123',
+ '10,123',
+ '10123',
+ '¥100',
+ ],
+ invalid: [
+ '1.234',
+ '-123,456.78',
+ '¥-6,954,231',
+ '¥-10.03',
+ '¥-10567.01',
+ '¥1.1',
+ '¥-10,123',
+ '¥-10,123.45',
+ '5,00',
+ '¥-100',
+ '.0001',
+ '¥.001',
+ '¥-.001',
+ '¥0.001',
+ '12,34.56',
+ '123456,123,123456',
+ '123 456',
+ ',123',
+ '¥-,123',
+ '',
+ ' ',
+ '¥',
+ '¥-',
+ '¥-,.',
+ '-',
+ '- ¥',
+ '-¥',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: 'R',
+ negative_sign_before_digits: true,
+ thousands_separator: ' ',
+ decimal_separator: ',',
+ allow_negative_sign_placeholder: true,
+ },
+ 'R ## ###,## and R-10 123,25 (el-ZA)',
+ ],
+ valid: [
+ '123 456,78',
+ '-10 123',
+ 'R-10 123',
+ 'R 6 954 231',
+ 'R10,03',
+ '10,03',
+ '1,39',
+ ',03',
+ '0,10',
+ 'R10567,01',
+ 'R0,01',
+ 'R1 234 567,89',
+ 'R10 123',
+ 'R 10 123',
+ 'R 10123',
+ 'R-10123',
+ '10 123',
+ '10123',
+ ],
+ invalid: [
+ '1,234',
+ 'R -10123',
+ 'R- 10123',
+ 'R,1',
+ ',0001',
+ 'R,001',
+ 'R0,001',
+ '12 34,56',
+ '123456 123 123456',
+ ' 123',
+ '- 123',
+ '123 ',
+ '',
+ ' ',
+ 'R',
+ 'R- .1',
+ 'R-',
+ '-',
+ '-R 10123',
+ 'R00',
+ 'R -',
+ '-R',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: '€',
+ thousands_separator: '.',
+ decimal_separator: ',',
+ allow_space_after_symbol: true,
+ },
+ '-€ ##.###,## (it-IT)',
+ ],
+ valid: [
+ '123.456,78',
+ '-123.456,78',
+ '€6.954.231',
+ '-€6.954.231',
+ '€ 896.954.231',
+ '-€ 896.954.231',
+ '16.954.231',
+ '-16.954.231',
+ '€10,03',
+ '-€10,03',
+ '10,03',
+ '-10,03',
+ '-1,39',
+ ',03',
+ '0,10',
+ '-€10567,01',
+ '-€ 10567,01',
+ '€ 0,01',
+ '€1.234.567,89',
+ '€10.123',
+ '10.123',
+ '-€10.123',
+ '€ 10.123',
+ '€10.123',
+ '€ 10123',
+ '10.123',
+ '-10123',
+ ],
+ invalid: [
+ '1,234',
+ '€ 1,1',
+ '50#,50',
+ '123,@€ ',
+ '€€500',
+ ',0001',
+ '€ ,001',
+ '€0,001',
+ '12.34,56',
+ '123456.123.123456',
+ '€123€',
+ '',
+ ' ',
+ '€',
+ ' €',
+ '€ ',
+ '€€',
+ ' 123',
+ '- 123',
+ '.123',
+ '-€.123',
+ '123 ',
+ '€-',
+ '- €',
+ '€ - ',
+ '-',
+ '- ',
+ '-€',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: '€',
+ thousands_separator: '.',
+ symbol_after_digits: true,
+ decimal_separator: ',',
+ allow_space_after_digits: true,
+ },
+ '-##.###,## € (el-GR)',
+ ],
+ valid: [
+ '123.456,78',
+ '-123.456,78',
+ '6.954.231 €',
+ '-6.954.231 €',
+ '896.954.231',
+ '-896.954.231',
+ '16.954.231',
+ '-16.954.231',
+ '10,03€',
+ '-10,03€',
+ '10,03',
+ '-10,03',
+ '1,39',
+ ',03',
+ '-,03',
+ '-,03 €',
+ '-,03€',
+ '0,10',
+ '10567,01€',
+ '0,01 €',
+ '1.234.567,89€',
+ '10.123€',
+ '10.123',
+ '10.123€',
+ '10.123 €',
+ '10123 €',
+ '10.123',
+ '10123',
+ ],
+ invalid: [
+ '1,234',
+ '1,1 €',
+ ',0001',
+ ',001 €',
+ '0,001€',
+ '12.34,56',
+ '123456.123.123456',
+ '€123€',
+ '',
+ ' ',
+ '€',
+ ' €',
+ '€ ',
+ ' 123',
+ '- 123',
+ '.123',
+ '-.123€',
+ '-.123 €',
+ '123 ',
+ '-€',
+ '- €',
+ '-',
+ '- ',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: 'kr.',
+ negative_sign_before_digits: true,
+ thousands_separator: '.',
+ decimal_separator: ',',
+ allow_space_after_symbol: true,
+ },
+ 'kr. -##.###,## (da-DK)',
+ ],
+ valid: [
+ '123.456,78',
+ '-10.123',
+ 'kr. -10.123',
+ 'kr.-10.123',
+ 'kr. 6.954.231',
+ 'kr.10,03',
+ 'kr. -10,03',
+ '10,03',
+ '1,39',
+ ',03',
+ '0,10',
+ 'kr. 10567,01',
+ 'kr. 0,01',
+ 'kr. 1.234.567,89',
+ 'kr. -1.234.567,89',
+ '10.123',
+ 'kr. 10.123',
+ 'kr.10.123',
+ '10123',
+ '10.123',
+ 'kr.-10123',
+ ],
+ invalid: [
+ '1,234',
+ 'kr. -10123',
+ 'kr.,1',
+ ',0001',
+ 'kr. ,001',
+ 'kr.0,001',
+ '12.34,56',
+ '123456.123.123456',
+ '.123',
+ 'kr.-.123',
+ 'kr. -.123',
+ '- 123',
+ '123 ',
+ '',
+ ' ',
+ 'kr.',
+ ' kr.',
+ 'kr. ',
+ 'kr.-',
+ 'kr. -',
+ 'kr. - ',
+ ' - ',
+ '-',
+ '- kr.',
+ '-kr.',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ symbol: 'kr.',
+ allow_negatives: false,
+ negative_sign_before_digits: true,
+ thousands_separator: '.',
+ decimal_separator: ',',
+ allow_space_after_symbol: true,
+ },
+ 'kr. ##.###,## with no negatives (da-DK)',
+ ],
+ valid: [
+ '123.456,78',
+ '10.123',
+ 'kr. 10.123',
+ 'kr.10.123',
+ 'kr. 6.954.231',
+ 'kr.10,03',
+ 'kr. 10,03',
+ '10,03',
+ '1,39',
+ ',03',
+ '0,10',
+ 'kr. 10567,01',
+ 'kr. 0,01',
+ 'kr. 1.234.567,89',
+ 'kr.1.234.567,89',
+ '10.123',
+ 'kr. 10.123',
+ 'kr.10.123',
+ '10123',
+ '10.123',
+ 'kr.10123',
+ ],
+ invalid: [
+ '1,234',
+ '-10.123',
+ 'kr. -10.123',
+ 'kr. -1.234.567,89',
+ 'kr.-10123',
+ 'kr. -10123',
+ 'kr.-10.123',
+ 'kr. -10,03',
+ 'kr.,1',
+ ',0001',
+ 'kr. ,001',
+ 'kr.0,001',
+ '12.34,56',
+ '123456.123.123456',
+ '.123',
+ 'kr.-.123',
+ 'kr. -.123',
+ '- 123',
+ '123 ',
+ '',
+ ' ',
+ 'kr.',
+ ' kr.',
+ 'kr. ',
+ 'kr.-',
+ 'kr. -',
+ 'kr. - ',
+ ' - ',
+ '-',
+ '- kr.',
+ '-kr.',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ {
+ parens_for_negatives: true,
+ },
+ '($##,###.##) (en-US, en-HK)',
+ ],
+ valid: [
+ '1,234',
+ '(1,234)',
+ '($6,954,231)',
+ '$10.03',
+ '(10.03)',
+ '($10.03)',
+ '1.39',
+ '.03',
+ '(.03)',
+ '($.03)',
+ '0.10',
+ '$10567.01',
+ '($0.01)',
+ '$1,234,567.89',
+ '$10,123',
+ '(10,123)',
+ '10123',
+ ],
+ invalid: [
+ '1.234',
+ '($1.1)',
+ '-$1.10',
+ '$ 32.50',
+ '500$',
+ '.0001',
+ '$.001',
+ '($0.001)',
+ '12,34.56',
+ '123456,123,123456',
+ '( 123)',
+ ',123',
+ '$-,123',
+ '',
+ ' ',
+ ' ',
+ ' ',
+ '$',
+ '$ ',
+ ' $',
+ ' 123',
+ '(123) ',
+ '.',
+ ',',
+ '00',
+ '$-',
+ '$ - ',
+ '$- ',
+ ' - ',
+ '-',
+ '- $',
+ '-$',
+ '()',
+ '( )',
+ '( -)',
+ '( - )',
+ '( - )',
+ '(-)',
+ '(-$)',
+ ],
+ });
+
+ test({
+ validator: 'isCurrency',
+ args: [
+ { allow_negatives: false },
+ '$##,###.## with no negatives (en-US, en-CA, en-AU, en-HK)',
+ ],
+ valid: [
+ '$10,123.45',
+ '$10123.45',
+ '10,123.45',
+ '10123.45',
+ '10,123',
+ '1,123,456',
+ '1123456',
+ '1.39',
+ '.03',
+ '0.10',
+ '$0.10',
+ '$100,234,567.89',
+ '$10,123',
+ '10,123',
+ ],
+ invalid: [
+ '1.234',
+ '-1.234',
+ '-10123',
+ '-$0.01',
+ '-$.99',
+ '$1.1',
+ '-$1.1',
+ '$ 32.50',
+ '500$',
+ '.0001',
+ '$.001',
+ '$0.001',
+ '12,34.56',
+ '123456,123,123456',
+ '-123456,123,123456',
+ '123,4',
+ ',123',
+ '$-,123',
+ '$',
+ '.',
+ ',',
+ '00',
+ '$-',
+ '$-,.',
+ '-',
+ '-$',
+ '',
+ '- $',
+ '-$10,123.45',
+ ],
+ });
+
+ test({
+ validator: 'isBoolean',
+ valid: [
+ 'true',
+ 'false',
+ '0',
+ '1',
+ ],
+ invalid: [
+ '1.0',
+ '0.0',
+ 'true ',
+ 'False',
+ 'True',
+ 'yes',
+ ],
+ });
+ });
+
+ it('should validate ISO 8601 dates', function () {
+ // from http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
+ test({
+ validator: 'isISO8601',
+ valid: [
+ '2009-12T12:34',
+ '2009',
+ '2009-05-19',
+ '2009-05-19',
+ '20090519',
+ '2009123',
+ '2009-05',
+ '2009-123',
+ '2009-222',
+ '2009-001',
+ '2009-W01-1',
+ '2009-W51-1',
+ '2009-W511',
+ '2009-W33',
+ '2009W511',
+ '2009-05-19',
+ '2009-05-19 00:00',
+ '2009-05-19 14',
+ '2009-05-19 14:31',
+ '2009-05-19 14:39:22',
+ '2009-05-19T14:39Z',
+ '2009-W21-2',
+ '2009-W21-2T01:22',
+ '2009-139',
+ '2009-05-19 14:39:22-06:00',
+ '2009-05-19 14:39:22+0600',
+ '2009-05-19 14:39:22-01',
+ '20090621T0545Z',
+ '2007-04-06T00:00',
+ '2007-04-05T24:00',
+ '2010-02-18T16:23:48.5',
+ '2010-02-18T16:23:48,444',
+ '2010-02-18T16:23:48,3-06:00',
+ '2010-02-18T16:23.4',
+ '2010-02-18T16:23,25',
+ '2010-02-18T16:23.33+0600',
+ '2010-02-18T16.23334444',
+ '2010-02-18T16,2283',
+ '2009-05-19 143922.500',
+ '2009-05-19 1439,55',
+ ],
+ invalid: [
+ '200905',
+ '2009367',
+ '2009-',
+ '2007-04-05T24:50',
+ '2009-000',
+ '2009-M511',
+ '2009M511',
+ '2009-05-19T14a39r',
+ '2009-05-19T14:3924',
+ '2009-0519',
+ '2009-05-1914:39',
+ '2009-05-19 14:',
+ '2009-05-19r14:39',
+ '2009-05-19 14a39a22',
+ '200912-01',
+ '2009-05-19 14:39:22+06a00',
+ '2009-05-19 146922.500',
+ '2010-02-18T16.5:23.35:48',
+ '2010-02-18T16:23.35:48',
+ '2010-02-18T16:23.35:48.45',
+ '2009-05-19 14.5.44',
+ '2010-02-18T16:23.33.600',
+ '2010-02-18T16,25:23:48,444',
+ ],
+ });
+ });
+
+ it('should validate whitelisted characters', function () {
+ test({
+ validator: 'isWhitelisted',
+ args: ['abcdefghijklmnopqrstuvwxyz-'],
+ valid: ['foo', 'foobar', 'baz-foo'],
+ invalid: ['foo bar', 'fo.bar', 'türkçe'],
+ });
+ });
+
+ it('should error on non-string input', function () {
+ var empty = [undefined, null, [], NaN];
+ empty.forEach(function (item) {
+ assert.throws(validator.isEmpty.bind(null, item));
+ });
+ });
+
+ it('should validate dataURI', function () {
+ /* eslint-disable max-len */
+ test({
+ validator: 'isDataURI',
+ valid: [
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC',
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAFVBMVEXk5OTn5+ft7e319fX29vb5+fn///++GUmVAAAALUlEQVQIHWNICnYLZnALTgpmMGYIFWYIZTA2ZFAzTTFlSDFVMwVyQhmAwsYMAKDaBy0axX/iAAAAAElFTkSuQmCC',
+ ' data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAFVBMVEXk5OTn5+ft7e319fX29vb5+fn///++GUmVAAAALUlEQVQIHWNICnYLZnALTgpmMGYIFWYIZTA2ZFAzTTFlSDFVMwVyQhmAwsYMAKDaBy0axX/iAAAAAElFTkSuQmCC ',
+ 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20fill%3D%22%2300B1FF%22%20width%3D%22100%22%20height%3D%22100%22%2F%3E%3C%2Fsvg%3E',
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCBmaWxsPSIjMDBCMUZGIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIvPjwvc3ZnPg==',
+ ' data:,Hello%2C%20World!',
+ ' data:,Hello World!',
+ ' data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D',
+ ' data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E',
+ 'data:,A%20brief%20note',
+ 'data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E',
+ ],
+ invalid: [
+ 'dataxbase64',
+ 'data:HelloWorld',
+ 'data:text/html;charset=,%3Ch1%3EHello!%3C%2Fh1%3E',
+ 'data:text/html;charset,%3Ch1%3EHello!%3C%2Fh1%3E', 'data:base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC',
+ '',
+ 'http://wikipedia.org',
+ 'base64',
+ 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC',
+ ],
+ });
+ /* eslint-enable max-len */
+ });
+ });
+}
diff --git a/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/xpcshell.toml b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..d877b73bff
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = "devtools"
+head = "head_stringvalidator.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_sanitizers.js"]
+
+["test_validators.js"]
diff --git a/devtools/shared/storage/vendor/stringvalidator/util/assert.js b/devtools/shared/storage/vendor/stringvalidator/util/assert.js
new file mode 100644
index 0000000000..3bd796aebd
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/util/assert.js
@@ -0,0 +1,215 @@
+// // based on node assert, original notice:
+// // NB: The URL to the CommonJS spec is kept just for tradition.
+// // node-assert has evolved a lot since then, both in API and behavior.
+//
+// // http://wiki.commonjs.org/wiki/Unit_Testing/1.0
+// //
+// // THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!
+// //
+// // Originally from narwhal.js (http://narwhaljs.org)
+// // Copyright (c) 2009 Thomas Robinson <280north.com>
+// //
+// // Permission is hereby granted, free of charge, to any person obtaining a copy
+// // of this software and associated documentation files (the 'Software'), to
+// // deal in the Software without restriction, including without limitation the
+// // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// // sell copies of the Software, and to permit persons to whom the Software is
+// // furnished to do so, subject to the following conditions:
+// //
+// // The above copyright notice and this permission notice shall be included in
+// // all copies or substantial portions of the Software.
+// //
+// // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// // AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+// // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"use strict";
+
+var regex = /\s*function\s+([^\(\s]*)\s*/;
+
+var functionsHaveNames = (function () {
+ return function foo() {}.name === "foo";
+}());
+
+function assert(value, message) {
+ if (!value) {
+ fail(value, true, message, "==", assert.ok);
+ }
+}
+
+assert.equal = function equal(actual, expected, message) {
+ if (actual != expected) {
+ fail(actual, expected, message, "==", assert.equal);
+ }
+};
+
+assert.throws = function (block, error, message) {
+ _throws(true, block, error, message);
+};
+
+function _throws(shouldThrow, block, expected, message) {
+ var actual;
+
+ if (typeof block !== "function") {
+ throw new TypeError(`"block" argument must be a function`);
+ }
+
+ if (typeof expected === "string") {
+ message = expected;
+ expected = null;
+ }
+
+ actual = _tryBlock(block);
+
+ message = (expected?.name ? " (" + expected.name + ")." : ".") +
+ (message ? " " + message : ".");
+
+ if (shouldThrow && !actual) {
+ fail(actual, expected, "Missing expected exception" + message);
+ }
+
+ var userProvidedMessage = typeof message === "string";
+ var isUnwantedException = !shouldThrow && isError(actual);
+ var isUnexpectedException = !shouldThrow && actual && !expected;
+
+ if ((isUnwantedException &&
+ userProvidedMessage &&
+ expectedException(actual, expected)) ||
+ isUnexpectedException) {
+ fail(actual, expected, "Got unwanted exception" + message);
+ }
+
+ if ((shouldThrow && actual && expected &&
+ !expectedException(actual, expected)) || (!shouldThrow && actual)) {
+ throw actual;
+ }
+}
+
+function fail(actual, expected, message, operator, stackStartFunction) {
+ throw new assert.AssertionError({
+ message: message,
+ actual: actual,
+ expected: expected,
+ operator: operator,
+ stackStartFunction: stackStartFunction
+ });
+}
+
+assert.fail = fail;
+
+assert.AssertionError = function AssertionError(options) {
+ this.name = "AssertionError";
+ this.actual = options.actual;
+ this.expected = options.expected;
+ this.operator = options.operator;
+ if (options.message) {
+ this.message = options.message;
+ this.generatedMessage = false;
+ } else {
+ this.message = getMessage(this);
+ this.generatedMessage = true;
+ }
+ var stackStartFunction = options.stackStartFunction || fail;
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, stackStartFunction);
+ } else {
+ // non v8 browsers so we can have a stacktrace
+ var err = new Error();
+ if (err.stack) {
+ var out = err.stack;
+
+ // try to strip useless frames
+ var fn_name = getName(stackStartFunction);
+ var idx = out.indexOf("\n" + fn_name);
+ if (idx >= 0) {
+ // once we have located the function frame
+ // we need to strip out everything before it (and its line)
+ var next_line = out.indexOf("\n", idx + 1);
+ out = out.substring(next_line + 1);
+ }
+
+ this.stack = out;
+ }
+ }
+};
+
+function expectedException(actual, expected) {
+ if (!actual || !expected) {
+ return false;
+ }
+
+ if (Object.prototype.toString.call(expected) == "[object RegExp]") {
+ return expected.test(actual);
+ }
+
+ try {
+ if (actual instanceof expected) {
+ return true;
+ }
+ } catch (e) {
+ // Ignore. The instanceof check doesn"t work for arrow functions.
+ }
+
+ if (Error.isPrototypeOf(expected)) {
+ return false;
+ }
+
+ return expected.call({}, actual) === true;
+}
+
+function _tryBlock(block) {
+ var error;
+ try {
+ block();
+ } catch (e) {
+ error = e;
+ }
+ return error;
+}
+
+function isError(obj) {
+ return obj instanceof Error;
+}
+
+function isFunction(value) {
+ return typeof value === "function";
+}
+
+function getMessage(self) {
+ return truncate(inspect(self.actual), 128) + " " +
+ self.operator + " " +
+ truncate(inspect(self.expected), 128);
+}
+
+function getName(func) {
+ if (!isFunction(func)) {
+ return null;
+ }
+ if (functionsHaveNames) {
+ return func.name;
+ }
+ var str = func.toString();
+ var match = str.match(regex);
+ return match?.[1];
+}
+
+function truncate(s, n) {
+ if (typeof s === "string") {
+ return s.length < n ? s : s.slice(0, n);
+ }
+ return s;
+}
+
+function inspect(something) {
+ if (functionsHaveNames || !isFunction(something)) {
+ throw new Error(something);
+ }
+ var rawname = getName(something);
+ var name = rawname ? ": " + rawname : "";
+ return "[Function" + name + "]";
+}
+
+exports.assert = assert;
diff --git a/devtools/shared/storage/vendor/stringvalidator/util/moz.build b/devtools/shared/storage/vendor/stringvalidator/util/moz.build
new file mode 100644
index 0000000000..cc8fbcfab1
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/util/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ 'assert.js',
+)
diff --git a/devtools/shared/storage/vendor/stringvalidator/validator.js b/devtools/shared/storage/vendor/stringvalidator/validator.js
new file mode 100644
index 0000000000..e11886599b
--- /dev/null
+++ b/devtools/shared/storage/vendor/stringvalidator/validator.js
@@ -0,0 +1,1489 @@
+/*
+ * Copyright (c) 2016 Chris O"Hara <cohara87@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * NOTE: This utility is derived from https://github.com/chriso/validator.js but it is
+ * **NOT** the same as the original. We have made the following changes:
+ * - Changed mocha tests to xpcshell based tests.
+ * - Merged the following pull requests:
+ * - [isMobileNumber] Added Lithuanian number pattern #667
+ * - Hongkong mobile number #665
+ * - Added option to validate any phone locale #663
+ * - Added validation for ISRC strings #660
+ * - Added isRFC5646 for rfc 5646 #572
+ * - Added isSemVer for version numbers.
+ * - Added isRGBColor for RGB colors.
+ *
+ * UPDATING: PLEASE FOLLOW THE INSTRUCTIONS INSIDE UPDATING.md
+ */
+
+"use strict";
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.validator = factory());
+}(this, function () { 'use strict';
+
+ function assertString(input) {
+ if (typeof input !== 'string') {
+ throw new TypeError('This library (validator.js) validates strings only');
+ }
+ }
+
+ function toDate(date) {
+ assertString(date);
+ date = Date.parse(date);
+ return !isNaN(date) ? new Date(date) : null;
+ }
+
+ function toFloat(str) {
+ assertString(str);
+ return parseFloat(str);
+ }
+
+ function toInt(str, radix) {
+ assertString(str);
+ return parseInt(str, radix || 10);
+ }
+
+ function toBoolean(str, strict) {
+ assertString(str);
+ if (strict) {
+ return str === '1' || str === 'true';
+ }
+ return str !== '0' && str !== 'false' && str !== '';
+ }
+
+ function equals(str, comparison) {
+ assertString(str);
+ return str === comparison;
+ }
+
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ return typeof obj;
+ } : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+ };
+
+ var asyncGenerator = function () {
+ function AwaitValue(value) {
+ this.value = value;
+ }
+
+ function AsyncGenerator(gen) {
+ var front, back;
+
+ function send(key, arg) {
+ return new Promise(function (resolve, reject) {
+ var request = {
+ key: key,
+ arg: arg,
+ resolve: resolve,
+ reject: reject,
+ next: null
+ };
+
+ if (back) {
+ back = back.next = request;
+ } else {
+ front = back = request;
+ resume(key, arg);
+ }
+ });
+ }
+
+ function resume(key, arg) {
+ try {
+ var result = gen[key](arg);
+ var value = result.value;
+
+ if (value instanceof AwaitValue) {
+ Promise.resolve(value.value).then(function (arg) {
+ resume("next", arg);
+ }, function (arg) {
+ resume("throw", arg);
+ });
+ } else {
+ settle(result.done ? "return" : "normal", result.value);
+ }
+ } catch (err) {
+ settle("throw", err);
+ }
+ }
+
+ function settle(type, value) {
+ switch (type) {
+ case "return":
+ front.resolve({
+ value: value,
+ done: true
+ });
+ break;
+
+ case "throw":
+ front.reject(value);
+ break;
+
+ default:
+ front.resolve({
+ value: value,
+ done: false
+ });
+ break;
+ }
+
+ front = front.next;
+
+ if (front) {
+ resume(front.key, front.arg);
+ } else {
+ back = null;
+ }
+ }
+
+ this._invoke = send;
+
+ if (typeof gen.return !== "function") {
+ this.return = undefined;
+ }
+ }
+
+ if (typeof Symbol === "function" && Symbol.asyncIterator) {
+ AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
+ return this;
+ };
+ }
+
+ AsyncGenerator.prototype.next = function (arg) {
+ return this._invoke("next", arg);
+ };
+
+ AsyncGenerator.prototype.throw = function (arg) {
+ return this._invoke("throw", arg);
+ };
+
+ AsyncGenerator.prototype.return = function (arg) {
+ return this._invoke("return", arg);
+ };
+
+ return {
+ wrap: function (fn) {
+ return function () {
+ return new AsyncGenerator(fn.apply(this, arguments));
+ };
+ },
+ await: function (value) {
+ return new AwaitValue(value);
+ }
+ };
+ }();
+
+ function toString(input) {
+ if ((typeof input === 'undefined' ? 'undefined' : _typeof(input)) === 'object' && input !== null) {
+ if (typeof input.toString === 'function') {
+ input = input.toString();
+ } else {
+ input = '[object Object]';
+ }
+ } else if (input === null || typeof input === 'undefined' || isNaN(input) && !input.length) {
+ input = '';
+ }
+ return String(input);
+ }
+
+ function contains(str, elem) {
+ assertString(str);
+ return str.indexOf(toString(elem)) >= 0;
+ }
+
+ function matches(str, pattern, modifiers) {
+ assertString(str);
+ if (Object.prototype.toString.call(pattern) !== '[object RegExp]') {
+ pattern = new RegExp(pattern, modifiers);
+ }
+ return pattern.test(str);
+ }
+
+ function merge() {
+ var obj = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ var defaults = arguments[1];
+
+ for (var key in defaults) {
+ if (typeof obj[key] === 'undefined') {
+ obj[key] = defaults[key];
+ }
+ }
+ return obj;
+ }
+
+ /* eslint-disable prefer-rest-params */
+ function isByteLength(str, options) {
+ assertString(str);
+ var min = void 0;
+ var max = void 0;
+ if ((typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object') {
+ min = options.min || 0;
+ max = options.max;
+ } else {
+ // backwards compatibility: isByteLength(str, min [, max])
+ min = arguments[1];
+ max = arguments[2];
+ }
+ var len = encodeURI(str).split(/%..|./).length - 1;
+ return len >= min && (typeof max === 'undefined' || len <= max);
+ }
+
+ var default_fqdn_options = {
+ require_tld: true,
+ allow_underscores: false,
+ allow_trailing_dot: false
+ };
+
+ function isFDQN(str, options) {
+ assertString(str);
+ options = merge(options, default_fqdn_options);
+
+ /* Remove the optional trailing dot before checking validity */
+ if (options.allow_trailing_dot && str[str.length - 1] === '.') {
+ str = str.substring(0, str.length - 1);
+ }
+ var parts = str.split('.');
+ if (options.require_tld) {
+ var tld = parts.pop();
+ if (!parts.length || !/^([a-z\u00a1-\uffff]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)) {
+ return false;
+ }
+ }
+ for (var part, i = 0; i < parts.length; i++) {
+ part = parts[i];
+ if (options.allow_underscores) {
+ part = part.replace(/_/g, '');
+ }
+ if (!/^[a-z\u00a1-\uffff0-9-]+$/i.test(part)) {
+ return false;
+ }
+ if (/[\uff01-\uff5e]/.test(part)) {
+ // disallow full-width chars
+ return false;
+ }
+ if (part[0] === '-' || part[part.length - 1] === '-') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var default_email_options = {
+ allow_display_name: false,
+ require_display_name: false,
+ allow_utf8_local_part: true,
+ require_tld: true
+ };
+
+ /* eslint-disable max-len */
+ /* eslint-disable no-control-regex */
+ var displayName = /^[a-z\d!#\$%&'\*\+\-\/=\?\^_`{\|}~\.\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+[a-z\d!#\$%&'\*\+\-\/=\?\^_`{\|}~\.\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\s]*<(.+)>$/i;
+ var emailUserPart = /^[a-z\d!#\$%&'\*\+\-\/=\?\^_`{\|}~]+$/i;
+ var quotedEmailUser = /^([\s\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e]|(\\[\x01-\x09\x0b\x0c\x0d-\x7f]))*$/i;
+ var emailUserUtf8Part = /^[a-z\d!#\$%&'\*\+\-\/=\?\^_`{\|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+$/i;
+ var quotedEmailUserUtf8 = /^([\s\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|(\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*$/i;
+ /* eslint-enable max-len */
+ /* eslint-enable no-control-regex */
+
+ function isEmail(str, options) {
+ assertString(str);
+ options = merge(options, default_email_options);
+
+ if (options.require_display_name || options.allow_display_name) {
+ var display_email = str.match(displayName);
+ if (display_email) {
+ str = display_email[1];
+ } else if (options.require_display_name) {
+ return false;
+ }
+ }
+
+ var parts = str.split('@');
+ var domain = parts.pop();
+ var user = parts.join('@');
+
+ var lower_domain = domain.toLowerCase();
+ if (lower_domain === 'gmail.com' || lower_domain === 'googlemail.com') {
+ user = user.replace(/\./g, '').toLowerCase();
+ }
+
+ if (!isByteLength(user, { max: 64 }) || !isByteLength(domain, { max: 256 })) {
+ return false;
+ }
+
+ if (!isFDQN(domain, { require_tld: options.require_tld })) {
+ return false;
+ }
+
+ if (user[0] === '"') {
+ user = user.slice(1, user.length - 1);
+ return options.allow_utf8_local_part ? quotedEmailUserUtf8.test(user) : quotedEmailUser.test(user);
+ }
+
+ var pattern = options.allow_utf8_local_part ? emailUserUtf8Part : emailUserPart;
+
+ var user_parts = user.split('.');
+ for (var i = 0; i < user_parts.length; i++) {
+ if (!pattern.test(user_parts[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ var ipv4Maybe = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+ var ipv6Block = /^[0-9A-F]{1,4}$/i;
+
+ function isIP(str) {
+ var version = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+
+ assertString(str);
+ version = String(version);
+ if (!version) {
+ return isIP(str, 4) || isIP(str, 6);
+ } else if (version === '4') {
+ if (!ipv4Maybe.test(str)) {
+ return false;
+ }
+ var parts = str.split('.').sort(function (a, b) {
+ return a - b;
+ });
+ return parts[3] <= 255;
+ } else if (version === '6') {
+ var blocks = str.split(':');
+ var foundOmissionBlock = false; // marker to indicate ::
+
+ // At least some OS accept the last 32 bits of an IPv6 address
+ // (i.e. 2 of the blocks) in IPv4 notation, and RFC 3493 says
+ // that '::ffff:a.b.c.d' is valid for IPv4-mapped IPv6 addresses,
+ // and '::a.b.c.d' is deprecated, but also valid.
+ var foundIPv4TransitionBlock = isIP(blocks[blocks.length - 1], 4);
+ var expectedNumberOfBlocks = foundIPv4TransitionBlock ? 7 : 8;
+
+ if (blocks.length > expectedNumberOfBlocks) {
+ return false;
+ }
+ // initial or final ::
+ if (str === '::') {
+ return true;
+ } else if (str.substr(0, 2) === '::') {
+ blocks.shift();
+ blocks.shift();
+ foundOmissionBlock = true;
+ } else if (str.substr(str.length - 2) === '::') {
+ blocks.pop();
+ blocks.pop();
+ foundOmissionBlock = true;
+ }
+
+ for (var i = 0; i < blocks.length; ++i) {
+ // test for a :: which can not be at the string start/end
+ // since those cases have been handled above
+ if (blocks[i] === '' && i > 0 && i < blocks.length - 1) {
+ if (foundOmissionBlock) {
+ return false; // multiple :: in address
+ }
+ foundOmissionBlock = true;
+ } else if (foundIPv4TransitionBlock && i === blocks.length - 1) {
+ // it has been checked before that the last
+ // block is a valid IPv4 address
+ } else if (!ipv6Block.test(blocks[i])) {
+ return false;
+ }
+ }
+ if (foundOmissionBlock) {
+ return blocks.length >= 1;
+ }
+ return blocks.length === expectedNumberOfBlocks;
+ }
+ return false;
+ }
+
+ var default_url_options = {
+ protocols: ['http', 'https', 'ftp'],
+ require_tld: true,
+ require_protocol: false,
+ require_host: true,
+ require_valid_protocol: true,
+ allow_underscores: false,
+ allow_trailing_dot: false,
+ allow_protocol_relative_urls: false
+ };
+
+ var wrapped_ipv6 = /^\[([^\]]+)\](?::([0-9]+))?$/;
+
+ function isRegExp(obj) {
+ return Object.prototype.toString.call(obj) === '[object RegExp]';
+ }
+
+ function checkHost(host, matches) {
+ for (var i = 0; i < matches.length; i++) {
+ var match = matches[i];
+ if (host === match || isRegExp(match) && match.test(host)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function isURL(url, options) {
+ assertString(url);
+ if (!url || url.length >= 2083 || /[\s<>]/.test(url)) {
+ return false;
+ }
+ if (url.indexOf('mailto:') === 0) {
+ return false;
+ }
+ options = merge(options, default_url_options);
+ var protocol = void 0,
+ auth = void 0,
+ host = void 0,
+ hostname = void 0,
+ port = void 0,
+ port_str = void 0,
+ split = void 0,
+ ipv6 = void 0;
+
+ split = url.split('#');
+ url = split.shift();
+
+ split = url.split('?');
+ url = split.shift();
+
+ split = url.split('://');
+ if (split.length > 1) {
+ protocol = split.shift();
+ if (options.require_valid_protocol && options.protocols.indexOf(protocol) === -1) {
+ return false;
+ }
+ } else if (options.require_protocol) {
+ return false;
+ } else if (options.allow_protocol_relative_urls && url.substr(0, 2) === '//') {
+ split[0] = url.substr(2);
+ }
+ url = split.join('://');
+
+ split = url.split('/');
+ url = split.shift();
+
+ if (url === '' && !options.require_host) {
+ return true;
+ }
+
+ split = url.split('@');
+ if (split.length > 1) {
+ auth = split.shift();
+ if (auth.indexOf(':') >= 0 && auth.split(':').length > 2) {
+ return false;
+ }
+ }
+ hostname = split.join('@');
+
+ port_str = ipv6 = null;
+ var ipv6_match = hostname.match(wrapped_ipv6);
+ if (ipv6_match) {
+ host = '';
+ ipv6 = ipv6_match[1];
+ port_str = ipv6_match[2] || null;
+ } else {
+ split = hostname.split(':');
+ host = split.shift();
+ if (split.length) {
+ port_str = split.join(':');
+ }
+ }
+
+ if (port_str !== null) {
+ port = parseInt(port_str, 10);
+ if (!/^[0-9]+$/.test(port_str) || port <= 0 || port > 65535) {
+ return false;
+ }
+ }
+
+ if (!isIP(host) && !isFDQN(host, options) && (!ipv6 || !isIP(ipv6, 6)) && host !== 'localhost') {
+ return false;
+ }
+
+ host = host || ipv6;
+
+ if (options.host_whitelist && !checkHost(host, options.host_whitelist)) {
+ return false;
+ }
+ if (options.host_blacklist && checkHost(host, options.host_blacklist)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var macAddress = /^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/;
+
+ function isMACAddress(str) {
+ assertString(str);
+ return macAddress.test(str);
+ }
+
+ function isBoolean(str) {
+ assertString(str);
+ return ['true', 'false', '1', '0'].indexOf(str) >= 0;
+ }
+
+ var alpha = {
+ 'en-US': /^[A-Z]+$/i,
+ 'cs-CZ': /^[A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ]+$/i,
+ 'da-DK': /^[A-ZÆØÅ]+$/i,
+ 'de-DE': /^[A-ZÄÖÜß]+$/i,
+ 'es-ES': /^[A-ZÁÉÍÑÓÚÜ]+$/i,
+ 'fr-FR': /^[A-ZÀÂÆÇÉÈÊËÏÎÔŒÙÛÜŸ]+$/i,
+ 'nl-NL': /^[A-ZÉËÏÓÖÜ]+$/i,
+ 'hu-HU': /^[A-ZÁÉÍÓÖŐÚÜŰ]+$/i,
+ 'pl-PL': /^[A-ZĄĆĘŚŁŃÓŻŹ]+$/i,
+ 'pt-PT': /^[A-ZÃÁÀÂÇÉÊÍÕÓÔÚÜ]+$/i,
+ 'ru-RU': /^[А-ЯЁ]+$/i,
+ 'sr-RS@latin': /^[A-ZČĆŽŠĐ]+$/i,
+ 'sr-RS': /^[А-ЯЂЈЉЊЋЏ]+$/i,
+ 'tr-TR': /^[A-ZÇĞİıÖŞÜ]+$/i,
+ 'uk-UA': /^[А-ЩЬЮЯЄIЇҐ]+$/i,
+ ar: /^[ءآأؤإئابةتثجحخدذرزسشصضطظعغفقكلمنهوىيًٌٍَُِّْٰ]+$/
+ };
+
+ var alphanumeric = {
+ 'en-US': /^[0-9A-Z]+$/i,
+ 'cs-CZ': /^[0-9A-ZÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ]+$/i,
+ 'da-DK': /^[0-9A-ZÆØÅ]$/i,
+ 'de-DE': /^[0-9A-ZÄÖÜß]+$/i,
+ 'es-ES': /^[0-9A-ZÁÉÍÑÓÚÜ]+$/i,
+ 'fr-FR': /^[0-9A-ZÀÂÆÇÉÈÊËÏÎÔŒÙÛÜŸ]+$/i,
+ 'hu-HU': /^[0-9A-ZÁÉÍÓÖŐÚÜŰ]+$/i,
+ 'nl-NL': /^[0-9A-ZÉËÏÓÖÜ]+$/i,
+ 'pl-PL': /^[0-9A-ZĄĆĘŚŁŃÓŻŹ]+$/i,
+ 'pt-PT': /^[0-9A-ZÃÁÀÂÇÉÊÍÕÓÔÚÜ]+$/i,
+ 'ru-RU': /^[0-9А-ЯЁ]+$/i,
+ 'sr-RS@latin': /^[0-9A-ZČĆŽŠĐ]+$/i,
+ 'sr-RS': /^[0-9А-ЯЂЈЉЊЋЏ]+$/i,
+ 'tr-TR': /^[0-9A-ZÇĞİıÖŞÜ]+$/i,
+ 'uk-UA': /^[0-9А-ЩЬЮЯЄIЇҐ]+$/i,
+ ar: /^[٠١٢٣٤٥٦٧٨٩0-9ءآأؤإئابةتثجحخدذرزسشصضطظعغفقكلمنهوىيًٌٍَُِّْٰ]+$/
+ };
+
+ var englishLocales = ['AU', 'GB', 'HK', 'IN', 'NZ', 'ZA', 'ZM'];
+
+ for (var locale, i = 0; i < englishLocales.length; i++) {
+ locale = 'en-' + englishLocales[i];
+ alpha[locale] = alpha['en-US'];
+ alphanumeric[locale] = alphanumeric['en-US'];
+ }
+
+ alpha['pt-BR'] = alpha['pt-PT'];
+ alphanumeric['pt-BR'] = alphanumeric['pt-PT'];
+
+ // Source: http://www.localeplanet.com/java/
+ var arabicLocales = ['AE', 'BH', 'DZ', 'EG', 'IQ', 'JO', 'KW', 'LB', 'LY', 'MA', 'QM', 'QA', 'SA', 'SD', 'SY', 'TN', 'YE'];
+
+ for (var _locale, _i = 0; _i < arabicLocales.length; _i++) {
+ _locale = 'ar-' + arabicLocales[_i];
+ alpha[_locale] = alpha.ar;
+ alphanumeric[_locale] = alphanumeric.ar;
+ }
+
+ function isAlpha(str) {
+ var locale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en-US';
+
+ assertString(str);
+ if (locale in alpha) {
+ return alpha[locale].test(str);
+ }
+ throw new Error('Invalid locale \'' + locale + '\'');
+ }
+
+ function isAlphanumeric(str) {
+ var locale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en-US';
+
+ assertString(str);
+ if (locale in alphanumeric) {
+ return alphanumeric[locale].test(str);
+ }
+ throw new Error('Invalid locale \'' + locale + '\'');
+ }
+
+ var numeric = /^[-+]?[0-9]+$/;
+
+ function isNumeric(str) {
+ assertString(str);
+ return numeric.test(str);
+ }
+
+ function isLowercase(str) {
+ assertString(str);
+ return str === str.toLowerCase();
+ }
+
+ function isUppercase(str) {
+ assertString(str);
+ return str === str.toUpperCase();
+ }
+
+ /* eslint-disable no-control-regex */
+ var ascii = /^[\x00-\x7F]+$/;
+ /* eslint-enable no-control-regex */
+
+ function isAscii(str) {
+ assertString(str);
+ return ascii.test(str);
+ }
+
+ var fullWidth = /[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]/;
+
+ function isFullWidth(str) {
+ assertString(str);
+ return fullWidth.test(str);
+ }
+
+ var halfWidth = /[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]/;
+
+ function isHalfWidth(str) {
+ assertString(str);
+ return halfWidth.test(str);
+ }
+
+ function isVariableWidth(str) {
+ assertString(str);
+ return fullWidth.test(str) && halfWidth.test(str);
+ }
+
+ /* eslint-disable no-control-regex */
+ var multibyte = /[^\x00-\x7F]/;
+ /* eslint-enable no-control-regex */
+
+ function isMultibyte(str) {
+ assertString(str);
+ return multibyte.test(str);
+ }
+
+ var surrogatePair = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+ function isSurrogatePair(str) {
+ assertString(str);
+ return surrogatePair.test(str);
+ }
+
+ var int = /^(?:[-+]?(?:0|[1-9][0-9]*))$/;
+ var intLeadingZeroes = /^[-+]?[0-9]+$/;
+
+ function isInt(str, options) {
+ assertString(str);
+ options = options || {};
+
+ // Get the regex to use for testing, based on whether
+ // leading zeroes are allowed or not.
+ var regex = options.hasOwnProperty('allow_leading_zeroes') && !options.allow_leading_zeroes ? int : intLeadingZeroes;
+
+ // Check min/max/lt/gt
+ var minCheckPassed = !options.hasOwnProperty('min') || str >= options.min;
+ var maxCheckPassed = !options.hasOwnProperty('max') || str <= options.max;
+ var ltCheckPassed = !options.hasOwnProperty('lt') || str < options.lt;
+ var gtCheckPassed = !options.hasOwnProperty('gt') || str > options.gt;
+
+ return regex.test(str) && minCheckPassed && maxCheckPassed && ltCheckPassed && gtCheckPassed;
+ }
+
+ var float = /^(?:[-+])?(?:[0-9]+)?(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/;
+
+ function isFloat(str, options) {
+ assertString(str);
+ options = options || {};
+ if (str === '' || str === '.') {
+ return false;
+ }
+ return float.test(str) && (!options.hasOwnProperty('min') || str >= options.min) && (!options.hasOwnProperty('max') || str <= options.max) && (!options.hasOwnProperty('lt') || str < options.lt) && (!options.hasOwnProperty('gt') || str > options.gt);
+ }
+
+ var decimal = /^[-+]?([0-9]+|\.[0-9]+|[0-9]+\.[0-9]+)$/;
+
+ function isDecimal(str) {
+ assertString(str);
+ return str !== '' && decimal.test(str);
+ }
+
+ var hexadecimal = /^[0-9A-F]+$/i;
+
+ function isHexadecimal(str) {
+ assertString(str);
+ return hexadecimal.test(str);
+ }
+
+ function isDivisibleBy(str, num) {
+ assertString(str);
+ return toFloat(str) % parseInt(num, 10) === 0;
+ }
+
+ var hexcolor = /^#?([0-9A-F]{3}|[0-9A-F]{6})$/i;
+
+ function isHexColor(str) {
+ assertString(str);
+ return hexcolor.test(str);
+ }
+
+ var md5 = /^[a-f0-9]{32}$/;
+
+ function isMD5(str) {
+ assertString(str);
+ return md5.test(str);
+ }
+
+ function isJSON(str) {
+ assertString(str);
+ try {
+ var obj = JSON.parse(str);
+ return !!obj && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object';
+ } catch (e) {/* ignore */}
+ return false;
+ }
+
+ function isEmpty(str) {
+ assertString(str);
+ return str.length === 0;
+ }
+
+ /* eslint-disable prefer-rest-params */
+ function isLength(str, options) {
+ assertString(str);
+ var min = void 0;
+ var max = void 0;
+ if ((typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object') {
+ min = options.min || 0;
+ max = options.max;
+ } else {
+ // backwards compatibility: isLength(str, min [, max])
+ min = arguments[1];
+ max = arguments[2];
+ }
+ var surrogatePairs = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g) || [];
+ var len = str.length - surrogatePairs.length;
+ return len >= min && (typeof max === 'undefined' || len <= max);
+ }
+
+ var uuid = {
+ 3: /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
+ 4: /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
+ 5: /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
+ all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i
+ };
+
+ function isUUID(str) {
+ var version = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'all';
+
+ assertString(str);
+ var pattern = uuid[version];
+ return pattern && pattern.test(str);
+ }
+
+ function isMongoId(str) {
+ assertString(str);
+ return isHexadecimal(str) && str.length === 24;
+ }
+
+ function isAfter(str) {
+ var date = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : String(new Date());
+
+ assertString(str);
+ var comparison = toDate(date);
+ var original = toDate(str);
+ return !!(original && comparison && original > comparison);
+ }
+
+ function isBefore(str) {
+ var date = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : String(new Date());
+
+ assertString(str);
+ var comparison = toDate(date);
+ var original = toDate(str);
+ return !!(original && comparison && original < comparison);
+ }
+
+ function isIn(str, options) {
+ assertString(str);
+ var i = void 0;
+ if (Object.prototype.toString.call(options) === '[object Array]') {
+ var array = [];
+ for (i in options) {
+ if ({}.hasOwnProperty.call(options, i)) {
+ array[i] = toString(options[i]);
+ }
+ }
+ return array.indexOf(str) >= 0;
+ } else if ((typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object') {
+ return options.hasOwnProperty(str);
+ } else if (options && typeof options.indexOf === 'function') {
+ return options.indexOf(str) >= 0;
+ }
+ return false;
+ }
+
+ /* eslint-disable max-len */
+ var creditCard = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})|62[0-9]{14}$/;
+ /* eslint-enable max-len */
+
+ function isCreditCard(str) {
+ assertString(str);
+ var sanitized = str.replace(/[^0-9]+/g, '');
+ if (!creditCard.test(sanitized)) {
+ return false;
+ }
+ var sum = 0;
+ var digit = void 0;
+ var tmpNum = void 0;
+ var shouldDouble = void 0;
+ for (var i = sanitized.length - 1; i >= 0; i--) {
+ digit = sanitized.substring(i, i + 1);
+ tmpNum = parseInt(digit, 10);
+ if (shouldDouble) {
+ tmpNum *= 2;
+ if (tmpNum >= 10) {
+ sum += tmpNum % 10 + 1;
+ } else {
+ sum += tmpNum;
+ }
+ } else {
+ sum += tmpNum;
+ }
+ shouldDouble = !shouldDouble;
+ }
+ return !!(sum % 10 === 0 ? sanitized : false);
+ }
+
+ var isin = /^[A-Z]{2}[0-9A-Z]{9}[0-9]$/;
+
+ function isISIN(str) {
+ assertString(str);
+ if (!isin.test(str)) {
+ return false;
+ }
+
+ var checksumStr = str.replace(/[A-Z]/g, function (character) {
+ return parseInt(character, 36);
+ });
+
+ var sum = 0;
+ var digit = void 0;
+ var tmpNum = void 0;
+ var shouldDouble = true;
+ for (var i = checksumStr.length - 2; i >= 0; i--) {
+ digit = checksumStr.substring(i, i + 1);
+ tmpNum = parseInt(digit, 10);
+ if (shouldDouble) {
+ tmpNum *= 2;
+ if (tmpNum >= 10) {
+ sum += tmpNum + 1;
+ } else {
+ sum += tmpNum;
+ }
+ } else {
+ sum += tmpNum;
+ }
+ shouldDouble = !shouldDouble;
+ }
+
+ return parseInt(str.substr(str.length - 1), 10) === (10000 - sum) % 10;
+ }
+
+ var isbn10Maybe = /^(?:[0-9]{9}X|[0-9]{10})$/;
+ var isbn13Maybe = /^(?:[0-9]{13})$/;
+ var factor = [1, 3];
+
+ function isISBN(str) {
+ var version = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+
+ assertString(str);
+ version = String(version);
+ if (!version) {
+ return isISBN(str, 10) || isISBN(str, 13);
+ }
+ var sanitized = str.replace(/[\s-]+/g, '');
+ var checksum = 0;
+ var i = void 0;
+ if (version === '10') {
+ if (!isbn10Maybe.test(sanitized)) {
+ return false;
+ }
+ for (i = 0; i < 9; i++) {
+ checksum += (i + 1) * sanitized.charAt(i);
+ }
+ if (sanitized.charAt(9) === 'X') {
+ checksum += 10 * 10;
+ } else {
+ checksum += 10 * sanitized.charAt(9);
+ }
+ if (checksum % 11 === 0) {
+ return !!sanitized;
+ }
+ } else if (version === '13') {
+ if (!isbn13Maybe.test(sanitized)) {
+ return false;
+ }
+ for (i = 0; i < 12; i++) {
+ checksum += factor[i % 2] * sanitized.charAt(i);
+ }
+ if (sanitized.charAt(12) - (10 - checksum % 10) % 10 === 0) {
+ return !!sanitized;
+ }
+ }
+ return false;
+ }
+
+ var issn = '^\\d{4}-?\\d{3}[\\dX]$';
+
+ function isISSN(str) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ assertString(str);
+ var testIssn = issn;
+ testIssn = options.require_hyphen ? testIssn.replace('?', '') : testIssn;
+ testIssn = options.case_sensitive ? new RegExp(testIssn) : new RegExp(testIssn, 'i');
+ if (!testIssn.test(str)) {
+ return false;
+ }
+ var issnDigits = str.replace('-', '');
+ var position = 8;
+ var checksum = 0;
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = issnDigits[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var digit = _step.value;
+
+ var digitValue = digit.toUpperCase() === 'X' ? 10 : +digit;
+ checksum += digitValue * position;
+ --position;
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator.return) {
+ _iterator.return();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ return checksum % 11 === 0;
+ }
+
+ /* eslint-disable max-len */
+ var phones = {
+ 'ar-DZ': /^(\+?213|0)(5|6|7)\d{8}$/,
+ 'ar-SY': /^(!?(\+?963)|0)?9\d{8}$/,
+ 'ar-SA': /^(!?(\+?966)|0)?5\d{8}$/,
+ 'en-US': /^(\+?1)?[2-9]\d{2}[2-9](?!11)\d{6}$/,
+ 'cs-CZ': /^(\+?420)? ?[1-9][0-9]{2} ?[0-9]{3} ?[0-9]{3}$/,
+ 'de-DE': /^(\+?49[ \.\-])?([\(]{1}[0-9]{1,6}[\)])?([0-9 \.\-\/]{3,20})((x|ext|extension)[ ]?[0-9]{1,4})?$/,
+ 'da-DK': /^(\+?45)?(\d{8})$/,
+ 'el-GR': /^(\+?30)?(69\d{8})$/,
+ 'en-AU': /^(\+?61|0)4\d{8}$/,
+ 'en-GB': /^(\+?44|0)7\d{9}$/,
+ // According to http://www.ofca.gov.hk/filemanager/ofca/en/content_311/no_plan.pdf
+ 'en-HK': /^(\+?852-?)?((4(04[01]|06\d|09[3-9]|20\d|2[2-9]\d|3[3-9]\d|[467]\d{2}|5[1-9]\d|81\d|82[1-9]|8[69]\d|92[3-9]|95[2-9]|98\d)|5([1-79]\d{2})|6(0[1-9]\d|[1-9]\d{2})|7(0[1-9]\d|10[4-79]|11[458]|1[24578]\d|13[24-9]|16[0-8]|19[24579]|21[02-79]|2[456]\d|27[13-6]|3[456]\d|37[4578]|39[0146])|8(1[58]\d|2[45]\d|267|27[5-9]|2[89]\d|3[15-9]\d|32[5-8]|[46-9]\d{2}|5[013-9]\d)|9(0[1-9]\d|1[02-9]\d|[2-8]\d{2}))-?\d{4}|7130-?[0124-8]\d{3}|8167-?2\d{3})$/,
+ 'en-IN': /^(\+?91|0)?[789]\d{9}$/,
+ 'en-NG': /^(\+?234|0)?[789]\d{9}$/,
+ 'en-NZ': /^(\+?64|0)2\d{7,9}$/,
+ 'en-ZA': /^(\+?27|0)\d{9}$/,
+ 'en-ZM': /^(\+?26)?09[567]\d{7}$/,
+ 'es-ES': /^(\+?34)?(6\d{1}|7[1234])\d{7}$/,
+ 'fi-FI': /^(\+?358|0)\s?(4(0|1|2|4|5)?|50)\s?(\d\s?){4,8}\d$/,
+ 'fr-FR': /^(\+?33|0)[67]\d{8}$/,
+ 'he-IL': /^(\+972|0)([23489]|5[0248]|77)[1-9]\d{6}/,
+ 'hu-HU': /^(\+?36)(20|30|70)\d{7}$/,
+ 'lt-LT': /^(\+370|8)\d{8}$/,
+ 'id-ID': /^(\+?62|0[1-9])[\s|\d]+$/,
+ 'it-IT': /^(\+?39)?\s?3\d{2} ?\d{6,7}$/,
+ 'ko-KR': /^((\+?82)[ \-]?)?0?1([0|1|6|7|8|9]{1})[ \-]?\d{3,4}[ \-]?\d{4}$/,
+ 'ja-JP': /^(\+?81|0)\d{1,4}[ \-]?\d{1,4}[ \-]?\d{4}$/,
+ 'ms-MY': /^(\+?6?01){1}(([145]{1}(\-|\s)?\d{7,8})|([236789]{1}(\s|\-)?\d{7}))$/,
+ 'nb-NO': /^(\+?47)?[49]\d{7}$/,
+ 'nl-BE': /^(\+?32|0)4?\d{8}$/,
+ 'nn-NO': /^(\+?47)?[49]\d{7}$/,
+ 'pl-PL': /^(\+?48)? ?[5-8]\d ?\d{3} ?\d{2} ?\d{2}$/,
+ 'pt-BR': /^(\+?55|0)\-?[1-9]{2}\-?[2-9]{1}\d{3,4}\-?\d{4}$/,
+ 'pt-PT': /^(\+?351)?9[1236]\d{7}$/,
+ 'ro-RO': /^(\+?4?0)\s?7\d{2}(\/|\s|\.|\-)?\d{3}(\s|\.|\-)?\d{3}$/,
+ 'en-PK': /^((\+92)|(0092))-{0,1}\d{3}-{0,1}\d{7}$|^\d{11}$|^\d{4}-\d{7}$/,
+ 'ru-RU': /^(\+?7|8)?9\d{9}$/,
+ 'sr-RS': /^(\+3816|06)[- \d]{5,9}$/,
+ 'tr-TR': /^(\+?90|0)?5\d{9}$/,
+ 'vi-VN': /^(\+?84|0)?((1(2([0-9])|6([2-9])|88|99))|(9((?!5)[0-9])))([0-9]{7})$/,
+ 'zh-CN': /^(\+?0?86\-?)?1[345789]\d{9}$/,
+ 'zh-TW': /^(\+?886\-?|0)?9\d{8}$/
+ };
+ /* eslint-enable max-len */
+
+ // aliases
+ phones['en-CA'] = phones['en-US'];
+ phones['fr-BE'] = phones['nl-BE'];
+ phones['zh-HK'] = phones['en-HK'];
+
+ function isMobilePhone(str, locale) {
+ assertString(str);
+ if (locale in phones) {
+ return phones[locale].test(str);
+ } else if (locale === 'any') {
+ return !!Object.values(phones).find(phone => phone.test(str));
+ }
+ return false;
+ }
+
+ function currencyRegex(options) {
+ var symbol = '(\\' + options.symbol.replace(/\./g, '\\.') + ')' + (options.require_symbol ? '' : '?'),
+ negative = '-?',
+ whole_dollar_amount_without_sep = '[1-9]\\d*',
+ whole_dollar_amount_with_sep = '[1-9]\\d{0,2}(\\' + options.thousands_separator + '\\d{3})*',
+ valid_whole_dollar_amounts = ['0', whole_dollar_amount_without_sep, whole_dollar_amount_with_sep],
+ whole_dollar_amount = '(' + valid_whole_dollar_amounts.join('|') + ')?',
+ decimal_amount = '(\\' + options.decimal_separator + '\\d{2})?';
+ var pattern = whole_dollar_amount + decimal_amount;
+
+ // default is negative sign before symbol, but there are two other options (besides parens)
+ if (options.allow_negatives && !options.parens_for_negatives) {
+ if (options.negative_sign_after_digits) {
+ pattern += negative;
+ } else if (options.negative_sign_before_digits) {
+ pattern = negative + pattern;
+ }
+ }
+
+ // South African Rand, for example, uses R 123 (space) and R-123 (no space)
+ if (options.allow_negative_sign_placeholder) {
+ pattern = '( (?!\\-))?' + pattern;
+ } else if (options.allow_space_after_symbol) {
+ pattern = ' ?' + pattern;
+ } else if (options.allow_space_after_digits) {
+ pattern += '( (?!$))?';
+ }
+
+ if (options.symbol_after_digits) {
+ pattern += symbol;
+ } else {
+ pattern = symbol + pattern;
+ }
+
+ if (options.allow_negatives) {
+ if (options.parens_for_negatives) {
+ pattern = '(\\(' + pattern + '\\)|' + pattern + ')';
+ } else if (!(options.negative_sign_before_digits || options.negative_sign_after_digits)) {
+ pattern = negative + pattern;
+ }
+ }
+
+ /* eslint-disable prefer-template */
+ return new RegExp('^' +
+ // ensure there's a dollar and/or decimal amount, and that
+ // it doesn't start with a space or a negative sign followed by a space
+ '(?!-? )(?=.*\\d)' + pattern + '$');
+ /* eslint-enable prefer-template */
+ }
+
+ var default_currency_options = {
+ symbol: '$',
+ require_symbol: false,
+ allow_space_after_symbol: false,
+ symbol_after_digits: false,
+ allow_negatives: true,
+ parens_for_negatives: false,
+ negative_sign_before_digits: false,
+ negative_sign_after_digits: false,
+ allow_negative_sign_placeholder: false,
+ thousands_separator: ',',
+ decimal_separator: '.',
+ allow_space_after_digits: false
+ };
+
+ function isCurrency(str, options) {
+ assertString(str);
+ options = merge(options, default_currency_options);
+ return currencyRegex(options).test(str);
+ }
+
+ /* eslint-disable max-len */
+ // from http://goo.gl/0ejHHW
+ var iso8601 = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
+ /* eslint-enable max-len */
+
+ function isISO8601 (str) {
+ assertString(str);
+ return iso8601.test(str);
+ }
+
+ function isBase64(str) {
+ assertString(str);
+ // Value length must be divisible by 4.
+ var len = str.length;
+ if (!len || len % 4 !== 0) {
+ return false;
+ }
+
+ try {
+ if (atob(str)) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+
+ var dataURI = /^\s*data:([a-z]+\/[a-z0-9\-\+]+(;[a-z\-]+=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9!\$&',\(\)\*\+,;=\-\._~:@\/\?%\s]*\s*$/i; // eslint-disable-line max-len
+
+ function isDataURI(str) {
+ assertString(str);
+ return dataURI.test(str);
+ }
+
+ function ltrim(str, chars) {
+ assertString(str);
+ var pattern = chars ? new RegExp('^[' + chars + ']+', 'g') : /^\s+/g;
+ return str.replace(pattern, '');
+ }
+
+ function rtrim(str, chars) {
+ assertString(str);
+ var pattern = chars ? new RegExp('[' + chars + ']') : /\s/;
+
+ var idx = str.length - 1;
+ while (idx >= 0 && pattern.test(str[idx])) {
+ idx--;
+ }
+
+ return idx < str.length ? str.substr(0, idx + 1) : str;
+ }
+
+ function trim(str, chars) {
+ return rtrim(ltrim(str, chars), chars);
+ }
+
+ function escape(str) {
+ assertString(str);
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\//g, '&#x2F;').replace(/\\/g, '&#x5C;').replace(/`/g, '&#96;');
+ }
+
+ function unescape(str) {
+ assertString(str);
+ return str.replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#x2F;/g, '/').replace(/&#96;/g, '`');
+ }
+
+ function blacklist(str, chars) {
+ assertString(str);
+ return str.replace(new RegExp('[' + chars + ']+', 'g'), '');
+ }
+
+ function stripLow(str, keep_new_lines) {
+ assertString(str);
+ var chars = keep_new_lines ? '\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F' : '\\x00-\\x1F\\x7F';
+ return blacklist(str, chars);
+ }
+
+ function whitelist(str, chars) {
+ assertString(str);
+ return str.replace(new RegExp('[^' + chars + ']+', 'g'), '');
+ }
+
+ function isWhitelisted(str, chars) {
+ assertString(str);
+ for (var i = str.length - 1; i >= 0; i--) {
+ if (chars.indexOf(str[i]) === -1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var default_normalize_email_options = {
+ // The following options apply to all email addresses
+ // Lowercases the local part of the email address.
+ // Please note this may violate RFC 5321 as per http://stackoverflow.com/a/9808332/192024).
+ // The domain is always lowercased, as per RFC 1035
+ all_lowercase: true,
+
+ // The following conversions are specific to GMail
+ // Lowercases the local part of the GMail address (known to be case-insensitive)
+ gmail_lowercase: true,
+ // Removes dots from the local part of the email address, as that's ignored by GMail
+ gmail_remove_dots: true,
+ // Removes the subaddress (e.g. "+foo") from the email address
+ gmail_remove_subaddress: true,
+ // Conversts the googlemail.com domain to gmail.com
+ gmail_convert_googlemaildotcom: true,
+
+ // The following conversions are specific to Outlook.com / Windows Live / Hotmail
+ // Lowercases the local part of the Outlook.com address (known to be case-insensitive)
+ outlookdotcom_lowercase: true,
+ // Removes the subaddress (e.g. "+foo") from the email address
+ outlookdotcom_remove_subaddress: true,
+
+ // The following conversions are specific to Yahoo
+ // Lowercases the local part of the Yahoo address (known to be case-insensitive)
+ yahoo_lowercase: true,
+ // Removes the subaddress (e.g. "-foo") from the email address
+ yahoo_remove_subaddress: true,
+
+ // The following conversions are specific to iCloud
+ // Lowercases the local part of the iCloud address (known to be case-insensitive)
+ icloud_lowercase: true,
+ // Removes the subaddress (e.g. "+foo") from the email address
+ icloud_remove_subaddress: true
+ };
+
+ // List of domains used by iCloud
+ var icloud_domains = ['icloud.com', 'me.com'];
+
+ // List of domains used by Outlook.com and its predecessors
+ // This list is likely incomplete.
+ // Partial reference:
+ // https://blogs.office.com/2013/04/17/outlook-com-gets-two-step-verification-sign-in-by-alias-and-new-international-domains/
+ var outlookdotcom_domains = ['hotmail.at', 'hotmail.be', 'hotmail.ca', 'hotmail.cl', 'hotmail.co.il', 'hotmail.co.nz', 'hotmail.co.th', 'hotmail.co.uk', 'hotmail.com', 'hotmail.com.ar', 'hotmail.com.au', 'hotmail.com.br', 'hotmail.com.gr', 'hotmail.com.mx', 'hotmail.com.pe', 'hotmail.com.tr', 'hotmail.com.vn', 'hotmail.cz', 'hotmail.de', 'hotmail.dk', 'hotmail.es', 'hotmail.fr', 'hotmail.hu', 'hotmail.id', 'hotmail.ie', 'hotmail.in', 'hotmail.it', 'hotmail.jp', 'hotmail.kr', 'hotmail.lv', 'hotmail.my', 'hotmail.ph', 'hotmail.pt', 'hotmail.sa', 'hotmail.sg', 'hotmail.sk', 'live.be', 'live.co.uk', 'live.com', 'live.com.ar', 'live.com.mx', 'live.de', 'live.es', 'live.eu', 'live.fr', 'live.it', 'live.nl', 'msn.com', 'outlook.at', 'outlook.be', 'outlook.cl', 'outlook.co.il', 'outlook.co.nz', 'outlook.co.th', 'outlook.com', 'outlook.com.ar', 'outlook.com.au', 'outlook.com.br', 'outlook.com.gr', 'outlook.com.pe', 'outlook.com.tr', 'outlook.com.vn', 'outlook.cz', 'outlook.de', 'outlook.dk', 'outlook.es', 'outlook.fr', 'outlook.hu', 'outlook.id', 'outlook.ie', 'outlook.in', 'outlook.it', 'outlook.jp', 'outlook.kr', 'outlook.lv', 'outlook.my', 'outlook.ph', 'outlook.pt', 'outlook.sa', 'outlook.sg', 'outlook.sk', 'passport.com'];
+
+ // List of domains used by Yahoo Mail
+ // This list is likely incomplete
+ var yahoo_domains = ['rocketmail.com', 'yahoo.ca', 'yahoo.co.uk', 'yahoo.com', 'yahoo.de', 'yahoo.fr', 'yahoo.in', 'yahoo.it', 'ymail.com'];
+
+ function normalizeEmail(email, options) {
+ options = merge(options, default_normalize_email_options);
+
+ if (!isEmail(email)) {
+ return false;
+ }
+
+ var raw_parts = email.split('@');
+ var domain = raw_parts.pop();
+ var user = raw_parts.join('@');
+ var parts = [user, domain];
+
+ // The domain is always lowercased, as it's case-insensitive per RFC 1035
+ parts[1] = parts[1].toLowerCase();
+
+ if (parts[1] === 'gmail.com' || parts[1] === 'googlemail.com') {
+ // Address is GMail
+ if (options.gmail_remove_subaddress) {
+ parts[0] = parts[0].split('+')[0];
+ }
+ if (options.gmail_remove_dots) {
+ parts[0] = parts[0].replace(/\./g, '');
+ }
+ if (!parts[0].length) {
+ return false;
+ }
+ if (options.all_lowercase || options.gmail_lowercase) {
+ parts[0] = parts[0].toLowerCase();
+ }
+ parts[1] = options.gmail_convert_googlemaildotcom ? 'gmail.com' : parts[1];
+ } else if (~icloud_domains.indexOf(parts[1])) {
+ // Address is iCloud
+ if (options.icloud_remove_subaddress) {
+ parts[0] = parts[0].split('+')[0];
+ }
+ if (!parts[0].length) {
+ return false;
+ }
+ if (options.all_lowercase || options.icloud_lowercase) {
+ parts[0] = parts[0].toLowerCase();
+ }
+ } else if (~outlookdotcom_domains.indexOf(parts[1])) {
+ // Address is Outlook.com
+ if (options.outlookdotcom_remove_subaddress) {
+ parts[0] = parts[0].split('+')[0];
+ }
+ if (!parts[0].length) {
+ return false;
+ }
+ if (options.all_lowercase || options.outlookdotcom_lowercase) {
+ parts[0] = parts[0].toLowerCase();
+ }
+ } else if (~yahoo_domains.indexOf(parts[1])) {
+ // Address is Yahoo
+ if (options.yahoo_remove_subaddress) {
+ var components = parts[0].split('-');
+ parts[0] = components.length > 1 ? components.slice(0, -1).join('-') : components[0];
+ }
+ if (!parts[0].length) {
+ return false;
+ }
+ if (options.all_lowercase || options.yahoo_lowercase) {
+ parts[0] = parts[0].toLowerCase();
+ }
+ } else if (options.all_lowercase) {
+ // Any other address
+ parts[0] = parts[0].toLowerCase();
+ }
+ return parts.join('@');
+ }
+
+ // see http://isrc.ifpi.org/en/isrc-standard/code-syntax
+ var isrc = /^[A-Z]{2}[0-9A-Z]{3}\d{2}\d{5}$/;
+
+ function isISRC(str) {
+ assertString(str);
+ return isrc.test(str);
+ }
+
+ var cultureCodes = new Set(["ar", "bg", "ca", "zh-Hans", "cs", "da", "de",
+ "el", "en", "es", "fi", "fr", "he", "hu", "is", "it", "ja", "ko", "nl", "no",
+ "pl", "pt", "rm", "ro", "ru", "hr", "sk", "sq", "sv", "th", "tr", "ur", "id",
+ "uk", "be", "sl", "et", "lv", "lt", "tg", "fa", "vi", "hy", "az", "eu", "hsb",
+ "mk", "tn", "xh", "zu", "af", "ka", "fo", "hi", "mt", "se", "ga", "ms", "kk",
+ "ky", "sw", "tk", "uz", "tt", "bn", "pa", "gu", "or", "ta", "te", "kn", "ml",
+ "as", "mr", "sa", "mn", "bo", "cy", "km", "lo", "gl", "kok", "syr", "si", "iu",
+ "am", "tzm", "ne", "fy", "ps", "fil", "dv", "ha", "yo", "quz", "nso", "ba", "lb",
+ "kl", "ig", "ii", "arn", "moh", "br", "ug", "mi", "oc", "co", "gsw", "sah",
+ "qut", "rw", "wo", "prs", "gd", "ar-SA", "bg-BG", "ca-ES", "zh-TW", "cs-CZ",
+ "da-DK", "de-DE", "el-GR", "en-US", "fi-FI", "fr-FR", "he-IL", "hu-HU", "is-IS",
+ "it-IT", "ja-JP", "ko-KR", "nl-NL", "nb-NO", "pl-PL", "pt-BR", "rm-CH", "ro-RO",
+ "ru-RU", "hr-HR", "sk-SK", "sq-AL", "sv-SE", "th-TH", "tr-TR", "ur-PK", "id-ID",
+ "uk-UA", "be-BY", "sl-SI", "et-EE", "lv-LV", "lt-LT", "tg-Cyrl-TJ", "fa-IR",
+ "vi-VN", "hy-AM", "az-Latn-AZ", "eu-ES", "hsb-DE", "mk-MK", "tn-ZA", "xh-ZA",
+ "zu-ZA", "af-ZA", "ka-GE", "fo-FO", "hi-IN", "mt-MT", "se-NO", "ms-MY", "kk-KZ",
+ "ky-KG", "sw-KE", "tk-TM", "uz-Latn-UZ", "tt-RU", "bn-IN", "pa-IN", "gu-IN",
+ "or-IN", "ta-IN", "te-IN", "kn-IN", "ml-IN", "as-IN", "mr-IN", "sa-IN", "mn-MN",
+ "bo-CN", "cy-GB", "km-KH", "lo-LA", "gl-ES", "kok-IN", "syr-SY", "si-LK",
+ "iu-Cans-CA", "am-ET", "ne-NP", "fy-NL", "ps-AF", "fil-PH", "dv-MV",
+ "ha-Latn-NG", "yo-NG", "quz-BO", "nso-ZA", "ba-RU", "lb-LU", "kl-GL", "ig-NG",
+ "ii-CN", "arn-CL", "moh-CA", "br-FR", "ug-CN", "mi-NZ", "oc-FR", "co-FR",
+ "gsw-FR", "sah-RU", "qut-GT", "rw-RW", "wo-SN", "prs-AF", "gd-GB", "ar-IQ",
+ "zh-CN", "de-CH", "en-GB", "es-MX", "fr-BE", "it-CH", "nl-BE", "nn-NO", "pt-PT",
+ "sr-Latn-CS", "sv-FI", "az-Cyrl-AZ", "dsb-DE", "se-SE", "ga-IE", "ms-BN",
+ "uz-Cyrl-UZ", "bn-BD", "mn-Mong-CN", "iu-Latn-CA", "tzm-Latn-DZ", "quz-EC",
+ "ar-EG", "zh-HK", "de-AT", "en-AU", "es-ES", "fr-CA", "sr-Cyrl-CS", "se-FI",
+ "quz-PE", "ar-LY", "zh-SG", "de-LU", "en-CA", "es-GT", "fr-CH", "hr-BA",
+ "smj-NO", "ar-DZ", "zh-MO", "de-LI", "en-NZ", "es-CR", "fr-LU", "bs-Latn-BA",
+ "smj-SE", "ar-MA", "en-IE", "es-PA", "fr-MC", "sr-Latn-BA", "sma-NO", "ar-TN",
+ "en-ZA", "es-DO", "sr-Cyrl-BA", "sma-SE", "ar-OM", "en-JM", "es-VE",
+ "bs-Cyrl-BA", "sms-FI", "ar-YE", "en-029", "es-CO", "sr-Latn-RS", "smn-FI",
+ "ar-SY", "en-BZ", "es-PE", "sr-Cyrl-RS", "ar-JO", "en-TT", "es-AR", "sr-Latn-ME",
+ "ar-LB", "en-ZW", "es-EC", "sr-Cyrl-ME", "ar-KW", "en-PH", "es-CL", "ar-AE",
+ "es-UY", "ar-BH", "es-PY", "ar-QA", "en-IN", "es-BO", "en-MY", "es-SV", "en-SG",
+ "es-HN", "es-NI", "es-PR", "es-US", "bs-Cyrl", "bs-Latn", "sr-Cyrl", "sr-Latn",
+ "smn", "az-Cyrl", "sms", "zh", "nn", "bs", "az-Latn", "sma", "uz-Cyrl",
+ "mn-Cyrl", "iu-Cans", "zh-Hant", "nb", "sr", "tg-Cyrl", "dsb", "smj", "uz-Latn",
+ "mn-Mong", "iu-Latn", "tzm-Latn", "ha-Latn", "zh-CHS", "zh-CHT"]);
+
+ function isRFC5646(str) {
+ assertString(str);
+ // According to the spec these codes are case sensitive so we can check the
+ // string directly.
+ return cultureCodes.has(str);
+ }
+
+ var semver = /^v?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/i
+
+ function isSemVer(str) {
+ assertString(str);
+ return semver.test(str);
+ }
+
+ var rgbcolor = /^rgb?\(\s*(0|[1-9]\d?|1\d\d?|2[0-4]\d|25[0-5])\s*,\s*(0|[1-9]\d?|1\d\d?|2[0-4]\d|25[0-5])\s*,\s*(0|[1-9]\d?|1\d\d?|2[0-4]\d|25[0-5])\s*\)$/i
+
+ function isRGBColor(str) {
+ assertString(str);
+ return rgbcolor.test(str);
+ }
+
+ var version = '7.0.0';
+
+ var validator = {
+ blacklist: blacklist,
+ contains: contains,
+ equals: equals,
+ escape: escape,
+ isAfter: isAfter,
+ isAlpha: isAlpha,
+ isAlphanumeric: isAlphanumeric,
+ isAscii: isAscii,
+ isBase64: isBase64,
+ isBefore: isBefore,
+ isBoolean: isBoolean,
+ isByteLength: isByteLength,
+ isCreditCard: isCreditCard,
+ isCurrency: isCurrency,
+ isDataURI: isDataURI,
+ isDecimal: isDecimal,
+ isDivisibleBy: isDivisibleBy,
+ isEmail: isEmail,
+ isEmpty: isEmpty,
+ isFloat: isFloat,
+ isFQDN: isFDQN,
+ isFullWidth: isFullWidth,
+ isHalfWidth: isHalfWidth,
+ isHexadecimal: isHexadecimal,
+ isHexColor: isHexColor,
+ isIn: isIn,
+ isInt: isInt,
+ isIP: isIP,
+ isRFC5646: isRFC5646,
+ isISBN: isISBN,
+ isISIN: isISIN,
+ isISO8601: isISO8601,
+ isISRC: isISRC,
+ isRGBColor: isRGBColor,
+ isISSN: isISSN,
+ isJSON: isJSON,
+ isLength: isLength,
+ isLowercase: isLowercase,
+ isMACAddress: isMACAddress,
+ isMD5: isMD5,
+ isMobilePhone: isMobilePhone,
+ isMongoId: isMongoId,
+ isMultibyte: isMultibyte,
+ isNumeric: isNumeric,
+ isSemVer: isSemVer,
+ isSurrogatePair: isSurrogatePair,
+ isUppercase: isUppercase,
+ isURL: isURL,
+ isUUID: isUUID,
+ isVariableWidth: isVariableWidth,
+ isWhitelisted: isWhitelisted,
+ ltrim: ltrim,
+ matches: matches,
+ normalizeEmail: normalizeEmail,
+ rtrim: rtrim,
+ stripLow: stripLow,
+ toBoolean: toBoolean,
+ toDate: toDate,
+ toFloat: toFloat,
+ toInt: toInt,
+ toString: toString,
+ trim: trim,
+ unescape: unescape,
+ version: version,
+ whitelist: whitelist
+ };
+
+ return validator;
+}));
diff --git a/devtools/shared/system.js b/devtools/shared/system.js
new file mode 100644
index 0000000000..1a6988d5b8
--- /dev/null
+++ b/devtools/shared/system.js
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+loader.lazyRequireGetter(
+ this,
+ "DevToolsServer",
+ "resource://devtools/server/devtools-server.js",
+ true
+);
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+});
+loader.lazyGetter(this, "hostname", () => {
+ try {
+ // On some platforms (Linux according to try), this service does not exist and fails.
+ return Services.dns.myHostName;
+ } catch (e) {
+ return "";
+ }
+});
+loader.lazyGetter(this, "endianness", () => {
+ if (new Uint32Array(new Uint8Array([1, 2, 3, 4]).buffer)[0] === 0x04030201) {
+ return "LE";
+ }
+ return "BE";
+});
+
+const APP_MAP = {
+ "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "firefox",
+ "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "thunderbird",
+ "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "seamonkey",
+ "{718e30fb-e89b-41dd-9da7-e25a45638b28}": "sunbird",
+ "{aa3c5121-dab2-40e2-81ca-7ea25febc110}": "mobile/android",
+};
+
+var CACHED_INFO = null;
+
+function getSystemInfo() {
+ if (CACHED_INFO) {
+ return CACHED_INFO;
+ }
+
+ const appInfo = Services.appinfo;
+ const win = Services.wm.getMostRecentWindow(DevToolsServer.chromeWindowType);
+ const [processor, compiler] = appInfo.XPCOMABI.split("-");
+ let dpi, useragent, width, height, physicalWidth, physicalHeight, brandName;
+ const appid = appInfo.ID;
+ const apptype = APP_MAP[appid];
+ const geckoVersion = appInfo.platformVersion;
+ const hardware = "unknown";
+ let version = "unknown";
+
+ const os = appInfo.OS;
+ version = appInfo.version;
+
+ const bundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+ if (bundle) {
+ brandName = bundle.GetStringFromName("brandFullName");
+ } else {
+ brandName = null;
+ }
+
+ if (win) {
+ const utils = win.windowUtils;
+ dpi = utils.displayDPI;
+ useragent = win.navigator.userAgent;
+ width = win.screen.width;
+ height = win.screen.height;
+ physicalWidth = win.screen.width * win.devicePixelRatio;
+ physicalHeight = win.screen.height * win.devicePixelRatio;
+ }
+
+ const info = {
+ /**
+ * Information from nsIXULAppInfo, regarding
+ * the application itself.
+ */
+
+ // The XUL application's UUID.
+ appid,
+
+ // Name of the app, "firefox", "thunderbird", etc., listed in APP_MAP
+ apptype,
+
+ // Mixed-case or empty string of vendor, like "Mozilla"
+ vendor: appInfo.vendor,
+
+ // Name of the application, like "Firefox", "Thunderbird".
+ name: appInfo.name,
+
+ // The application's version, for example "0.8.0+" or "3.7a1pre".
+ // Typically, the version of Firefox, for example.
+ // It is different than the version of Gecko or the XULRunner platform.
+ version,
+
+ // The application's build ID/date, for example "2004051604".
+ appbuildid: appInfo.appBuildID,
+
+ // The build ID/date of Gecko and the XULRunner platform.
+ platformbuildid: appInfo.platformBuildID,
+ geckobuildid: appInfo.platformBuildID,
+
+ // The version of Gecko or XULRunner platform, for example "1.8.1.19" or
+ // "1.9.3pre". In "Firefox 3.7 alpha 1" the application version is "3.7a1pre"
+ // while the platform version is "1.9.3pre"
+ platformversion: geckoVersion,
+ geckoversion: geckoVersion,
+
+ // Locale used in this build
+ locale: Services.locale.appLocaleAsBCP47,
+
+ /**
+ * Information regarding the operating system.
+ */
+
+ // Returns the endianness of the architecture: either "LE" or "BE"
+ endianness,
+
+ // Returns the hostname of the machine
+ hostname,
+
+ // Name of the OS type. Typically the same as `uname -s`. Possible values:
+ // https://developer.mozilla.org/en/OS_TARGET
+ os,
+ platform: os,
+
+ // hardware and version info from `deviceinfo.hardware`
+ // and `deviceinfo.os`.
+ hardware,
+
+ // Device name. This property is only available on Android.
+ // e.g. "Pixel 2"
+ deviceName: getDeviceName(),
+
+ // Type of process architecture running:
+ // "arm", "ia32", "x86", "x64"
+ // Alias to both `arch` and `processor` for node/deviceactor compat
+ arch: processor,
+ processor,
+
+ // Name of compiler used for build:
+ // `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...`
+ compiler,
+
+ // Location for the current profile
+ profile: getProfileLocation(),
+
+ // Update channel
+ channel: lazy.AppConstants.MOZ_UPDATE_CHANNEL,
+
+ dpi,
+ useragent,
+ width,
+ height,
+ physicalWidth,
+ physicalHeight,
+ brandName,
+ };
+
+ CACHED_INFO = info;
+ return info;
+}
+
+function getDeviceName() {
+ try {
+ // Will throw on other platforms than Firefox for Android.
+ return Services.sysinfo.getProperty("device");
+ } catch (e) {
+ return null;
+ }
+}
+
+function getProfileLocation() {
+ // In child processes, we cannot access the profile location.
+ try {
+ // For some reason this line must come first or in xpcshell tests
+ // nsXREDirProvider never gets initialised and so the profile service
+ // crashes on initialisation.
+ const profd = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ const profservice = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ if (profservice.currentProfile) {
+ return profservice.currentProfile.name;
+ }
+
+ return profd.leafName;
+ } catch (e) {
+ return "";
+ }
+}
+
+exports.getSystemInfo = getSystemInfo;
diff --git a/devtools/shared/test-helpers/allocation-tracker.js b/devtools/shared/test-helpers/allocation-tracker.js
new file mode 100644
index 0000000000..17dcfafdf0
--- /dev/null
+++ b/devtools/shared/test-helpers/allocation-tracker.js
@@ -0,0 +1,637 @@
+/* this source code form is subject to the terms of the mozilla public
+ * license, v. 2.0. if a copy of the mpl was not distributed with this
+ * file, you can obtain one at http://mozilla.org/mpl/2.0/. */
+
+/*
+ * This file helps tracking Javascript object allocations.
+ * It is only included in local builds as a debugging helper.
+ *
+ * It is typicaly used when running DevTools tests (either mochitests or DAMP).
+ * To use it, you need to set the following environment variable:
+ * DEBUG_DEVTOOLS_ALLOCATIONS="normal"
+ * This will only print the number of JS objects created during your test.
+ * DEBUG_DEVTOOLS_ALLOCATIONS="verbose"
+ * This will print the allocation sites of all the JS objects created during your
+ * test. i.e. from which files and lines the objects have been created.
+ * In both cases, look for "DEVTOOLS ALLOCATION" in your terminal to see tracker's
+ * output.
+ *
+ * But you can also import it from your test script if you want to focus on one
+ * particular piece of code:
+ * const { allocationTracker } =
+ * require("devtools/shared/test-helpers/allocation-tracker");
+ * // Calling `allocationTracker` will immediately start recording allocations
+ * let tracker = allocationTracker();
+ *
+ * // Do something
+ *
+ * // If you want to log all the allocation sites, call this method:
+ * tracker.logAllocationLog();
+ * // Or, if you want to only print the number of objects being allocated, call this:
+ * tracker.logCount();
+ * // Once you are done, stop the tracker as it slow down execution a lot.
+ * tracker.stop();
+ */
+
+"use strict";
+
+const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
+ Ci.nsIMemoryReporterManager
+);
+
+const global = Cu.getGlobalForObject(this);
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+addDebuggerToGlobal(global);
+
+/**
+ * Start recording JS object allocations.
+ *
+ * @param Object watchGlobal
+ * One global object to observe. Only allocation made from this global
+ * will be recorded.
+ * @param Boolean watchAllGlobals
+ * If true, allocations from everywhere are going to be recorded.
+ * @param Boolean watchAllGlobals
+ * If true, only allocations made from DevTools contexts are going to be recorded.
+ */
+exports.allocationTracker = function ({
+ watchGlobal,
+ watchAllGlobals,
+ watchDevToolsGlobals,
+} = {}) {
+ dump("DEVTOOLS ALLOCATION: Start logging allocations\n");
+ let dbg = new global.Debugger();
+
+ // Enable allocation site tracking, to have the stack for each allocation
+ dbg.memory.trackingAllocationSites = true;
+ // Force saving *all* the allocation sites
+ dbg.memory.allocationSamplingProbability = 1.0;
+ // Bumps the default buffer size, which may prevent recording all the test allocations
+ dbg.memory.maxAllocationsLogLength = 5000000;
+
+ let acceptGlobal;
+ if (watchGlobal) {
+ acceptGlobal = () => false;
+ dbg.addDebuggee(watchGlobal);
+ } else if (watchAllGlobals) {
+ acceptGlobal = () => true;
+ } else if (watchDevToolsGlobals) {
+ // Only accept globals related to DevTools
+ const builtinGlobal = require("resource://devtools/shared/loader/builtin-modules.js");
+ acceptGlobal = g => {
+ // self-hosting-global crashes when trying to call unsafeDereference
+ if (g.class == "self-hosting-global") {
+ dump("TRACKER NEW GLOBAL: - : " + g.class + "\n");
+ return false;
+ }
+ let ref = g.unsafeDereference();
+ // If we are on a toolbox's iframe, typically each panel's iframe
+ // retrieve the toolbox iframe via window.top
+ if (g.class == "Window" && ref.top) {
+ ref = ref.top;
+ }
+ const location = Cu.getRealmLocation(ref);
+ let accept = !!location.match(/devtools/i);
+
+ // Also ignore the dedicated Sandbox used to spawn builtin-modules,
+ // as well as its internal ChromeDebugger Sandbox.
+ // We ignore the global used by the dedicated loader used to load
+ // the allocation-tracker module.
+ if (
+ ref == Cu.getGlobalForObject(builtinGlobal) ||
+ ref == Cu.getGlobalForObject(builtinGlobal.modules.ChromeDebugger)
+ ) {
+ accept = false;
+ }
+
+ dump(
+ "TRACKER NEW GLOBAL: " + (accept ? "+" : "-") + " : " + location + "\n"
+ );
+ return accept;
+ };
+ }
+
+ // Watch all globals
+ if (watchAllGlobals || watchDevToolsGlobals) {
+ dbg.addAllGlobalsAsDebuggees();
+
+ for (const g of dbg.getDebuggees()) {
+ if (!acceptGlobal(g)) {
+ dbg.removeDebuggee(g);
+ }
+ }
+ }
+
+ // Remove this global to ignore all its object/JS
+ dbg.removeDebuggee(global);
+
+ // addAllGlobalsAsDebuggees won't automatically track new ones,
+ // so ensure tracking all new globals
+ dbg.onNewGlobalObject = function (g) {
+ if (acceptGlobal(g)) {
+ dbg.addDebuggee(g);
+ }
+ };
+
+ return {
+ get overflowed() {
+ return dbg.memory.allocationsLogOverflowed;
+ },
+
+ async startRecordingAllocations(debug_allocations) {
+ // Do a first pass of GC, to ensure all to-be-freed objects from the first run
+ // are really freed.
+ // We have to temporarily disable allocation-site recording in order to ensure
+ // freeing everything and especially avoid retaining objects in the allocation-log
+ // related to `drainAllocationLog` feature.
+ dbg.memory.allocationSamplingProbability = 0.0;
+ // Also force clearing the allocation log in order to prevent holding alive globals
+ // which have been destroyed before we start recording
+ this.flushAllocations();
+ await this.doGC();
+ dbg.memory.allocationSamplingProbability = 1.0;
+
+ // Measure the current process memory usage
+ const memory = this.getAllocatedMemory();
+
+ // Then, record how many objects were already allocated, which should not be declared
+ // as potential leaks. For ex, there is all the modules already loaded
+ // in the main DevTools loader.
+ const objects = this.stillAllocatedObjects();
+
+ // Flush the allocations so that the next call to logAllocationLog
+ // ignore allocations which happened before this call.
+ if (debug_allocations == "allocations") {
+ this.flushAllocations();
+ }
+
+ // Retrieve all allocation sites of all the objects already allocated.
+ // So that we can ignore them when we stop the record.
+ const allocations =
+ debug_allocations == "leaks" ? this.getAllAllocations() : null;
+
+ this.data = { memory, objects, allocations };
+ return this.data;
+ },
+
+ async stopRecordingAllocations(debug_allocations) {
+ // We have to flush the allocation log in order to prevent leaking some objects
+ // being hold in memory solely by their allocation-site (i.e. `SavedFrame` in `Debugger::allocationsLog`)
+ if (debug_allocations != "allocations") {
+ this.flushAllocations();
+ }
+
+ // In the content process we watch for all globals.
+ // Disable allocation record immediately, as we get some allocation reported by the allocation-tracker itself.
+ if (watchAllGlobals) {
+ dbg.memory.allocationSamplingProbability = 0.0;
+ }
+
+ // Before computing allocations, re-do some GCs in order to free all what is to-be-freed.
+ await this.doGC();
+
+ // If we are in the parent process, we watch only for devtools globals.
+ // So we can more safely assert that no allocation occured while doing the GCs.
+ // If means that the test we are recording is having pending operation which aren't properly recorded.
+ if (!watchAllGlobals) {
+ const allocations = dbg.memory.drainAllocationsLog();
+ if (allocations.length) {
+ this.logAllocationLog(
+ allocations,
+ "Allocation that happened during the GC"
+ );
+ console.error(
+ "Allocation happened during the GC. Are you waiting correctly before calling stopRecordingAllocations?"
+ );
+ }
+ }
+
+ const memory = this.getAllocatedMemory();
+ const objects = this.stillAllocatedObjects();
+
+ let leaks;
+ if (debug_allocations == "allocations") {
+ this.logAllocationLog();
+ } else if (debug_allocations == "leaks") {
+ leaks = this.logAllocationSitesDiff(this.data.allocations);
+ }
+
+ return {
+ objectsWithoutStack:
+ objects.objectsWithoutStack - this.data.objects.objectsWithoutStack,
+ objectsWithStack:
+ objects.objectsWithStack - this.data.objects.objectsWithStack,
+ memory: memory - this.data.memory,
+ leaks,
+ };
+ },
+
+ /**
+ * Return the collection of currently allocated JS Objects.
+ *
+ * This returns an object whose structure is documented in logAllocationSites.
+ */
+ getAllAllocations() {
+ const sensus = dbg.memory.takeCensus({
+ breakdown: { by: "allocationStack" },
+ });
+ const sources = {};
+ for (const [k, v] of sensus.entries()) {
+ const src = k.source || "UNKNOWN";
+ const line = k.line || "?";
+ const count = v.count;
+
+ let item = sources[src];
+ if (!item) {
+ item = sources[src] = { count: 0, lines: {} };
+ }
+ item.count += count;
+ if (line != -1) {
+ if (!item.lines[line]) {
+ item.lines[line] = 0;
+ }
+ item.lines[line] += count;
+ }
+ }
+ return sources;
+ },
+
+ /**
+ * Substract count of `previousSources` from `newSources`.
+ * This help know which allocations where done between `previousSources` and `newSources` records,
+ * and, are still allocated.
+ *
+ * The structure of source objects is documented in logAllocationSites.
+ */
+ sourcesDiff(previousSources, newSources) {
+ for (const src in previousSources) {
+ const previousItem = previousSources[src];
+ const item = newSources[src];
+ if (!item) {
+ continue;
+ }
+ item.count -= previousItem.count;
+
+ for (const line in previousItem.lines) {
+ const count = previousItem.lines[line];
+ if (line != -1) {
+ if (!item.lines[line]) {
+ continue;
+ }
+ item.lines[line] -= count;
+ }
+ }
+ }
+ },
+
+ /**
+ * Print to stdout data about all recorded allocations
+ *
+ * It prints an array of allocations per file, sorted by files allocating the most
+ * objects. And get detail of allocation per line.
+ *
+ * [{ src: "chrome://devtools/content/framework/toolbox.js",
+ * count: 210, // Total # of allocs for toolbox.js
+ * lines: [
+ * "10: 200", // toolbox.js allocation 200 objects on line 10
+ * "124: 10
+ * ]
+ * },
+ * { src: "chrome://devtools/content/inspector/inspector.js",
+ * count: 12,
+ * lines: [
+ * "20: 12",
+ * ]
+ * }]
+ *
+ * @param first Number
+ * Retrieve only the top $first script allocation the most
+ * objects
+ */
+ logAllocationSites(message, sources, { first = 1000 } = {}) {
+ const allocationList = Object.entries(sources)
+ // Sort by number of total object
+ .sort(([srcA, itemA], [srcB, itemB]) => itemB.count - itemA.count)
+ // Keep only the first n-th sources, with the most allocations
+ .filter((_, i) => i < first)
+ .map(([src, item]) => {
+ const lines = [];
+ Object.entries(item.lines)
+ // Filter out lines where we only freed objects
+ .filter(([line, count]) => count > 0)
+ .sort(([lineA, countA], [lineB, countB]) => {
+ if (countA != countB) {
+ return countB - countA;
+ }
+ return lineB - lineA;
+ })
+ .forEach(([line, count]) => {
+ // Compress the data to make it readable on stdout
+ lines.push(line + ": " + count);
+ });
+ return { src, count: item.count, lines };
+ })
+ // Filter out modules where we only freed objects
+ .filter(({ count }) => count > 0);
+ dump(
+ "DEVTOOLS ALLOCATION: " +
+ message +
+ ":\n" +
+ JSON.stringify(allocationList, null, 2) +
+ "\n"
+ );
+ return allocationList;
+ },
+
+ /**
+ * This method requires a previous call to getAllAllocations
+ * and will print only the allocation sites which are still allocated.
+ * Usage:
+ * const previousSources = this.getAllAllocations();
+ * ... exercice something, which may leak ...
+ * this.logAllocationSitesDiff(previousSources);
+ */
+ logAllocationSitesDiff(previousSources) {
+ const newSources = this.getAllAllocations();
+ this.sourcesDiff(previousSources, newSources);
+ return this.logAllocationSites("allocations which leaked", newSources);
+ },
+
+ /**
+ * Convert allocation structure coming out from Memory API's `drainAllocationsLog()`
+ * to source structure documented in logAllocationSites.
+ */
+ allocationsToSources(allocations) {
+ const sources = {};
+ for (const alloc of allocations) {
+ const { frame } = alloc;
+ let src = "UNKNOWN";
+ let line = -1;
+ try {
+ if (frame) {
+ src = frame.source || "UNKNOWN";
+ line = frame.line || -1;
+ }
+ } catch (e) {
+ // For some frames accessing source throws
+ }
+ let item = sources[src];
+ if (!item) {
+ item = sources[src] = { count: 0, lines: {} };
+ }
+ item.count++;
+ if (line != -1) {
+ if (!item.lines[line]) {
+ item.lines[line] = 0;
+ }
+ item.lines[line]++;
+ }
+ }
+ return sources;
+ },
+
+ /**
+ * This method will log all the allocations that happened since the last call
+ * to this method -or- to `flushAllocations`.
+ * Reported allocations may have been freed.
+ * Use `logAllocationSitesDiff` to know what hasn't been freed.
+ */
+ logAllocationLog(allocations, msg = "") {
+ if (!allocations) {
+ allocations = dbg.memory.drainAllocationsLog();
+ }
+ const sources = this.allocationsToSources(allocations);
+ return this.logAllocationSites(
+ msg
+ ? msg
+ : "all allocations (which may be freed or are still allocated)",
+ sources
+ );
+ },
+
+ logCount() {
+ dump(
+ "DEVTOOLS ALLOCATION: Javascript object allocations: " +
+ this.countAllocations() +
+ "\n"
+ );
+ },
+
+ countAllocations() {
+ // Fetch all allocation sites from Debugger API
+ const allocations = dbg.memory.drainAllocationsLog();
+ return allocations.length;
+ },
+
+ /**
+ * Reset the allocation log, so that the next call to logAllocationLog/drainAllocationsLog
+ * will report all allocations which happened after this call to flushAllocations.
+ */
+ flushAllocations() {
+ dbg.memory.drainAllocationsLog();
+ },
+
+ /**
+ * Compute the live count of object currently allocated.
+ *
+ * `objects` attribute will count all the objects,
+ * while `objectsWithNoStack` will report how many are missing allocation site/stack.
+ */
+ stillAllocatedObjects() {
+ const sensus = dbg.memory.takeCensus({
+ breakdown: { by: "allocationStack" },
+ });
+ let objectsWithStack = 0;
+ let objectsWithoutStack = 0;
+ for (const [k, v] of sensus.entries()) {
+ // Objects with missing stack will all be keyed under "noStack" string,
+ // while all others will have a stack object as key.
+ if (k === "noStack") {
+ objectsWithoutStack += v.count;
+ } else {
+ objectsWithStack += v.count;
+ }
+ }
+ return { objectsWithStack, objectsWithoutStack };
+ },
+
+ /**
+ * Reports the amount of OS memory used by the current process.
+ */
+ getAllocatedMemory() {
+ return MemoryReporter.residentUnique;
+ },
+
+ async doGC() {
+ // In order to get stable results, we really have to do 3 GC attempts
+ // *and* do wait for 1s between each GC.
+ const numCycles = 3;
+ for (let i = 0; i < numCycles; i++) {
+ Cu.forceGC();
+ Cu.forceCC();
+ await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ // Also call minimizeMemoryUsage as that's the only way to purge JIT cache.
+ // CachedIR objects (JIT related objects) are ultimately leading to keep
+ // all transient globals in memory. For some reason, when enabling trackingAllocationSites=true
+ // we compute stack traces (SavedFrame) for each object being allocated.
+ // This either create new CachedIR -or- force holding alive existing CachedIR
+ // and CachedIR itself hold strong references to the transient globals.
+ // See bug 1733480.
+ await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
+ },
+
+ /**
+ * Return the absolute file path to a memory snapshot.
+ * This is used to compute dominator trees in `traceObjects`.
+ */
+ getSnapshotFile() {
+ return ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ },
+
+ /**
+ * Print information about why a list of objects are being held in memory.
+ *
+ * @param Array<NodeId> objects
+ * List of NodeId's of objects to debug. NodeIds can be retrieved
+ * via ChromeUtils.getObjectNodeId.
+ * @param String snapshotFile
+ * Absolute path to a Heap snapshot file retrieved via this.getSnapshotFile.
+ * This is used to trace content process objects. We have to record the snapshot
+ * from the content process, but can only read it from the parent process because
+ * of I/O restrictions in content processes.
+ */
+ traceObjects(objects, snapshotFile) {
+ // There is no API to get the heap snapshot at runtime,
+ // the only way is to save it to disk and then load it from disk
+ if (!snapshotFile) {
+ snapshotFile = this.getSnapshotFile();
+ }
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFile);
+
+ function getObjectClass(id) {
+ if (!id) {
+ return "<null>";
+ }
+ try {
+ let stack = [...snapshot.describeNode({ by: "allocationStack" }, id)];
+ let line;
+ if (stack) {
+ stack = stack.find(([src]) => src != "noStack");
+ if (stack) {
+ line = stack[0].line;
+ stack = stack[0].source;
+ if (stack) {
+ const pstack = stack;
+ stack = stack.match(/\/([^\/]+)$/);
+ if (stack) {
+ stack = stack[1];
+ } else {
+ stack = pstack;
+ }
+ } else {
+ stack = "no-source";
+ }
+ } else {
+ stack = "no-stack";
+ }
+ } else {
+ stack = "no-desc";
+ }
+ return (
+ Object.entries(
+ snapshot.describeNode({ by: "objectClass" }, id)
+ )[0][0] + (stack ? "@" + stack + ":" + line : "")
+ );
+ } catch (e) {
+ if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
+ return "<not-in-memory-snapshot:is-from-untracked-global?>";
+ }
+ return "<invalid:" + id + ":" + e + ">";
+ }
+ }
+ function printPath(src, dst) {
+ let paths;
+ try {
+ paths = snapshot.computeShortestPaths(src, [dst], 10);
+ } catch (e) {}
+ if (paths && paths.has(dst)) {
+ let pathLength = Infinity;
+ for (const path of paths.get(dst)) {
+ // Only print the smaller paths.
+ // The longer ones will only repeat the smaller ones, with some extra edges.
+ if (path.length > pathLength) {
+ continue;
+ }
+ pathLength = path.length;
+ dump(
+ "- " +
+ path
+ .map(
+ ({ predecessor, edge }) =>
+ getObjectClass(predecessor) + "." + edge
+ )
+ .join("\n \\--> ") +
+ "\n \\--> " +
+ getObjectClass(dst) +
+ "\n"
+ );
+ }
+ } else {
+ dump("NO-PATH\n");
+ }
+ }
+
+ const tree = snapshot.computeDominatorTree();
+ for (const objectNodeId of objects) {
+ dump(" # Tracing: " + getObjectClass(objectNodeId) + "\n");
+
+ // Print the path from the global object down to leaked object.
+ // This print the allocation site of each object which has a reference
+ // to another object, ultimately leading to our leaked object.
+ dump("### Path(s) from root:\n");
+ printPath(tree.root, objectNodeId);
+
+ /**
+ * This happens to be redundant with printPath, but printed the other way around.
+ *
+ // Print the dominators.
+ // i.e. from the leaked object, print all parent objects whichs
+ // keeps a reference to the previous object, up to a global object.
+ dump("### Dominators:\n");
+ let node = objectNodeId,
+ dump(" " + getObjectClass(node) + "\n");
+ while ((node = tree.getImmediateDominator(node))) {
+ dump(" ^-- " + getObjectClass(node) + "\n");
+ }
+ */
+
+ /**
+ * In case you are not able to figure out what the object is.
+ * This will print all what it keeps allocated,
+ * kinds of list of attributes
+ *
+ dump("### Dominateds:\n");
+ node = objectNodeId,
+ dump(" " + getObjectClass(node) + "\n");
+ for (const n of tree.getImmediatelyDominated(objectNodeId)) {
+ dump(" --> " + getObjectClass(n) + "\n");
+ }
+ */
+ }
+ },
+
+ stop() {
+ dump("DEVTOOLS ALLOCATION: Stop logging allocations\n");
+ dbg.onNewGlobalObject = undefined;
+ dbg.removeAllDebuggees();
+ dbg = null;
+ },
+ };
+};
diff --git a/devtools/shared/test-helpers/browser.toml b/devtools/shared/test-helpers/browser.toml
new file mode 100644
index 0000000000..f1e5775f91
--- /dev/null
+++ b/devtools/shared/test-helpers/browser.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = ["allocation-tracker.js"]
+
+["browser_allocation_tracker.js"]
+skip-if = [
+ "debug", # Bug 1730507 - objects without stacks get allocated during the GC of the first test when running multiple times. Also avoid running in debug as we don't try to track memory from debug builds. And ccov as this doesn't aim to cover any production code, we are only testing test helpers here.
+ "verify",
+ "ccov",
+]
diff --git a/devtools/shared/test-helpers/browser_allocation_tracker.js b/devtools/shared/test-helpers/browser_allocation_tracker.js
new file mode 100644
index 0000000000..33303ddee8
--- /dev/null
+++ b/devtools/shared/test-helpers/browser_allocation_tracker.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load the tracker in a dedicated loader using invisibleToDebugger and freshCompartment
+// so that it can inspect any other module/compartment, even DevTools, chrome,
+// and this script!
+const { DevToolsLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ freshCompartment: true,
+});
+const { allocationTracker } = loader.require(
+ "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker"
+);
+const TrackedObjects = loader.require(
+ "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
+);
+
+// This test record multiple times complete heap snapshot,
+// so that it can take a little bit to complete.
+requestLongerTimeout(2);
+
+add_task(async function () {
+ // Use a sandbox to allocate test javascript object in order to avoid any
+ // external noise
+ const global = Cu.Sandbox("http://example.com");
+
+ const tracker = allocationTracker({ watchGlobal: global });
+ const before = tracker.stillAllocatedObjects();
+
+ /* eslint-disable no-undef */
+ // This will allocation 1001 objects. The array and 1000 elements in it.
+ Cu.evalInSandbox(
+ "let list; new " +
+ function () {
+ list = [];
+ for (let i = 0; i < 1000; i++) {
+ list.push({});
+ }
+ },
+ global,
+ undefined,
+ "test-file.js",
+ 1,
+ /* enforceFilenameRestrictions */ false
+ );
+ /* eslint-enable no-undef */
+
+ const allocations = tracker.countAllocations();
+ Assert.greaterOrEqual(
+ allocations,
+ 1001,
+ `At least 1001 objects are reported as created (${allocations})`
+ );
+
+ // Uncomment this and comment the call to `countAllocations` to debug the allocations.
+ // The call to `countAllocations` will reset the allocation record.
+ // tracker.logAllocationSites();
+
+ const afterCreation = tracker.stillAllocatedObjects();
+ is(
+ afterCreation.objectsWithStack - before.objectsWithStack,
+ 1001,
+ "We got exactly the expected number of objects recorded with an allocation site"
+ );
+ Assert.greater(
+ afterCreation.objectsWithStack,
+ before.objectsWithStack,
+ "We got some random number of objects without an allocation site"
+ );
+
+ Cu.evalInSandbox(
+ "list = null;",
+ global,
+ undefined,
+ "test-file.js",
+ 7,
+ /* enforceFilenameRestrictions */ false
+ );
+
+ Cu.forceGC();
+ Cu.forceCC();
+
+ const afterGC = tracker.stillAllocatedObjects();
+ is(
+ afterCreation.objectsWithStack - afterGC.objectsWithStack,
+ 1001,
+ "All the expected objects were reported freed in the count with allocation sites"
+ );
+ Assert.less(
+ afterGC.objectsWithoutStack,
+ afterCreation.objectsWithoutStack,
+ "And we released some random number of objects without an allocation site"
+ );
+
+ tracker.stop();
+});
+
+add_task(async function () {
+ const leaked = {};
+ TrackedObjects.track(leaked);
+ let transient = {};
+ TrackedObjects.track(transient);
+
+ is(TrackedObjects.getAllNodeIds().length, 2, "The two objects are reported");
+
+ info("Free the transient object");
+ transient = null;
+ Cu.forceGC();
+
+ is(
+ TrackedObjects.getAllNodeIds().length,
+ 1,
+ "We now only have the leaked object"
+ );
+ TrackedObjects.clear();
+});
+
+add_task(async function () {
+ info("Test start and stop recording without any debug mode");
+ const tracker = allocationTracker({ watchDevToolsGlobals: true });
+ await tracker.startRecordingAllocations();
+ await tracker.stopRecordingAllocations();
+ tracker.stop();
+});
+
+add_task(async function () {
+ info("Test start and stop recording with 'allocations' debug mode");
+ const tracker = allocationTracker({ watchDevToolsGlobals: true });
+ await tracker.startRecordingAllocations("allocations");
+ await tracker.stopRecordingAllocations("allocations");
+ tracker.stop();
+});
+
+add_task(async function () {
+ info("Test start and stop recording with 'leaks' debug mode");
+ const tracker = allocationTracker({ watchDevToolsGlobals: true });
+ await tracker.startRecordingAllocations("leaks");
+ await tracker.stopRecordingAllocations("leaks");
+ tracker.stop();
+});
+
+add_task(async function () {
+ info("Test start and stop recording with tracked objects");
+
+ const leaked = {};
+ TrackedObjects.track(leaked);
+
+ const tracker = allocationTracker({ watchAllGlobals: true });
+ await tracker.startRecordingAllocations();
+ await tracker.stopRecordingAllocations();
+ tracker.stop();
+
+ TrackedObjects.clear();
+});
+
+add_task(async function () {
+ info("Test start and stop recording with tracked objects");
+
+ const sandbox = Cu.Sandbox(window);
+ const tracker = allocationTracker({ watchGlobal: sandbox });
+ await tracker.startRecordingAllocations("leaks");
+
+ Cu.evalInSandbox("this.foo = {};", sandbox, null, "sandbox.js", 1);
+
+ const record = await tracker.stopRecordingAllocations("leaks");
+ is(
+ record.objectsWithStack,
+ 1,
+ "We get only one leaked objects, the foo object of the sandbox."
+ );
+ Assert.greater(
+ record.objectsWithoutStack,
+ 10,
+ "We get an handful of objects without stacks. Most likely created by Memory API itself."
+ );
+
+ is(
+ record.leaks.length,
+ 2,
+ "We get the one leak and the objects with missing stacks"
+ );
+ is(
+ record.leaks[0].src,
+ "UNKNOWN",
+ "First item is the objects with missing stacks"
+ );
+ // In theory the two following values should be equal,
+ // but they aren't always because of some dark matter around objects with missing stacks.
+ // `count` is computed out of `takeCensus`, while `objectsWithoutStack` uses `drainAllocationsLog`
+ // While the first go through the current GC graph, the second is a record of allocations over time,
+ // this probably explain why there is some subtle difference
+ Assert.lessOrEqual(
+ record.leaks[0].count,
+ record.objectsWithoutStack,
+ "For now, the leak report intermittently assume there is less leaked objects than the summary"
+ );
+ is(record.leaks[1].src, "sandbox.js", "Second item if about our 'foo' leak");
+ is(record.leaks[1].count, 1, "We leak one object on this file");
+ is(record.leaks[1].lines.length, 1, "We leak from only one line");
+ is(record.leaks[1].lines[0], "1: 1", "On first line, we leak one object");
+ tracker.stop();
+
+ TrackedObjects.clear();
+});
+
+add_task(async function () {
+ info("Test that transient globals are not leaked");
+
+ const tracker = allocationTracker({ watchAllGlobals: true });
+
+ let sandboxBefore = Cu.Sandbox(window);
+ // We need to allocate at least one object from the global to reproduce the leak
+ Cu.evalInSandbox(
+ "this.foo = {};",
+ sandboxBefore,
+ null,
+ "sandbox-before.js",
+ 1
+ );
+ const weakBefore = Cu.getWeakReference(sandboxBefore);
+ sandboxBefore = null;
+
+ await tracker.startRecordingAllocations();
+
+ ok(
+ !weakBefore.get(),
+ "Sandbox created before the record should have been freed by GCs done by startRecordingAllocations"
+ );
+
+ let sandboxDuring = Cu.Sandbox(window);
+ // We need to allocate at least one object from the global to reproduce the leak
+ Cu.evalInSandbox(
+ "this.bar = {};",
+ sandboxDuring,
+ null,
+ "sandbox-during.js",
+ 1
+ );
+ const weakDuring = Cu.getWeakReference(sandboxDuring);
+ sandboxDuring = null;
+
+ await tracker.stopRecordingAllocations();
+
+ ok(
+ !weakDuring.get(),
+ "Sandbox should have been freed by GCs done by stopRecordingAllocations"
+ );
+
+ tracker.stop();
+});
diff --git a/devtools/shared/test-helpers/moz.build b/devtools/shared/test-helpers/moz.build
new file mode 100644
index 0000000000..92b1d5212d
--- /dev/null
+++ b/devtools/shared/test-helpers/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(
+ "thread-helpers.sys.mjs",
+ "tracked-objects.sys.mjs",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "General")
diff --git a/devtools/shared/test-helpers/test_javascript_tracer.js b/devtools/shared/test-helpers/test_javascript_tracer.js
new file mode 100644
index 0000000000..268e054c56
--- /dev/null
+++ b/devtools/shared/test-helpers/test_javascript_tracer.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test for the thread helpers utility module.
+ *
+ * This uses a xpcshell test in order to avoid recording the noise
+ * of all Firefox components when using a mochitest.
+ */
+
+const { traceAllJSCalls } = ChromeUtils.importESModule(
+ "resource://devtools/shared/test-helpers/thread-helpers.sys.mjs"
+);
+// ESLint thinks this is a browser test, but it's actually an xpcshell
+// test and so `setTimeout` isn't available out of the box.
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+add_task(async function sanityCheck() {
+ let ranTheOtherEventLoop = false;
+ setTimeout(function otherEventLoop() {
+ ranTheOtherEventLoop = true;
+ }, 0);
+ const jsTracer = traceAllJSCalls();
+ function foo() {}
+ for (let i = 0; i < 10; i++) {
+ foo();
+ }
+ jsTracer.stop();
+ ok(
+ !ranTheOtherEventLoop,
+ "When we don't pause frame execution, the other event do not execute"
+ );
+});
+
+add_task(async function withPrefix() {
+ const jsTracer = traceAllJSCalls({ prefix: "my-prefix" });
+ function foo() {}
+ for (let i = 0; i < 10; i++) {
+ foo();
+ }
+ jsTracer.stop();
+ ok(true, "Were able to run with a prefix argument");
+});
+
+add_task(async function pause() {
+ const start = Cu.now();
+ let ranTheOtherEventLoop = false;
+ setTimeout(function otherEventLoop() {
+ ranTheOtherEventLoop = true;
+ }, 0);
+ const jsTracer = traceAllJSCalls({ pause: 100 });
+ function foo() {}
+ for (let i = 0; i < 10; i++) {
+ foo();
+ }
+ jsTracer.stop();
+ const duration = Cu.now() - start;
+ Assert.greater(
+ duration,
+ 10 * 100,
+ "The execution of the for loop was slow down by at least the pause duration in each loop"
+ );
+ ok(
+ ranTheOtherEventLoop,
+ "When we pause frame execution, the other event can execute"
+ );
+});
diff --git a/devtools/shared/test-helpers/thread-helpers.sys.mjs b/devtools/shared/test-helpers/thread-helpers.sys.mjs
new file mode 100644
index 0000000000..b99fc16f00
--- /dev/null
+++ b/devtools/shared/test-helpers/thread-helpers.sys.mjs
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Helper code to play with the javascript thread
+ **/
+
+function getSandboxWithDebuggerSymbol() {
+ // Bug 1835268 - Changing this to an ES module import currently throws an
+ // assertion in test_javascript_tracer.js in debug builds.
+ const { addDebuggerToGlobal } = ChromeUtils.import(
+ "resource://gre/modules/jsdebugger.jsm"
+ );
+ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+ const debuggerSandbox = Cu.Sandbox(systemPrincipal, {
+ // This sandbox is also reused for ChromeDebugger implementation.
+ // As we want to load the `Debugger` API for debugging chrome contexts,
+ // we have to ensure loading it in a distinct compartment from its debuggee.
+ freshCompartment: true,
+ invisibleToDebugger: true,
+ });
+ addDebuggerToGlobal(debuggerSandbox);
+
+ return debuggerSandbox;
+}
+
+/**
+ * Implementation of a Javascript tracer logging traces to stdout.
+ *
+ * To be used like this:
+
+ const { traceAllJSCalls } = ChromeUtils.importESModule(
+ "resource://devtools/shared/test-helpers/thread-helpers.sys.mjs"
+ );
+ const jsTracer = traceAllJSCalls();
+ [... execute some code to tracer ...]
+ jsTracer.stop();
+
+ * @param prefix String
+ * Optional, if passed, this will be displayed in front of each
+ * line reporting a new frame execution.
+ * @param pause Number
+ * Optional, if passed, hold off each frame for `pause` ms,
+ * by letting the other event loops run in between.
+ * Be careful that it can introduce unexpected race conditions
+ * that can't necessarily be reproduced without this.
+ */
+export function traceAllJSCalls({ prefix = "", pause } = {}) {
+ const debuggerSandbox = getSandboxWithDebuggerSymbol();
+
+ debuggerSandbox.Services = Services;
+ const f = Cu.evalInSandbox(
+ "(" +
+ function (pauseInMs, prefixString) {
+ const dbg = new Debugger();
+ // Add absolutely all the globals...
+ dbg.addAllGlobalsAsDebuggees();
+ // ...but avoid tracing this sandbox code
+ const global = Cu.getGlobalForObject(this);
+ dbg.removeDebuggee(global);
+
+ // Add all globals created later on
+ dbg.onNewGlobalObject = g => dbg.addDebuggee(g);
+
+ function formatDisplayName(frame) {
+ if (frame.type === "call") {
+ const callee = frame.callee;
+ return callee.name || callee.userDisplayName || callee.displayName;
+ }
+
+ return `(${frame.type})`;
+ }
+
+ function stop() {
+ dbg.onEnterFrame = undefined;
+ dbg.removeAllDebuggees();
+ }
+ global.stop = stop;
+
+ let depth = 0;
+ dbg.onEnterFrame = frame => {
+ if (depth == 100) {
+ dump(
+ "Looks like an infinite loop? We stop the js tracer, but code may still be running!\n"
+ );
+ stop();
+ return;
+ }
+
+ const { script } = frame;
+ const { lineNumber, columnNumber } = script.getOffsetMetadata(
+ frame.offset
+ );
+ const padding = new Array(depth).join(" ");
+ dump(
+ `${prefixString}${padding}--[${frame.implementation}]--> ${
+ script.source.url
+ } @ ${lineNumber}:${columnNumber} - ${formatDisplayName(frame)}\n`
+ );
+
+ depth++;
+ frame.onPop = () => {
+ depth--;
+ };
+
+ // Optionaly pause the frame execute by letting the other event loop to run in between.
+ if (typeof pauseInMs == "number") {
+ let freeze = true;
+ const timer = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+ timer.initWithCallback(
+ () => {
+ freeze = false;
+ },
+ pauseInMs,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ Services.tm.spinEventLoopUntil("debugger-slow-motion", function () {
+ return !freeze;
+ });
+ }
+ };
+
+ return { stop };
+ } +
+ ")",
+ debuggerSandbox,
+ undefined,
+ "debugger-javascript-tracer",
+ 1,
+ /* enforceFilenameRestrictions */ false
+ );
+ f(pause, prefix);
+
+ return {
+ stop() {
+ debuggerSandbox.stop();
+ },
+ };
+}
diff --git a/devtools/shared/test-helpers/tracked-objects.sys.mjs b/devtools/shared/test-helpers/tracked-objects.sys.mjs
new file mode 100644
index 0000000000..54f53fb5fa
--- /dev/null
+++ b/devtools/shared/test-helpers/tracked-objects.sys.mjs
@@ -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/. */
+
+// Test-only module in order to register objects later inspected by
+// the allocation tracker (in the same folder).
+//
+// We are going to store a weak reference to the passed objects,
+// in order to prevent holding them in memory.
+// Allocation tracker will then print detailed information
+// about why these objects are still allocated.
+
+const objects = [];
+
+/**
+ * Request to track why the given object is kept in memory,
+ * later on, when retrieving all the watched object via getAllNodeIds.
+ */
+export function track(obj) {
+ // We store a weak reference, so that we do force keeping the object in memory!!
+ objects.push(Cu.getWeakReference(obj));
+}
+
+/**
+ * Return the NodeId's of all the objects passed via `track()` method.
+ *
+ * NodeId's are used by spidermonkey memory API to designates JS objects in head snapshots.
+ */
+export function getAllNodeIds() {
+ // Filter out objects which have been freed already
+ return (
+ objects
+ .map(weak => weak.get())
+ .filter(obj => !!obj)
+ // Convert objects from here instead of from allocation tracker in order
+ // to be from the shared system compartment and avoid trying to compute the NodeId
+ // of a wrapper!
+ .map(ChromeUtils.getObjectNodeId)
+ );
+}
+
+/**
+ * Used by tests to clear all tracked objects
+ */
+export function clear() {
+ objects.length = 0;
+}
diff --git a/devtools/shared/test-helpers/xpcshell.toml b/devtools/shared/test-helpers/xpcshell.toml
new file mode 100644
index 0000000000..5ded960f83
--- /dev/null
+++ b/devtools/shared/test-helpers/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = "devtools"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_javascript_tracer.js"]
diff --git a/devtools/shared/tests/browser/browser.toml b/devtools/shared/tests/browser/browser.toml
new file mode 100644
index 0000000000..e39e8c39ae
--- /dev/null
+++ b/devtools/shared/tests/browser/browser.toml
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "../../../server/tests/browser/head.js",
+]
+
+["browser_async_storage.js"]
+
+["browser_l10n_localizeMarkup.js"]
diff --git a/devtools/shared/tests/browser/browser_async_storage.js b/devtools/shared/tests/browser/browser_async_storage.js
new file mode 100644
index 0000000000..87a1ef169f
--- /dev/null
+++ b/devtools/shared/tests/browser/browser_async_storage.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the basic functionality of async-storage.
+// Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/apps/sharedtest/test/unit/async_storage_test.js.
+
+const asyncStorage = require("resource://devtools/shared/async-storage.js");
+add_task(async function () {
+ is(typeof asyncStorage.length, "function", "API exists.");
+ is(typeof asyncStorage.key, "function", "API exists.");
+ is(typeof asyncStorage.getItem, "function", "API exists.");
+ is(typeof asyncStorage.setItem, "function", "API exists.");
+ is(typeof asyncStorage.removeItem, "function", "API exists.");
+ is(typeof asyncStorage.clear, "function", "API exists.");
+});
+
+add_task(async function () {
+ await asyncStorage.setItem("foo", "bar");
+ let value = await asyncStorage.getItem("foo");
+ is(value, "bar", "value is correct");
+ await asyncStorage.setItem("foo", "overwritten");
+ value = await asyncStorage.getItem("foo");
+ is(value, "overwritten", "value is correct");
+ await asyncStorage.removeItem("foo");
+ value = await asyncStorage.getItem("foo");
+ is(value, null, "value is correct");
+});
+
+add_task(async function () {
+ const object = {
+ x: 1,
+ y: "foo",
+ z: true,
+ };
+
+ await asyncStorage.setItem("myobj", object);
+ let value = await asyncStorage.getItem("myobj");
+ is(object.x, value.x, "value is correct");
+ is(object.y, value.y, "value is correct");
+ is(object.z, value.z, "value is correct");
+ await asyncStorage.removeItem("myobj");
+ value = await asyncStorage.getItem("myobj");
+ is(value, null, "value is correct");
+});
+
+add_task(async function () {
+ await asyncStorage.clear();
+ let len = await asyncStorage.length();
+ is(len, 0, "length is correct");
+ await asyncStorage.setItem("key1", "value1");
+ len = await asyncStorage.length();
+ is(len, 1, "length is correct");
+ await asyncStorage.setItem("key2", "value2");
+ len = await asyncStorage.length();
+ is(len, 2, "length is correct");
+ await asyncStorage.setItem("key3", "value3");
+ len = await asyncStorage.length();
+ is(len, 3, "length is correct");
+
+ let key = await asyncStorage.key(0);
+ is(key, "key1", "key is correct");
+ key = await asyncStorage.key(1);
+ is(key, "key2", "key is correct");
+ key = await asyncStorage.key(2);
+ is(key, "key3", "key is correct");
+ key = await asyncStorage.key(3);
+ is(key, null, "key is correct");
+ await asyncStorage.clear();
+ key = await asyncStorage.key(0);
+ is(key, null, "key is correct");
+
+ len = await asyncStorage.length();
+ is(len, 0, "length is correct");
+});
diff --git a/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
new file mode 100644
index 0000000000..f53b9a18ce
--- /dev/null
+++ b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../../server/tests/browser/head.js */
+
+// Tests that the markup localization works properly.
+
+const { localizeMarkup } = require("resource://devtools/shared/l10n.js");
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+add_task(async function () {
+ info("Check that the strings used for this test are still valid");
+ const STARTUP_L10N = new LocalizationHelper(
+ "devtools/client/locales/startup.properties"
+ );
+ const TOOLBOX_L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+ );
+ const str1 = STARTUP_L10N.getStr("inspector.label");
+ const str2 = STARTUP_L10N.getStr("inspector.accesskey");
+ const str3 = TOOLBOX_L10N.getStr("toolbox.defaultTitle");
+ ok(
+ str1 && str2 && str3,
+ "If this failed, strings should be updated in the test"
+ );
+
+ info("Create the test markup");
+ const div = document.createElementNS(HTML_NS, "div");
+ div.setAttribute(
+ "data-localization-bundle",
+ "devtools/client/locales/startup.properties"
+ );
+ const div0 = document.createElementNS(HTML_NS, "div");
+ div0.setAttribute("id", "d0");
+ div0.setAttribute("data-localization", "content=inspector.someInvalidKey");
+ div.appendChild(div0);
+ const div1 = document.createElementNS(HTML_NS, "div");
+ div1.setAttribute("id", "d1");
+ div1.setAttribute("data-localization", "content=inspector.label");
+ div.appendChild(div1);
+ div1.append("Text will disappear");
+ const div2 = document.createElementNS(HTML_NS, "div");
+ div2.setAttribute("id", "d2");
+ div2.setAttribute(
+ "data-localization",
+ "content=inspector.label;title=inspector.accesskey"
+ );
+ div.appendChild(div2);
+ const div3 = document.createElementNS(HTML_NS, "div");
+ div3.setAttribute("id", "d3");
+ div3.setAttribute(
+ "data-localization",
+ "content=inspector.label;title=inspector.accesskey"
+ );
+ div.appendChild(div3);
+ const div4 = document.createElementNS(HTML_NS, "div");
+ div4.setAttribute("id", "d4");
+ div4.setAttribute("data-localization", "aria-label=inspector.label");
+ div.appendChild(div4);
+ div4.append("Some content");
+ const toolboxDiv = document.createElementNS(HTML_NS, "div");
+ toolboxDiv.setAttribute(
+ "data-localization-bundle",
+ "devtools/client/locales/toolbox.properties"
+ );
+ div.appendChild(toolboxDiv);
+ const div5 = document.createElementNS(HTML_NS, "div");
+ div5.setAttribute("id", "d5");
+ div5.setAttribute("data-localization", "content=toolbox.defaultTitle");
+ toolboxDiv.appendChild(div5);
+
+ info("Use localization helper to localize the test markup");
+ localizeMarkup(div);
+
+ is(div1.innerHTML, str1, "The content of #d1 is localized");
+ is(div2.innerHTML, str1, "The content of #d2 is localized");
+ is(div2.getAttribute("title"), str2, "The title of #d2 is localized");
+ is(div3.innerHTML, str1, "The content of #d3 is localized");
+ is(div3.getAttribute("title"), str2, "The title of #d3 is localized");
+ is(div4.innerHTML, "Some content", "The content of #d4 is not replaced");
+ is(
+ div4.getAttribute("aria-label"),
+ str1,
+ "The aria-label of #d4 is localized"
+ );
+ is(
+ div5.innerHTML,
+ str3,
+ "The content of #d5 is localized with another bundle"
+ );
+});
diff --git a/devtools/shared/tests/chrome/chrome.toml b/devtools/shared/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..3d1dddc84a
--- /dev/null
+++ b/devtools/shared/tests/chrome/chrome.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = "devtools"
+skip-if = ["os == 'android'"]
+
+["test_css-logic-findCssSelector.html"]
+
+["test_css-logic-getCssPath.html"]
+
+["test_css-logic-getXPath.html"]
+skip-if = ["os == 'linux' && debug"] # Bug 1205739
diff --git a/devtools/shared/tests/chrome/test_css-logic-findCssSelector.html b/devtools/shared/tests/chrome/test_css-logic-findCssSelector.html
new file mode 100644
index 0000000000..b7d364664b
--- /dev/null
+++ b/devtools/shared/tests/chrome/test_css-logic-findCssSelector.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for CSS logic helper </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">
+ <script type="application/javascript">
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+const { findCssSelector } = require("devtools/shared/inspector/css-logic");
+
+var _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ _tests.shift()();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+addTest(function findAllCssSelectors() {
+ const nodes = document.querySelectorAll("*");
+ for (let i = 0; i < nodes.length; i++) {
+ const selector = findCssSelector(nodes[i]);
+ const matches = document.querySelectorAll(selector);
+
+ is(matches.length, 1, "There is a single match: " + selector);
+ is(matches[0], nodes[i], "The selector matches the correct node: " + selector);
+ }
+
+ runNextTest();
+});
+
+addTest(function findCssSelectorNotContainedInDocument() {
+ const unattached = document.createElement("div");
+ unattached.id = "unattached";
+ is(findCssSelector(unattached), "", "Unattached node returns empty string");
+
+ const unattachedChild = document.createElement("div");
+ unattached.appendChild(unattachedChild);
+ is(findCssSelector(unattachedChild), "", "Unattached child returns empty string");
+
+ const unattachedBody = document.createElement("body");
+ is(findCssSelector(unattachedBody), "", "Unattached body returns empty string");
+
+ runNextTest();
+});
+
+addTest(function findCssSelectorBasic() {
+ const data = [
+ "#one",
+ "#" + CSS.escape("2"),
+ ".three",
+ "." + CSS.escape("4"),
+ "#find-css-selector > div:nth-child(5)",
+ "#find-css-selector > p:nth-child(6)",
+ ".seven",
+ ".eight",
+ ".nine",
+ ".ten",
+ "div.sameclass:nth-child(11)",
+ "div.sameclass:nth-child(12)",
+ "div.sameclass:nth-child(13)",
+ "#" + CSS.escape("!, \", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \\, ], ^, `, {, |, }, ~"),
+ ];
+
+ const container = document.querySelector("#find-css-selector");
+ is(container.children.length, data.length, "Container has correct number of children.");
+
+ for (let i = 0; i < data.length; i++) {
+ const node = container.children[i];
+ is(findCssSelector(node), data[i], "matched id for index " + (i - 1));
+ }
+
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+ <div id="find-css-selector">
+ <div id="one"></div> <!-- Basic ID -->
+ <div id="2"></div> <!-- Escaped ID -->
+ <div class="three"></div> <!-- Basic Class -->
+ <div class="4"></div> <!-- Escaped Class -->
+ <div attr="5"></div> <!-- Only an attribute -->
+ <p></p> <!-- Nothing unique -->
+ <div class="seven seven"></div> <!-- Two classes with same name -->
+ <div class="eight eight2"></div> <!-- Two classes with different names -->
+
+ <!-- Two elements with the same id - should not use ID -->
+ <div class="nine" id="nine-and-ten"></div>
+ <div class="ten" id="nine-and-ten"></div>
+
+ <!-- Three elements with the same id - should use class and nth-child instead -->
+ <div class="sameclass" id="11-12-13"></div>
+ <div class="sameclass" id="11-12-13"></div>
+ <div class="sameclass" id="11-12-13"></div>
+
+ <!-- Special characters -->
+ <div id="!, &quot;, #, $, %, &amp;, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, `, {, |, }, ~"></div>
+ </div>
+</body>
+</html>
diff --git a/devtools/shared/tests/chrome/test_css-logic-getCssPath.html b/devtools/shared/tests/chrome/test_css-logic-getCssPath.html
new file mode 100644
index 0000000000..333c9e0fdf
--- /dev/null
+++ b/devtools/shared/tests/chrome/test_css-logic-getCssPath.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1323700
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1323700</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">
+ <script type="application/javascript">
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+const CssLogic = require("devtools/shared/inspector/css-logic");
+
+var _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish()
+ return;
+ }
+ _tests.shift()();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+}
+
+addTest(function getCssPathForUnattachedElement() {
+ const unattached = document.createElement("div");
+ unattached.id = "unattached";
+ is(CssLogic.getCssPath(unattached), "", "Unattached node returns empty string");
+
+ const unattachedChild = document.createElement("div");
+ unattached.appendChild(unattachedChild);
+ is(CssLogic.getCssPath(unattachedChild), "", "Unattached child returns empty string");
+
+ const unattachedBody = document.createElement("body");
+ is(CssLogic.getCssPath(unattachedBody), "", "Unattached body returns empty string");
+
+ runNextTest();
+});
+
+addTest(function cssPathHasOneStepForEachAncestor() {
+ for (const el of [...document.querySelectorAll('*')]) {
+ const splitPath = CssLogic.getCssPath(el).split(" ");
+
+ let expectedNbOfParts = 0;
+ let parent = el.parentNode;
+ while (parent) {
+ expectedNbOfParts ++;
+ parent = parent.parentNode;
+ }
+
+ is(splitPath.length, expectedNbOfParts, "There are enough parts in the full path");
+ }
+
+ runNextTest();
+});
+
+addTest(function getCssPath() {
+ const data = [{
+ selector: "#id",
+ path: "html body div div div.class div#id"
+ }, {
+ selector: "html",
+ path: "html"
+ }, {
+ selector: "body",
+ path: "html body"
+ }, {
+ selector: ".c1.c2.c3",
+ path: "html body span.c1.c2.c3"
+ }, {
+ selector: "#i",
+ path: "html body span#i.c1.c2"
+ }];
+
+ for (const {selector, path} of data) {
+ const node = document.querySelector(selector);
+ is (CssLogic.getCssPath(node), path, `Full css path is correct for ${selector}`);
+ }
+
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+ <div>
+ <div>
+ <div class="class">
+ <div id="id"></div>
+ </div>
+ </div>
+ </div>
+ <span class="c1 c2 c3"></span>
+ <span id="i" class="c1 c2"></span>
+</body>
+</html>
diff --git a/devtools/shared/tests/chrome/test_css-logic-getXPath.html b/devtools/shared/tests/chrome/test_css-logic-getXPath.html
new file mode 100644
index 0000000000..469e188cf0
--- /dev/null
+++ b/devtools/shared/tests/chrome/test_css-logic-getXPath.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=987877
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 987877</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">
+ <script type="application/javascript">
+"use strict";
+
+const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+const CssLogic = require("devtools/shared/inspector/css-logic");
+
+const _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ _tests.shift()();
+}
+
+window.onload = function () {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+addTest(function getXPathForUnattachedElement() {
+ const unattached = document.createElement("div");
+ unattached.id = "unattached";
+ is(CssLogic.getXPath(unattached), "", "Unattached node returns empty string");
+
+ const unattachedChild = document.createElement("div");
+ unattached.appendChild(unattachedChild);
+ is(CssLogic.getXPath(unattachedChild), "", "Unattached child returns empty string");
+
+ const unattachedBody = document.createElement("body");
+ is(CssLogic.getXPath(unattachedBody), "", "Unattached body returns empty string");
+
+ runNextTest();
+});
+
+addTest(function getXPath() {
+ const data = [{
+ // Target elements that have an ID get a short XPath.
+ selector: "#i-have-an-id",
+ path: "//*[@id=\"i-have-an-id\"]"
+ }, {
+ selector: "html",
+ path: "/html"
+ }, {
+ selector: "body",
+ path: "/html/body"
+ }, {
+ selector: "body > div:nth-child(2) > div > div:nth-child(4)",
+ path: "/html/body/div[2]/div/div[4]"
+ }, {
+ // XPath should support namespace.
+ selector: "namespace\\:body",
+ path: "/html/body/namespace:test/namespace:body"
+ }];
+
+ for (const {selector, path} of data) {
+ const node = document.querySelector(selector);
+ is(CssLogic.getXPath(node), path, `Full css path is correct for ${selector}`);
+ }
+
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+ <div id="i-have-an-id">find me</div>
+ <div>
+ <div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div>me too!</div>
+ </div>
+ </div>
+ <namespace:test>
+ <namespace:header></namespace:header>
+ <namespace:body>and me</namespace:body>
+ </namespace:test>
+</body>
+</html>
diff --git a/devtools/shared/tests/xpcshell/.eslintrc.js b/devtools/shared/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..cc1ed286cc
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/tests/xpcshell/exposeLoader.js b/devtools/shared/tests/xpcshell/exposeLoader.js
new file mode 100644
index 0000000000..7c8acdd759
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/exposeLoader.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+exports.exerciseLazyRequire = (name, path) => {
+ const o = {};
+ loader.lazyRequireGetter(o, name, path);
+ return o;
+};
diff --git a/devtools/shared/tests/xpcshell/head_devtools.js b/devtools/shared/tests/xpcshell/head_devtools.js
new file mode 100644
index 0000000000..cee2218a2a
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/head_devtools.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported DevToolsUtils, DevToolsLoader */
+
+"use strict";
+
+const { require, DevToolsLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+
+Services.prefs.setBoolPref("devtools.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.testing");
+});
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+
+// If for whatever reason the test needs to post console errors that aren't
+// failures, set this to true.
+var ALLOW_CONSOLE_ERRORS = false;
+
+// XXX This listener is broken, see bug 1456634, for now turn off no-undef here,
+// this needs turning back on!
+/* eslint-disable no-undef */
+var listener = {
+ observe(message) {
+ let string;
+ try {
+ message.QueryInterface(Ci.nsIScriptError);
+ dump(
+ message.sourceName +
+ ":" +
+ message.lineNumber +
+ ": " +
+ scriptErrorFlagsToKind(message.flags) +
+ ": " +
+ message.errorMessage +
+ "\n"
+ );
+ string = message.errorMessage;
+ } catch (ex) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ string = "" + message.message;
+ } catch (e) {
+ string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (DevToolsServer.xpcInspector.eventLoopNestLevel > 0) {
+ DevToolsServer.xpcInspector.exitNestedEventLoop();
+ }
+
+ if (!ALLOW_CONSOLE_ERRORS) {
+ do_throw("head_devtools.js got console message: " + string + "\n");
+ }
+ },
+};
+/* eslint-enable no-undef */
+
+Services.console.registerListener(listener);
diff --git a/devtools/shared/tests/xpcshell/test_assert.js b/devtools/shared/tests/xpcshell/test_assert.js
new file mode 100644
index 0000000000..45ae9eb1a2
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_assert.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test DevToolsUtils.assert
+
+ALLOW_CONSOLE_ERRORS = true;
+
+function run_test() {
+ const { assert } = DevToolsUtils;
+ equal(typeof assert, "function");
+
+ try {
+ assert(true, "this assertion should not fail");
+ } catch (e) {
+ // If you catch assertion failures in practice, I will hunt you down. I get
+ // email notifications every time it happens.
+ ok(
+ false,
+ "Should not get an error for an assertion that should not fail. Got " +
+ DevToolsUtils.safeErrorString(e)
+ );
+ }
+
+ let assertionFailed = false;
+ try {
+ assert(false, "this assertion should fail");
+ } catch (e) {
+ ok(
+ e.message.startsWith("Assertion failure:"),
+ "Should be an assertion failure error"
+ );
+ assertionFailed = true;
+ }
+
+ ok(
+ assertionFailed,
+ "The assertion should have failed, which should throw an error when assertions " +
+ "are enabled."
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_console_filtering.js b/devtools/shared/tests/xpcshell/test_console_filtering.js
new file mode 100644
index 0000000000..681b93e523
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_console_filtering.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+);
+const {
+ ConsoleAPIListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+
+var seenMessages = 0;
+var seenTypes = 0;
+
+var onConsoleAPICall = function (message) {
+ if (message.consoleID && message.consoleID == "addon/foo") {
+ Assert.equal(message.level, "warn");
+ Assert.equal(message.arguments[0], "Warning from foo");
+ seenTypes |= 1;
+ } else if (message.addonId == "bar") {
+ Assert.equal(message.level, "error");
+ Assert.equal(message.arguments[0], "Error from bar");
+ seenTypes |= 2;
+ } else {
+ Assert.equal(message.level, "log");
+ Assert.equal(message.arguments[0], "Hello from default console");
+ seenTypes |= 4;
+ }
+ seenMessages++;
+};
+
+let policy;
+registerCleanupFunction(() => {
+ policy.active = false;
+});
+
+function createFakeAddonWindow({ addonId } = {}) {
+ const uuidGen = Services.uuid;
+ const uuid = uuidGen.generateUUID().number.slice(1, -1);
+
+ if (policy) {
+ policy.active = false;
+ }
+ /* globals MatchPatternSet, WebExtensionPolicy */
+ policy = new WebExtensionPolicy({
+ id: addonId,
+ mozExtensionHostname: uuid,
+ baseURL: "file:///",
+ allowedOrigins: new MatchPatternSet([]),
+ localizeCallback() {},
+ });
+ policy.active = true;
+
+ const baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ baseURI,
+ {}
+ );
+ const chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+ const { docShell } = chromeWebNav;
+ docShell.createAboutBlankDocumentViewer(principal, principal);
+ const addonWindow = docShell.docViewer.DOMDocument.defaultView;
+
+ return { addonWindow, chromeWebNav };
+}
+
+/**
+ * Tests that the consoleID property of the ConsoleAPI options gets passed
+ * through to console messages.
+ */
+function run_test() {
+ // console1 Test Console.sys.mjs messages tagged by the Addon SDK
+ // are still filtered correctly.
+ const console1 = new ConsoleAPI({
+ consoleID: "addon/foo",
+ });
+
+ // console2 - WebExtension page's console messages tagged
+ // by 'originAttributes.addonId' are filtered correctly.
+ const { addonWindow, chromeWebNav } = createFakeAddonWindow({
+ addonId: "bar",
+ });
+ const console2 = addonWindow.console;
+
+ // console - Plain console object (messages are tagged with window ids
+ // and originAttributes, but the addonId will be empty).
+ console.log("Hello from default console");
+
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+
+ let listener = new ConsoleAPIListener(null, onConsoleAPICall);
+ listener.init();
+ let messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(onConsoleAPICall);
+ Assert.equal(seenMessages, 3);
+ Assert.equal(seenTypes, 7);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+ Assert.equal(seenMessages, 3);
+ Assert.equal(seenTypes, 7);
+
+ listener.destroy();
+
+ listener = new ConsoleAPIListener(null, onConsoleAPICall, { addonId: "foo" });
+ listener.init();
+ messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(onConsoleAPICall);
+ Assert.equal(seenMessages, 2);
+ Assert.equal(seenTypes, 1);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+ Assert.equal(seenMessages, 1);
+ Assert.equal(seenTypes, 1);
+
+ listener.destroy();
+
+ listener = new ConsoleAPIListener(null, onConsoleAPICall, { addonId: "bar" });
+ listener.init();
+ messages = listener.getCachedMessages();
+
+ seenTypes = 0;
+ seenMessages = 0;
+ messages.forEach(onConsoleAPICall);
+ Assert.equal(seenMessages, 3);
+ Assert.equal(seenTypes, 2);
+
+ seenTypes = 0;
+ seenMessages = 0;
+ console.log("Hello from default console");
+ console1.warn("Warning from foo");
+ console2.error("Error from bar");
+
+ Assert.equal(seenMessages, 1);
+ Assert.equal(seenTypes, 2);
+
+ listener.destroy();
+
+ // Close the addon window's chromeWebNav.
+ chromeWebNav.close();
+}
diff --git a/devtools/shared/tests/xpcshell/test_csslexer.js b/devtools/shared/tests/xpcshell/test_csslexer.js
new file mode 100644
index 0000000000..417fb6b1df
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_csslexer.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 jsLexer = require("resource://devtools/shared/css/lexer.js");
+
+function test_lexer(cssText, tokenTypes) {
+ const lexer = jsLexer.getCSSLexer(cssText);
+ let reconstructed = "";
+ let lastTokenEnd = 0;
+ let i = 0;
+ while (true) {
+ const token = lexer.nextToken();
+ if (!token) {
+ break;
+ }
+ let combined = token.tokenType;
+ if (token.text) {
+ combined += ":" + token.text;
+ }
+ equal(combined, tokenTypes[i]);
+ Assert.greater(token.endOffset, token.startOffset);
+ equal(token.startOffset, lastTokenEnd);
+ lastTokenEnd = token.endOffset;
+ reconstructed += cssText.substring(token.startOffset, token.endOffset);
+ ++i;
+ }
+ // Ensure that we saw the correct number of tokens.
+ equal(i, tokenTypes.length);
+ // Ensure that the reported offsets cover all the text.
+ equal(reconstructed, cssText);
+}
+
+var LEX_TESTS = [
+ ["simple", ["ident:simple"]],
+ [
+ "simple: { hi; }",
+ [
+ "ident:simple",
+ "symbol::",
+ "whitespace",
+ "symbol:{",
+ "whitespace",
+ "ident:hi",
+ "symbol:;",
+ "whitespace",
+ "symbol:}",
+ ],
+ ],
+ ["/* whatever */", ["comment"]],
+ ["'string'", ["string:string"]],
+ ['"string"', ["string:string"]],
+ [
+ "rgb(1,2,3)",
+ [
+ "function:rgb",
+ "number",
+ "symbol:,",
+ "number",
+ "symbol:,",
+ "number",
+ "symbol:)",
+ ],
+ ],
+ ["@media", ["at:media"]],
+ ["#hibob", ["id:hibob"]],
+ ["#123", ["hash:123"]],
+ ["23px", ["dimension:px"]],
+ ["23%", ["percentage"]],
+ ["url(http://example.com)", ["url:http://example.com"]],
+ ["url('http://example.com')", ["url:http://example.com"]],
+ ["url( 'http://example.com' )", ["url:http://example.com"]],
+ // In CSS Level 3, this is an ordinary URL, not a BAD_URL.
+ ["url(http://example.com", ["url:http://example.com"]],
+ ["url(http://example.com @", ["bad_url:http://example.com"]],
+ ["quo\\ting", ["ident:quoting"]],
+ ["'bad string\n", ["bad_string:bad string", "whitespace"]],
+ ["~=", ["includes"]],
+ ["|=", ["dashmatch"]],
+ ["^=", ["beginsmatch"]],
+ ["$=", ["endsmatch"]],
+ ["*=", ["containsmatch"]],
+
+ // URANGE may be on the way out, and it isn't used by devutils, so
+ // let's skip it.
+
+ [
+ "<!-- html comment -->",
+ [
+ "htmlcomment",
+ "whitespace",
+ "ident:html",
+ "whitespace",
+ "ident:comment",
+ "whitespace",
+ "htmlcomment",
+ ],
+ ],
+
+ // earlier versions of CSS had "bad comment" tokens, but in level 3,
+ // unterminated comments are just comments.
+ ["/* bad comment", ["comment"]],
+];
+
+function test_lexer_linecol(cssText, locations) {
+ const lexer = jsLexer.getCSSLexer(cssText);
+ let i = 0;
+ while (true) {
+ const token = lexer.nextToken();
+ const startLine = lexer.lineNumber;
+ const startColumn = lexer.columnNumber;
+
+ // We do this in a bit of a funny way so that we can also test the
+ // location of the EOF.
+ let combined = ":" + startLine + ":" + startColumn;
+ if (token) {
+ combined = token.tokenType + combined;
+ }
+
+ equal(combined, locations[i]);
+ ++i;
+
+ if (!token) {
+ break;
+ }
+ }
+ // Ensure that we saw the correct number of tokens.
+ equal(i, locations.length);
+}
+
+function test_lexer_eofchar(
+ cssText,
+ argText,
+ expectedAppend,
+ expectedNoAppend
+) {
+ const lexer = jsLexer.getCSSLexer(cssText);
+ while (lexer.nextToken()) {
+ // Nothing.
+ }
+
+ info("EOF char test, input = " + cssText);
+
+ let result = lexer.performEOFFixup(argText, true);
+ equal(result, expectedAppend);
+
+ result = lexer.performEOFFixup(argText, false);
+ equal(result, expectedNoAppend);
+}
+
+var LINECOL_TESTS = [
+ ["simple", ["ident:0:0", ":0:6"]],
+ ["\n stuff", ["whitespace:0:0", "ident:1:4", ":1:9"]],
+ [
+ '"string with \\\nnewline" \r\n',
+ ["string:0:0", "whitespace:1:8", ":2:0"],
+ ],
+];
+
+var EOFCHAR_TESTS = [
+ ["hello", "hello"],
+ ["hello \\", "hello \\\\", "hello \\\uFFFD"],
+ ["'hello", "'hello'"],
+ ['"hello', '"hello"'],
+ ["'hello\\", "'hello\\\\'", "'hello'"],
+ ['"hello\\', '"hello\\\\"', '"hello"'],
+ ["/*hello", "/*hello*/"],
+ ["/*hello*", "/*hello*/"],
+ ["/*hello\\", "/*hello\\*/"],
+ ["url(hello", "url(hello)"],
+ ["url('hello", "url('hello')"],
+ ['url("hello', 'url("hello")'],
+ ["url(hello\\", "url(hello\\\\)", "url(hello\\\uFFFD)"],
+ ["url('hello\\", "url('hello\\\\')", "url('hello')"],
+ ['url("hello\\', 'url("hello\\\\")', 'url("hello")'],
+];
+
+function run_test() {
+ let text, result;
+ for ([text, result] of LEX_TESTS) {
+ test_lexer(text, result);
+ }
+
+ for ([text, result] of LINECOL_TESTS) {
+ test_lexer_linecol(text, result);
+ }
+
+ let expectedAppend, expectedNoAppend;
+ for ([text, expectedAppend, expectedNoAppend] of EOFCHAR_TESTS) {
+ if (!expectedNoAppend) {
+ expectedNoAppend = expectedAppend;
+ }
+ test_lexer_eofchar(text, text, expectedAppend, expectedNoAppend);
+ }
+
+ // Ensure that passing a different inputString to performEOFFixup
+ // doesn't cause an assertion trying to strip a backslash from the
+ // end of an empty string.
+ test_lexer_eofchar("'\\", "", "\\'", "'");
+}
diff --git a/devtools/shared/tests/xpcshell/test_debugger_client.js b/devtools/shared/tests/xpcshell/test_debugger_client.js
new file mode 100644
index 0000000000..d908ddfb27
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_debugger_client.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// DevToolsClient tests
+
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+add_task(async function () {
+ await testCloseLoops();
+ await fakeTransportShutdown();
+});
+
+function createClient() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ return client;
+}
+
+// Ensure that closing the client while it is closing doesn't loop
+async function testCloseLoops() {
+ const client = createClient();
+ await client.connect();
+
+ await new Promise(resolve => {
+ let called = false;
+ client.on("closed", async () => {
+ dump(">> CLOSED\n");
+ if (called) {
+ ok(
+ false,
+ "Calling client.close from closed event listener introduce loops"
+ );
+ return;
+ }
+ called = true;
+ await client.close();
+ resolve();
+ });
+ client.close();
+ });
+}
+
+// Check that, if we fake a transport shutdown (like if a device is unplugged)
+// the client is automatically closed, and we can still call client.close.
+async function fakeTransportShutdown() {
+ const client = createClient();
+ await client.connect();
+
+ await new Promise(resolve => {
+ const onClosed = async function () {
+ client.off("closed", onClosed);
+ ok(true, "Client emitted 'closed' event");
+ resolve();
+ };
+ client.on("closed", onClosed);
+ client.transport.close();
+ });
+
+ await client.close();
+ ok(true, "client.close() successfully resolves");
+}
diff --git a/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js b/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js
new file mode 100644
index 0000000000..2f53b377de
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test DevToolsUtils.defineLazyPrototypeGetter
+
+function Class() {}
+DevToolsUtils.defineLazyPrototypeGetter(Class.prototype, "foo", () => []);
+
+function run_test() {
+ test_prototype_attributes();
+ test_instance_attributes();
+ test_multiple_instances();
+ test_callback_receiver();
+}
+
+function test_prototype_attributes() {
+ // Check that the prototype has a getter property with expected attributes.
+ const descriptor = Object.getOwnPropertyDescriptor(Class.prototype, "foo");
+ Assert.equal(typeof descriptor.get, "function");
+ Assert.equal(descriptor.set, undefined);
+ Assert.equal(descriptor.enumerable, false);
+ Assert.equal(descriptor.configurable, true);
+}
+
+function test_instance_attributes() {
+ // Instances should not have an own property until the lazy getter has been
+ // activated.
+ const instance = new Class();
+ Assert.ok(!instance.hasOwnProperty("foo"));
+ instance.foo;
+ Assert.ok(instance.hasOwnProperty("foo"));
+
+ // Check that the instance has an own property with the expecred value and
+ // attributes after the lazy getter is activated.
+ const descriptor = Object.getOwnPropertyDescriptor(instance, "foo");
+ Assert.ok(descriptor.value instanceof Array);
+ Assert.equal(descriptor.writable, true);
+ Assert.equal(descriptor.enumerable, false);
+ Assert.equal(descriptor.configurable, true);
+}
+
+function test_multiple_instances() {
+ const instance1 = new Class();
+ const instance2 = new Class();
+ const foo1 = instance1.foo;
+ const foo2 = instance2.foo;
+ // Check that the lazy getter returns the expected type of value.
+ Assert.ok(foo1 instanceof Array);
+ Assert.ok(foo2 instanceof Array);
+ // Make sure the lazy getter runs once and only once per instance.
+ Assert.equal(instance1.foo, foo1);
+ Assert.equal(instance2.foo, foo2);
+ // Make sure each instance gets its own unique value.
+ Assert.notEqual(foo1, foo2);
+}
+
+function test_callback_receiver() {
+ function Foo() {}
+ DevToolsUtils.defineLazyPrototypeGetter(Foo.prototype, "foo", function () {
+ return this;
+ });
+
+ // Check that the |this| value in the callback is the instance itself.
+ const instance = new Foo();
+ Assert.equal(instance.foo, instance);
+}
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js b/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js
new file mode 100644
index 0000000000..9a35ce1f98
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+add_task(function testAbortSingleListener() {
+ // Test a simple case with AbortController
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup an event listener on test-event, controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 1, "We received one event, as expected");
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit test-event again");
+ emitter.emit("test-event");
+ equal(eventsReceived, 1, "We didn't receive new event after aborting");
+});
+
+add_task(function testAbortSingleListenerOnce() {
+ // Test a simple case with AbortController and once
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup an event listener on test-event, controlled by an AbortSignal");
+ let eventReceived = false;
+ emitter.once(
+ "test-event",
+ () => {
+ eventReceived = true;
+ },
+ { signal }
+ );
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit test-event");
+ emitter.emit("test-event");
+ equal(eventReceived, false, "We didn't receive the event after aborting");
+});
+
+add_task(function testAbortMultipleListener() {
+ // Test aborting multiple event listeners with one call to abort
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup 3 event listeners controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter.on("test-event", () => eventsReceived++, { signal });
+ emitter.on("test-event", () => eventsReceived++, { signal });
+ emitter.on("other-test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event and other-test-event");
+ emitter.emit("test-event");
+ emitter.emit("other-test-event");
+ equal(eventsReceived, 3, "We received 3 events, as expected");
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit events again");
+ emitter.emit("test-event");
+ emitter.emit("other-test-event");
+ equal(eventsReceived, 3, "We didn't receive new event after aborting");
+});
+
+add_task(function testAbortMultipleEmitter() {
+ // Test aborting multiple event listeners on different emitters with one call to abort
+ info("Create 2 EventEmitter");
+ const emitter1 = new EventEmitter();
+ const emitter2 = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup 2 event listeners on test-event, controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter1.on("test-event", () => eventsReceived++, { signal });
+ emitter2.on("other-test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event and other-test-event");
+ emitter1.emit("test-event");
+ emitter2.emit("other-test-event");
+ equal(eventsReceived, 2, "We received 2 events, as expected");
+
+ info("Abort the AbortController…");
+ abortController.abort();
+ info("… and emit events again");
+ emitter1.emit("test-event");
+ emitter2.emit("other-test-event");
+ equal(eventsReceived, 2, "We didn't receive new event after aborting");
+});
+
+add_task(function testAbortBeforeEmitting() {
+ // Check that aborting before emitting does unregister the event listener
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info("Setup an event listener on test-event, controlled by an AbortSignal");
+ let eventsReceived = 0;
+ emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Abort the AbortController…");
+ abortController.abort();
+
+ info("… and emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 0, "We didn't receive any event");
+});
+
+add_task(function testAbortBeforeSettingListener() {
+ // Check that aborting before creating the event listener won't register it
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+
+ info("Create an AbortController and abort it immediately");
+ const abortController = new AbortController();
+ const { signal } = abortController;
+ abortController.abort();
+
+ info(
+ "Setup an event listener on test-event, controlled by the aborted AbortSignal"
+ );
+ let eventsReceived = 0;
+ const off = emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 0, "We didn't receive any event");
+
+ equal(typeof off, "function", "emitter.on still returned a function");
+ // check that calling off does not throw
+ off();
+});
+
+add_task(function testAbortAfterEventListenerIsRemoved() {
+ // Check that aborting after there's no more event listener does not throw
+ info("Create an EventEmitter");
+ const emitter = new EventEmitter();
+
+ const abortController = new AbortController();
+ const { signal } = abortController;
+
+ info(
+ "Setup an event listener on test-event, controlled by the aborted AbortSignal"
+ );
+ let eventsReceived = 0;
+ const off = emitter.on("test-event", () => eventsReceived++, { signal });
+
+ info("Emit test-event");
+ emitter.emit("test-event");
+ equal(eventsReceived, 1, "We received the expected event");
+
+ info("Remove the event listener with the function returned by `on`");
+ off();
+
+ info("Emit test-event a second time");
+ emitter.emit("test-event");
+ equal(
+ eventsReceived,
+ 1,
+ "We didn't receive new event after removing the event listener"
+ );
+
+ info("Abort to check it doesn't throw");
+ abortController.abort();
+});
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_basic.js b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js
new file mode 100644
index 0000000000..caf2186bff
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js
@@ -0,0 +1,345 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ConsoleAPIListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const hasMethod = (target, method) =>
+ method in target && typeof target[method] === "function";
+
+/**
+ * Each method of this object is a test; tests can be synchronous or asynchronous:
+ *
+ * 1. Plain functions are synchronous tests.
+ * 2. methods with `async` keyword are asynchronous tests.
+ * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to
+ * finish the test).
+ */
+const TESTS = {
+ testEventEmitterCreation() {
+ const emitter = getEventEmitter();
+ const isAnEmitter = emitter instanceof EventEmitter;
+
+ ok(emitter, "We have an event emitter");
+ ok(
+ hasMethod(emitter, "on") &&
+ hasMethod(emitter, "off") &&
+ hasMethod(emitter, "once") &&
+ hasMethod(emitter, "count") &&
+ !hasMethod(emitter, "decorate"),
+ `Event Emitter ${
+ isAnEmitter ? "instance" : "mixin"
+ } has the expected methods.`
+ );
+ },
+
+ testEmittingEvents(done) {
+ const emitter = getEventEmitter();
+
+ let beenHere1 = false;
+ let beenHere2 = false;
+
+ function next(str1, str2) {
+ equal(str1, "abc", "Argument 1 is correct");
+ equal(str2, "def", "Argument 2 is correct");
+
+ ok(!beenHere1, "first time in next callback");
+ beenHere1 = true;
+
+ emitter.off("next", next);
+
+ emitter.emit("next");
+
+ emitter.once("onlyonce", onlyOnce);
+
+ emitter.emit("onlyonce");
+ emitter.emit("onlyonce");
+ }
+
+ function onlyOnce() {
+ ok(!beenHere2, '"once" listener has been called once');
+ beenHere2 = true;
+ emitter.emit("onlyonce");
+
+ done();
+ }
+
+ emitter.on("next", next);
+ emitter.emit("next", "abc", "def");
+ },
+
+ testThrowingExceptionInListener(done) {
+ const emitter = getEventEmitter();
+ const listener = new ConsoleAPIListener(null, message => {
+ equal(message.level, "error");
+ const [arg] = message.arguments;
+ equal(arg.message, "foo");
+ equal(arg.stack, "bar");
+ listener.destroy();
+ done();
+ });
+
+ listener.init();
+
+ function throwListener() {
+ emitter.off("throw-exception");
+ const err = new Error("foo");
+ err.stack = "bar";
+ throw err;
+ }
+
+ emitter.on("throw-exception", throwListener);
+ emitter.emit("throw-exception");
+ },
+
+ testKillItWhileEmitting(done) {
+ const emitter = getEventEmitter();
+
+ const c1 = () => ok(true, "c1 called");
+ const c2 = () => {
+ ok(true, "c2 called");
+ emitter.off("tick", c3);
+ };
+ const c3 = () => ok(false, "c3 should not be called");
+ const c4 = () => {
+ ok(true, "c4 called");
+ done();
+ };
+
+ emitter.on("tick", c1);
+ emitter.on("tick", c2);
+ emitter.on("tick", c3);
+ emitter.on("tick", c4);
+
+ emitter.emit("tick");
+ },
+
+ testOffAfterOnce() {
+ const emitter = getEventEmitter();
+
+ let enteredC1 = false;
+ const c1 = () => (enteredC1 = true);
+
+ emitter.once("oao", c1);
+ emitter.off("oao", c1);
+
+ emitter.emit("oao");
+
+ ok(!enteredC1, "c1 should not be called");
+ },
+
+ testPromise() {
+ const emitter = getEventEmitter();
+ const p = emitter.once("thing");
+
+ // Check that the promise is only resolved once event though we
+ // emit("thing") more than once
+ let firstCallbackCalled = false;
+ const check1 = p.then(arg => {
+ equal(firstCallbackCalled, false, "first callback called only once");
+ firstCallbackCalled = true;
+ equal(arg, "happened", "correct arg in promise");
+ return "rval from c1";
+ });
+
+ emitter.emit("thing", "happened", "ignored");
+
+ // Check that the promise is resolved asynchronously
+ let secondCallbackCalled = false;
+ const check2 = p.then(arg => {
+ ok(true, "second callback called");
+ equal(arg, "happened", "correct arg in promise");
+ secondCallbackCalled = true;
+ equal(arg, "happened", "correct arg in promise (a second time)");
+ return "rval from c2";
+ });
+
+ // Shouldn't call any of the above listeners
+ emitter.emit("thing", "trashinate");
+
+ // Check that we can still separate events with different names
+ // and that it works with no parameters
+ const pfoo = emitter.once("foo");
+ const pbar = emitter.once("bar");
+
+ const check3 = pfoo.then(arg => {
+ Assert.strictEqual(arg, undefined, "no arg for foo event");
+ return "rval from c3";
+ });
+
+ pbar.then(() => {
+ ok(false, "pbar should not be called");
+ });
+
+ emitter.emit("foo");
+
+ equal(secondCallbackCalled, false, "second callback not called yet");
+
+ return Promise.all([check1, check2, check3]).then(args => {
+ equal(args[0], "rval from c1", "callback 1 done good");
+ equal(args[1], "rval from c2", "callback 2 done good");
+ equal(args[2], "rval from c3", "callback 3 done good");
+ });
+ },
+
+ testClearEvents() {
+ const emitter = getEventEmitter();
+
+ const received = [];
+ const listener = (...args) => received.push(args);
+
+ emitter.on("a", listener);
+ emitter.on("b", listener);
+ emitter.on("c", listener);
+
+ emitter.emit("a", 1);
+ emitter.emit("b", 1);
+ emitter.emit("c", 1);
+
+ equal(received.length, 3, "the listener was triggered three times");
+
+ emitter.clearEvents();
+ emitter.emit("a", 1);
+ emitter.emit("b", 1);
+ emitter.emit("c", 1);
+ equal(received.length, 3, "the listener was not called after clearEvents");
+ },
+
+ testOnReturn() {
+ const emitter = getEventEmitter();
+
+ let called = false;
+ const removeOnTest = emitter.on("test", () => {
+ called = true;
+ });
+
+ equal(typeof removeOnTest, "function", "`on` returns a function");
+ removeOnTest();
+
+ emitter.emit("test");
+ equal(called, false, "event listener wasn't called");
+ },
+
+ async testEmitAsync() {
+ const emitter = getEventEmitter();
+
+ let resolve1, resolve2;
+ emitter.once("test", async () => {
+ return new Promise(r => {
+ resolve1 = r;
+ });
+ });
+
+ // Adding a listener which doesn't return a promise should trigger a console warning.
+ emitter.once("test", () => {});
+
+ emitter.once("test", async () => {
+ return new Promise(r => {
+ resolve2 = r;
+ });
+ });
+
+ info("Emit an event and wait for all listener resolutions");
+ const onConsoleWarning = onConsoleWarningLogged(
+ "Listener for event 'test' did not return a promise."
+ );
+ const onEmitted = emitter.emitAsync("test");
+ let resolved = false;
+ onEmitted.then(() => {
+ info("emitAsync just resolved");
+ resolved = true;
+ });
+
+ info("Waiting for warning message about the second listener");
+ await onConsoleWarning;
+
+ // Spin the event loop, to ensure that emitAsync did not resolved too early
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+
+ ok(resolve1, "event listener has been called");
+ ok(!resolved, "but emitAsync hasn't resolved yet");
+
+ info("Resolve the first listener function");
+ resolve1();
+ ok(!resolved, "emitAsync isn't resolved until all listener resolve");
+
+ info("Resolve the second listener function");
+ resolve2();
+
+ // emitAsync is only resolved in the next event loop
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+ ok(resolved, "once we resolve all the listeners, emitAsync is resolved");
+ },
+
+ testCount() {
+ const emitter = getEventEmitter();
+
+ equal(emitter.count("foo"), 0, "no listeners for 'foo' events");
+ emitter.on("foo", () => {});
+ equal(emitter.count("foo"), 1, "listener registered");
+ emitter.on("foo", () => {});
+ equal(emitter.count("foo"), 2, "another listener registered");
+ emitter.off("foo");
+ equal(emitter.count("foo"), 0, "listeners unregistered");
+ },
+};
+
+// Wait for the next call to console.warn which includes
+// the text passed as argument
+function onConsoleWarningLogged(warningMessage) {
+ return new Promise(resolve => {
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+
+ const observer = subject => {
+ // This is the first argument passed to console.warn()
+ const message = subject.wrappedJSObject.arguments[0];
+ if (message.includes(warningMessage)) {
+ ConsoleAPIStorage.removeLogEventListener(observer);
+ resolve();
+ }
+ };
+
+ ConsoleAPIStorage.addLogEventListener(
+ observer,
+ Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
+ );
+ });
+}
+
+/**
+ * Create a runnable tests based on the tests descriptor given.
+ *
+ * @param {Object} tests
+ * The tests descriptor object, contains the tests to run.
+ */
+const runnable = tests =>
+ async function () {
+ for (const name of Object.keys(tests)) {
+ info(name);
+ if (tests[name].length === 1) {
+ await new Promise(resolve => tests[name](resolve));
+ } else {
+ await tests[name]();
+ }
+ }
+ };
+
+// We want to run the same tests for both an instance of `EventEmitter` and an object
+// decorate with EventEmitter; therefore we create two strategies (`createNewEmitter` and
+// `decorateObject`) and a factory (`getEventEmitter`), where the factory is the actual
+// function used in the tests.
+
+const createNewEmitter = () => new EventEmitter();
+const decorateObject = () => EventEmitter.decorate({});
+
+// First iteration of the tests with a new instance of `EventEmitter`.
+let getEventEmitter = createNewEmitter;
+add_task(runnable(TESTS));
+// Second iteration of the tests with an object decorate using `EventEmitter`
+add_task(() => (getEventEmitter = decorateObject));
+add_task(runnable(TESTS));
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js b/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js
new file mode 100644
index 0000000000..715dd1c466
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_destroy.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";
+
+add_task(function () {
+ const { DevToolsLoader, require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+
+ // Force-load the module once in the global loader to avoid Bug 1622718.
+ require("resource://devtools/shared/event-emitter.js");
+
+ const emitterRef = (function () {
+ const loader = new DevToolsLoader();
+
+ const ref = Cu.getWeakReference(
+ loader.require("resource://devtools/shared/event-emitter.js")
+ );
+
+ loader.destroy();
+ return ref;
+ })();
+
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceGC();
+ Cu.forceCC();
+
+ Assert.ok(!emitterRef.get(), "weakref has been cleared by gc");
+});
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_static.js b/devtools/shared/tests/xpcshell/test_eventemitter_static.js
new file mode 100644
index 0000000000..9b17a7612f
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_static.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ConsoleAPIListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+const {
+ on,
+ once,
+ off,
+ emit,
+ count,
+ handler,
+} = require("resource://devtools/shared/event-emitter.js");
+
+const pass = message => ok(true, message);
+const fail = message => ok(false, message);
+
+/**
+ * Each method of this object is a test; tests can be synchronous or asynchronous:
+ *
+ * 1. Plain method are synchronous tests.
+ * 2. methods with `async` keyword are asynchronous tests.
+ * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to
+ * complete the test).
+ */
+const TESTS = {
+ testAddListener() {
+ const events = [{ name: "event#1" }, "event#2"];
+ const target = { name: "target" };
+
+ on(target, "message", function (message) {
+ equal(this, target, "this is a target object");
+ equal(message, events.shift(), "message is emitted event");
+ });
+
+ emit(target, "message", events[0]);
+ emit(target, "message", events[0]);
+ },
+
+ testListenerIsUniquePerType() {
+ const actual = [];
+ const target = {};
+ listener = () => actual.push(1);
+
+ on(target, "message", listener);
+ on(target, "message", listener);
+ on(target, "message", listener);
+ on(target, "foo", listener);
+ on(target, "foo", listener);
+
+ emit(target, "message");
+ deepEqual([1], actual, "only one message listener added");
+
+ emit(target, "foo");
+ deepEqual([1, 1], actual, "same listener added for other event");
+ },
+
+ testEventTypeMatters() {
+ const target = { name: "target" };
+ on(target, "message", () => fail("no event is expected"));
+ on(target, "done", () => pass("event is emitted"));
+
+ emit(target, "foo");
+ emit(target, "done");
+ },
+
+ testAllArgumentsArePassed() {
+ const foo = { name: "foo" },
+ bar = "bar";
+ const target = { name: "target" };
+
+ on(target, "message", (a, b) => {
+ equal(a, foo, "first argument passed");
+ equal(b, bar, "second argument passed");
+ });
+
+ emit(target, "message", foo, bar);
+ },
+
+ testNoSideEffectsInEmit() {
+ const target = { name: "target" };
+
+ on(target, "message", () => {
+ pass("first listener is called");
+
+ on(target, "message", () => fail("second listener is called"));
+ });
+ emit(target, "message");
+ },
+
+ testCanRemoveNextListener() {
+ const target = { name: "target" };
+
+ on(target, "data", () => {
+ pass("first listener called");
+ off(target, "data", fail);
+ });
+ on(target, "data", fail);
+
+ emit(target, "data", "Listener should be removed");
+ },
+
+ testOrderOfPropagation() {
+ const actual = [];
+ const target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ emit(target, "message");
+
+ deepEqual([1, 2, 3], actual, "called in order they were added");
+ },
+
+ testRemoveListener() {
+ const target = { name: "target" };
+ const actual = [];
+
+ on(target, "message", function listener() {
+ actual.push(1);
+ on(target, "message", () => {
+ off(target, "message", listener);
+ actual.push(2);
+ });
+ });
+
+ emit(target, "message");
+ deepEqual([1], actual, "first listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2], actual, "second listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2, 2, 2], actual, "first listener removed");
+ },
+
+ testRemoveAllListenersForType() {
+ const actual = [];
+ const target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ on(target, "bar", () => actual.push("b"));
+ off(target, "message");
+
+ emit(target, "message");
+ emit(target, "bar");
+
+ deepEqual(["b"], actual, "all message listeners were removed");
+ },
+
+ testRemoveAllListeners() {
+ const actual = [];
+ const target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ on(target, "bar", () => actual.push("b"));
+
+ off(target);
+
+ emit(target, "message");
+ emit(target, "bar");
+
+ deepEqual([], actual, "all listeners events were removed");
+ },
+
+ testFalsyArgumentsAreFine() {
+ let type, listener;
+ const target = { name: "target" },
+ actual = [];
+ on(target, "bar", () => actual.push(0));
+
+ off(target, "bar", listener);
+ emit(target, "bar");
+ deepEqual([0], actual, "3rd bad arg will keep listener");
+
+ off(target, type);
+ emit(target, "bar");
+ deepEqual([0, 0], actual, "2nd bad arg will keep listener");
+
+ off(target, type, listener);
+ emit(target, "bar");
+ deepEqual([0, 0, 0], actual, "2nd & 3rd bad args will keep listener");
+ },
+
+ testUnhandledExceptions(done) {
+ const listener = new ConsoleAPIListener(null, message => {
+ equal(message.level, "error", "Got the first exception");
+ equal(
+ message.arguments[0].message,
+ "Boom!",
+ "unhandled exception is logged"
+ );
+
+ listener.destroy();
+ done();
+ });
+
+ listener.init();
+
+ const target = {};
+
+ on(target, "message", () => {
+ throw Error("Boom!");
+ });
+
+ emit(target, "message");
+ },
+
+ testCount() {
+ const target = { name: "target" };
+
+ equal(count(target, "foo"), 0, "no listeners for 'foo' events");
+ on(target, "foo", () => {});
+ equal(count(target, "foo"), 1, "listener registered");
+ on(target, "foo", () => {});
+ equal(count(target, "foo"), 2, "another listener registered");
+ off(target);
+ equal(count(target, "foo"), 0, "listeners unregistered");
+ },
+
+ async testOnce() {
+ const target = { name: "target" };
+ const called = false;
+
+ const pFoo = once(target, "foo", function (value) {
+ ok(!called, "listener called only once");
+ equal(value, "bar", "correct argument was passed");
+ equal(this, target, "the contextual object is correct");
+ });
+ const pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+ testRemovingOnce(done) {
+ const target = { name: "target" };
+
+ once(target, "foo", fail);
+ once(target, "done", done);
+
+ off(target, "foo", fail);
+
+ emit(target, "foo", "listener was called");
+ emit(target, "done", "");
+ },
+
+ testAddListenerWithHandlerMethod() {
+ const target = { name: "target" };
+ const actual = [];
+ const listener = function (...args) {
+ equal(
+ this,
+ target,
+ "the contextual object is correct for function listener"
+ );
+ deepEqual(args, [10, 20, 30], "arguments are properly passed");
+ };
+
+ const object = {
+ name: "target",
+ [handler](type, ...rest) {
+ actual.push(type);
+ equal(
+ this,
+ object,
+ "the contextual object is correct for object listener"
+ );
+ deepEqual(rest, [10, 20, 30], "arguments are properly passed");
+ },
+ };
+
+ on(target, "foo", listener);
+ on(target, "bar", object);
+ on(target, "baz", object);
+
+ emit(target, "foo", 10, 20, 30);
+ emit(target, "bar", 10, 20, 30);
+ emit(target, "baz", 10, 20, 30);
+
+ deepEqual(
+ actual,
+ ["bar", "baz"],
+ "object's listener called in the expected order"
+ );
+ },
+
+ testRemoveListenerWithHandlerMethod() {
+ const target = {};
+ const actual = [];
+
+ const object = {
+ [handler](type) {
+ actual.push(1);
+ on(target, "message", () => {
+ off(target, "message", object);
+ actual.push(2);
+ });
+ },
+ };
+
+ on(target, "message", object);
+
+ emit(target, "message");
+ deepEqual([1], actual, "first listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2], actual, "second listener called");
+
+ emit(target, "message");
+ deepEqual([1, 1, 2, 2, 2], actual, "first listener removed");
+ },
+
+ async testOnceListenerWithHandlerMethod() {
+ const target = { name: "target" };
+ const called = false;
+
+ const object = {
+ [handler](type, value) {
+ ok(!called, "listener called only once");
+ equal(type, "foo", "event type is properly passed");
+ equal(value, "bar", "correct argument was passed");
+ equal(
+ this,
+ object,
+ "the contextual object is correct for object listener"
+ );
+ },
+ };
+
+ const pFoo = once(target, "foo", object);
+
+ const pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+ testCallingOffWithMoreThan3Args() {
+ const target = { name: "target" };
+ on(target, "data", fail);
+ off(target, "data", fail, undefined);
+ emit(target, "data", "Listener should be removed");
+ },
+};
+
+/**
+ * Create a runnable tests based on the tests descriptor given.
+ *
+ * @param {Object} tests
+ * The tests descriptor object, contains the tests to run.
+ */
+const runnable = tests =>
+ async function () {
+ for (const name of Object.keys(tests)) {
+ info(name);
+ if (tests[name].length === 1) {
+ await new Promise(resolve => tests[name](resolve));
+ } else {
+ await tests[name]();
+ }
+ }
+ };
+
+add_task(runnable(TESTS));
diff --git a/devtools/shared/tests/xpcshell/test_executeSoon.js b/devtools/shared/tests/xpcshell/test_executeSoon.js
new file mode 100644
index 0000000000..acb60360a1
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_executeSoon.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Client request stacks should span the entire process from before making the
+ * request to handling the reply from the server. The server frames are not
+ * included, nor can they be in most cases, since the server can be a remote
+ * device.
+ */
+
+var { executeSoon } = require("resource://devtools/shared/DevToolsUtils.js");
+
+add_task(async function () {
+ await waitForTick();
+
+ let stack = Components.stack;
+ while (stack) {
+ info(stack.name);
+ if (stack.name == "waitForTick") {
+ // Reached back to outer function before executeSoon
+ ok(true, "Complete stack");
+ return;
+ }
+ stack = stack.asyncCaller || stack.caller;
+ }
+ ok(false, "Incomplete stack");
+});
+
+function waitForTick() {
+ return new Promise(resolve => {
+ executeSoon(resolve);
+ });
+}
diff --git a/devtools/shared/tests/xpcshell/test_fetch-bom.js b/devtools/shared/tests/xpcshell/test_fetch-bom.js
new file mode 100644
index 0000000000..566133fda6
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-bom.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch BOM detection.
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+function write8(bos) {
+ bos.write8(0xef);
+ bos.write8(0xbb);
+ bos.write8(0xbf);
+ bos.write8(0x68);
+ bos.write8(0xc4);
+ bos.write8(0xb1);
+}
+
+function write16be(bos) {
+ bos.write8(0xfe);
+ bos.write8(0xff);
+ bos.write8(0x00);
+ bos.write8(0x68);
+ bos.write8(0x01);
+ bos.write8(0x31);
+}
+
+function write16le(bos) {
+ bos.write8(0xff);
+ bos.write8(0xfe);
+ bos.write8(0x68);
+ bos.write8(0x00);
+ bos.write8(0x31);
+ bos.write8(0x01);
+}
+
+function getHandler(writer) {
+ return function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ const bos = new BinaryOutputStream(response.bodyOutputStream);
+ writer(bos);
+ };
+}
+
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.registerPathHandler("/u8", getHandler(write8));
+server.registerPathHandler("/u16be", getHandler(write16be));
+server.registerPathHandler("/u16le", getHandler(write16le));
+server.start(-1);
+
+const port = server.identity.primaryPort;
+const serverURL = "http://localhost:" + port;
+
+do_get_profile();
+
+registerCleanupFunction(() => {
+ return new Promise(resolve => server.stop(resolve));
+});
+
+add_task(async function () {
+ await test_one(serverURL + "/u8", "UTF-8");
+ await test_one(serverURL + "/u16be", "UTF-16BE");
+ await test_one(serverURL + "/u16le", "UTF-16LE");
+});
+
+async function test_one(url, encoding) {
+ // Be sure to set the encoding to something that will yield an
+ // invalid result if BOM sniffing is not done.
+ await DevToolsUtils.fetch(url, { charset: "ISO-8859-1" }).then(
+ ({ content }) => {
+ Assert.equal(content, "hı", "The content looks correct for " + encoding);
+ }
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_fetch-chrome.js b/devtools/shared/tests/xpcshell/test_fetch-chrome.js
new file mode 100644
index 0000000000..38021d49c9
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-chrome.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on chrome:// URI's.
+
+const URL_FOUND = "chrome://devtools-shared/locale/debugger.properties";
+const URL_NOT_FOUND = "chrome://this/is/not/here.js";
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(async function test_missing() {
+ await DevToolsUtils.fetch(URL_NOT_FOUND).then(
+ result => {
+ info(result);
+ ok(false, "fetch resolved unexpectedly for non-existent chrome:// URI");
+ },
+ () => {
+ ok(true, "fetch rejected as the chrome:// URI was non-existent.");
+ }
+ );
+});
+
+/**
+ * Tests that existing files are handled correctly.
+ */
+add_task(async function test_normal() {
+ await DevToolsUtils.fetch(URL_FOUND).then(result => {
+ notDeepEqual(
+ result.content,
+ "",
+ "chrome:// URI seems to be read correctly."
+ );
+ });
+});
diff --git a/devtools/shared/tests/xpcshell/test_fetch-file.js b/devtools/shared/tests/xpcshell/test_fetch-file.js
new file mode 100644
index 0000000000..47d3f52681
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-file.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on file:// URI's.
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+const TEST_CONTENT = "aéd";
+
+// The TEST_CONTENT encoded as UTF-8.
+const UTF8_TEST_BUFFER = new Uint8Array([0x61, 0xc3, 0xa9, 0x64]);
+
+// The TEST_CONTENT encoded as ISO 8859-1.
+const ISO_8859_1_BUFFER = new Uint8Array([0x61, 0xe9, 0x64]);
+
+/**
+ * Tests that URLs with arrows pointing to an actual source are handled properly
+ * (bug 808960). For example 'resource://gre/modules/XPIProvider.sys.mjs ->
+ * file://l10n.js' should load 'file://l10n.js'.
+ */
+add_task(async function test_arrow_urls() {
+ const { path } = createTemporaryFile(".js");
+ const url = "resource://gre/modules/XPIProvider.sys.mjs -> file://" + path;
+
+ await IOUtils.writeUTF8(path, TEST_CONTENT);
+ const { content } = await DevToolsUtils.fetch(url);
+
+ deepEqual(content, TEST_CONTENT, "The file contents were correctly read.");
+});
+
+/**
+ * Tests that empty files are read correctly.
+ */
+add_task(async function test_empty() {
+ const { path } = createTemporaryFile();
+ const { content } = await DevToolsUtils.fetch("file://" + path);
+ deepEqual(content, "", "The empty file was read correctly.");
+});
+
+/**
+ * Tests that UTF-8 encoded files are correctly read.
+ */
+add_task(async function test_encoding_utf8() {
+ const { path } = createTemporaryFile();
+ await IOUtils.write(path, UTF8_TEST_BUFFER);
+
+ const { content } = await DevToolsUtils.fetch(path);
+ deepEqual(
+ content,
+ TEST_CONTENT,
+ "The UTF-8 encoded file was correctly read."
+ );
+});
+
+/**
+ * Tests that ISO 8859-1 (Latin-1) encoded files are correctly read.
+ */
+add_task(async function test_encoding_iso_8859_1() {
+ const { path } = createTemporaryFile();
+ await IOUtils.write(path, ISO_8859_1_BUFFER);
+
+ const { content } = await DevToolsUtils.fetch(path);
+ deepEqual(
+ content,
+ TEST_CONTENT,
+ "The ISO 8859-1 encoded file was correctly read."
+ );
+});
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(async function test_missing() {
+ await DevToolsUtils.fetch("file:///file/not/found.right").then(
+ result => {
+ info(result);
+ ok(false, "Fetch resolved unexpectedly when the file was not found.");
+ },
+ () => {
+ ok(true, "Fetch rejected as expected because the file was not found.");
+ }
+ );
+});
+
+/**
+ * Test that URLs without file:// scheme work.
+ */
+add_task(async function test_schemeless_files() {
+ const { path } = createTemporaryFile();
+
+ await IOUtils.writeUTF8(path, TEST_CONTENT);
+
+ const { content } = await DevToolsUtils.fetch(path);
+ deepEqual(content, TEST_CONTENT, "The content was correct.");
+});
+
+/**
+ * Creates a temporary file that is removed after the test completes.
+ */
+function createTemporaryFile(extension) {
+ const name = "test_fetch-file-" + Math.random() + (extension || "");
+ const file = new FileUtils.File(PathUtils.join(PathUtils.tempDir, name));
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0755", 8));
+
+ registerCleanupFunction(() => {
+ file.remove(false);
+ });
+
+ return file;
+}
diff --git a/devtools/shared/tests/xpcshell/test_fetch-http.js b/devtools/shared/tests/xpcshell/test_fetch-http.js
new file mode 100644
index 0000000000..deb9c7b8f6
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-http.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on http:// URI's.
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.registerPathHandler("/cached.json", cacheRequestHandler);
+server.start(-1);
+
+const port = server.identity.primaryPort;
+const serverURL = "http://localhost:" + port;
+const CACHED_URL = serverURL + "/cached.json";
+const NORMAL_URL = serverURL + "/test_fetch-http.js";
+
+function cacheRequestHandler(request, response) {
+ info("Got request for " + request.path);
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+
+ const body = "[" + Math.random() + "]";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+do_get_profile();
+
+registerCleanupFunction(() => {
+ return new Promise(resolve => server.stop(resolve));
+});
+
+add_task(async function test_normal() {
+ await DevToolsUtils.fetch(NORMAL_URL).then(({ content }) => {
+ ok(
+ content.includes("The content looks correct."),
+ "The content looks correct."
+ );
+ });
+});
+
+add_task(async function test_caching() {
+ let initialContent = null;
+
+ info("Performing the first request.");
+ await DevToolsUtils.fetch(CACHED_URL).then(({ content }) => {
+ info("Got the first response: " + content);
+ initialContent = content;
+ });
+
+ info("Performing another request, expecting to get cached response.");
+ await DevToolsUtils.fetch(CACHED_URL).then(({ content }) => {
+ deepEqual(content, initialContent, "The content was loaded from cache.");
+ });
+
+ info("Performing a third request with cache bypassed.");
+ const opts = { loadFromCache: false };
+ await DevToolsUtils.fetch(CACHED_URL, opts).then(({ content }) => {
+ notDeepEqual(
+ content,
+ initialContent,
+ "The URL wasn't loaded from cache with loadFromCache: false."
+ );
+ });
+});
diff --git a/devtools/shared/tests/xpcshell/test_fetch-resource.js b/devtools/shared/tests/xpcshell/test_fetch-resource.js
new file mode 100644
index 0000000000..a320d63151
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_fetch-resource.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests for DevToolsUtils.fetch on resource:// URI's.
+
+const URL_FOUND = "resource://devtools/shared/DevToolsUtils.js";
+const URL_NOT_FOUND = "resource://devtools/this/is/not/here.js";
+
+// Disable `xpc::IsInAutomation()` so we don't crash when accessing a
+// nonexistent resource URI.
+Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+);
+
+/**
+ * Test that non-existent files are handled correctly.
+ */
+add_task(async function test_missing() {
+ await DevToolsUtils.fetch(URL_NOT_FOUND).then(
+ result => {
+ info(result);
+ ok(false, "fetch resolved unexpectedly for non-existent resource:// URI");
+ },
+ () => {
+ ok(true, "fetch rejected as the resource:// URI was non-existent.");
+ }
+ );
+});
+
+/**
+ * Tests that existing files are handled correctly.
+ */
+add_task(async function test_normal() {
+ await DevToolsUtils.fetch(URL_FOUND).then(result => {
+ notDeepEqual(
+ result.content,
+ "",
+ "resource:// URI seems to be read correctly."
+ );
+ });
+});
diff --git a/devtools/shared/tests/xpcshell/test_flatten.js b/devtools/shared/tests/xpcshell/test_flatten.js
new file mode 100644
index 0000000000..2e2a80a9d5
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_flatten.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test ThreadSafeDevToolsUtils.flatten
+
+function run_test() {
+ const { flatten } = DevToolsUtils;
+
+ const flat = flatten([
+ ["a", "b", "c"],
+ ["d", "e", "f"],
+ ["g", "h", "i"],
+ ]);
+
+ equal(flat.length, 9);
+ equal(flat[0], "a");
+ equal(flat[1], "b");
+ equal(flat[2], "c");
+ equal(flat[3], "d");
+ equal(flat[4], "e");
+ equal(flat[5], "f");
+ equal(flat[6], "g");
+ equal(flat[7], "h");
+ equal(flat[8], "i");
+}
diff --git a/devtools/shared/tests/xpcshell/test_indentation.js b/devtools/shared/tests/xpcshell/test_indentation.js
new file mode 100644
index 0000000000..7ed84c6a87
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_indentation.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ EXPAND_TAB,
+ TAB_SIZE,
+ DETECT_INDENT,
+ getTabPrefs,
+ getIndentationFromPrefs,
+ getIndentationFromIteration,
+ getIndentationFromString,
+} = require("resource://devtools/shared/indentation.js");
+
+function test_indent_from_prefs() {
+ Services.prefs.setBoolPref(DETECT_INDENT, true);
+ equal(
+ getIndentationFromPrefs(),
+ false,
+ "getIndentationFromPrefs returning false"
+ );
+
+ Services.prefs.setIntPref(TAB_SIZE, 73);
+ Services.prefs.setBoolPref(EXPAND_TAB, false);
+ Services.prefs.setBoolPref(DETECT_INDENT, false);
+ deepEqual(
+ getTabPrefs(),
+ { indentUnit: 73, indentWithTabs: true },
+ "getTabPrefs basic test"
+ );
+ deepEqual(
+ getIndentationFromPrefs(),
+ { indentUnit: 73, indentWithTabs: true },
+ "getIndentationFromPrefs basic test"
+ );
+}
+
+const TESTS = [
+ {
+ desc: "two spaces",
+ input: [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ " color: red;",
+ " background: blue;",
+ "}",
+ " ",
+ "span {",
+ " padding-left: 10px;",
+ "}",
+ ],
+ expected: { indentUnit: 2, indentWithTabs: false },
+ },
+ {
+ desc: "four spaces",
+ input: [
+ "var obj = {",
+ " addNumbers: function() {",
+ " var x = 5;",
+ " var y = 18;",
+ " return x + y;",
+ " },",
+ " ",
+ " /*",
+ " * Do some stuff to two numbers",
+ " * ",
+ " * @param x",
+ " * @param y",
+ " * ",
+ " * @return the result of doing stuff",
+ " */",
+ " subtractNumbers: function(x, y) {",
+ " var x += 7;",
+ " var y += 18;",
+ " var result = x - y;",
+ " result %= 2;",
+ " }",
+ "}",
+ ],
+ expected: { indentUnit: 4, indentWithTabs: false },
+ },
+ {
+ desc: "tabs",
+ input: [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ "\tcolor: red;",
+ "\tbackground: blue;",
+ "}",
+ "",
+ "span {",
+ "\tpadding-left: 10px;",
+ "}",
+ ],
+ expected: { indentUnit: 2, indentWithTabs: true },
+ },
+ {
+ desc: "no indent",
+ input: [
+ "var x = 0;",
+ " // stray thing",
+ "var y = 9;",
+ " ",
+ "",
+ ],
+ expected: { indentUnit: 2, indentWithTabs: false },
+ },
+];
+
+function test_indent_detection() {
+ Services.prefs.setIntPref(TAB_SIZE, 2);
+ Services.prefs.setBoolPref(EXPAND_TAB, true);
+ Services.prefs.setBoolPref(DETECT_INDENT, true);
+
+ for (const test of TESTS) {
+ const iterFn = function (start, end, callback) {
+ test.input.slice(start, end).forEach(callback);
+ };
+
+ deepEqual(
+ getIndentationFromIteration(iterFn),
+ test.expected,
+ "test getIndentationFromIteration " + test.desc
+ );
+ }
+
+ for (const test of TESTS) {
+ deepEqual(
+ getIndentationFromString(test.input.join("\n")),
+ test.expected,
+ "test getIndentationFromString " + test.desc
+ );
+ }
+}
+
+function run_test() {
+ try {
+ test_indent_from_prefs();
+ test_indent_detection();
+ } finally {
+ Services.prefs.clearUserPref(TAB_SIZE);
+ Services.prefs.clearUserPref(EXPAND_TAB);
+ Services.prefs.clearUserPref(DETECT_INDENT);
+ }
+}
diff --git a/devtools/shared/tests/xpcshell/test_independent_loaders.js b/devtools/shared/tests/xpcshell/test_independent_loaders.js
new file mode 100644
index 0000000000..ee8771db25
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_independent_loaders.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that each instance of the Dev Tools loader contains its own loader
+ * instance, and also returns unique objects. This ensures there is no sharing
+ * in place between loaders.
+ */
+function run_test() {
+ const loader1 = new DevToolsLoader();
+ const loader2 = new DevToolsLoader();
+
+ const indent1 = loader1.require("resource://devtools/shared/indentation.js");
+ const indent2 = loader2.require("resource://devtools/shared/indentation.js");
+
+ Assert.ok(indent1 !== indent2);
+
+ Assert.ok(loader1.loader !== loader2.loader);
+ Assert.ok(loader1.id !== loader2.id);
+}
diff --git a/devtools/shared/tests/xpcshell/test_invisible_loader.js b/devtools/shared/tests/xpcshell/test_invisible_loader.js
new file mode 100644
index 0000000000..d83efd9c2a
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_invisible_loader.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+addDebuggerToGlobal(globalThis);
+
+/**
+ * Ensure that sandboxes created via the Dev Tools loader respect the
+ * invisibleToDebugger flag.
+ */
+function run_test() {
+ visible_loader();
+ invisible_loader();
+ // TODO: invisibleToDebugger should be deprecated in favor of
+ // useDistinctSystemPrincipalLoader, but we might move out from the loader
+ // to using only standard imports instead.
+ distinct_system_principal_loader();
+}
+
+function visible_loader() {
+ const loader = new DevToolsLoader({
+ invisibleToDebugger: false,
+ });
+ loader.require("resource://devtools/shared/indentation.js");
+
+ const dbg = new Debugger();
+ const sandbox = loader.loader.sharedGlobal;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ Assert.ok(true);
+ } catch (e) {
+ do_throw("debugger could not add visible value");
+ }
+}
+
+function invisible_loader() {
+ const loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ });
+ loader.require("resource://devtools/shared/indentation.js");
+
+ const dbg = new Debugger();
+ const sandbox = loader.loader.sharedGlobal;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ do_throw("debugger added invisible value");
+ } catch (e) {
+ Assert.ok(true);
+ }
+}
+
+function distinct_system_principal_loader() {
+ const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+ } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+ );
+
+ const requester = {};
+ const loader = useDistinctSystemPrincipalLoader(requester);
+ loader.require("resource://devtools/shared/indentation.js");
+
+ const dbg = new Debugger();
+ const sandbox = loader.loader.sharedGlobal;
+
+ try {
+ dbg.addDebuggee(sandbox);
+ do_throw("debugger added invisible value");
+ } catch (e) {
+ Assert.ok(true);
+ }
+ releaseDistinctSystemPrincipalLoader(requester);
+}
diff --git a/devtools/shared/tests/xpcshell/test_isSet.js b/devtools/shared/tests/xpcshell/test_isSet.js
new file mode 100644
index 0000000000..73ccb6fe6a
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_isSet.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test ThreadSafeDevToolsUtils.isSet
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+
+ const { isSet } = DevToolsUtils;
+
+ equal(isSet(new Set()), true);
+ equal(isSet(new Map()), false);
+ equal(isSet({}), false);
+ equal(isSet("I swear I'm a Set"), false);
+ equal(isSet(5), false);
+
+ const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+ const sandbox = new Cu.Sandbox(systemPrincipal);
+
+ equal(isSet(Cu.evalInSandbox("new Set()", sandbox)), true);
+ equal(isSet(Cu.evalInSandbox("new Map()", sandbox)), false);
+ equal(isSet(Cu.evalInSandbox("({})", sandbox)), false);
+ equal(isSet(Cu.evalInSandbox("'I swear I\\'m a Set'", sandbox)), false);
+ equal(isSet(Cu.evalInSandbox("5", sandbox)), false);
+}
diff --git a/devtools/shared/tests/xpcshell/test_loader.js b/devtools/shared/tests/xpcshell/test_loader.js
new file mode 100644
index 0000000000..56ee6459d3
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_loader.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+} = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+);
+
+function run_test() {
+ const requester = {},
+ requester2 = {};
+
+ const loader = useDistinctSystemPrincipalLoader(requester);
+
+ // The DevTools dedicated global forces invisibleToDebugger on its realm at
+ // https://searchfox.org/mozilla-central/rev/12a18f7e112a4dcf88d8441d439b84144bfbe9a3/js/xpconnect/loader/mozJSModuleLoader.cpp#591-593
+ // but this is not observable directly.
+ const DevToolsSpecialGlobal = Cu.getGlobalForObject(
+ ChromeUtils.importESModule(
+ "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs",
+ { loadInDevToolsLoader: true }
+ )
+ );
+
+ const regularLoader = new DevToolsLoader();
+ Assert.notStrictEqual(
+ DevToolsSpecialGlobal,
+ regularLoader.loader.sharedGlobal,
+ "The regular loader is not using the special DevTools global"
+ );
+
+ info("Assert the key difference with the other regular loaders:");
+ Assert.strictEqual(
+ DevToolsSpecialGlobal,
+ loader.loader.sharedGlobal,
+ "The system principal loader is using the special DevTools global"
+ );
+
+ ok(loader.loader, "Loader is not destroyed before calling release");
+
+ info("Now assert the precise behavior of release");
+ releaseDistinctSystemPrincipalLoader({});
+ ok(
+ loader.loader,
+ "Loader is still not destroyed after calling release with another requester"
+ );
+
+ releaseDistinctSystemPrincipalLoader(requester);
+ ok(
+ !loader.loader,
+ "Loader is destroyed after calling release with the right requester"
+ );
+
+ info("Now test the behavior with two concurrent usages");
+ const loader2 = useDistinctSystemPrincipalLoader(requester);
+ Assert.notEqual(loader, loader2, "We get a new loader instance");
+ Assert.strictEqual(
+ DevToolsSpecialGlobal,
+ loader2.loader.sharedGlobal,
+ "The new system principal loader is also using the special DevTools global"
+ );
+
+ const loader3 = useDistinctSystemPrincipalLoader(requester2);
+ Assert.equal(loader2, loader3, "The two loader last loaders are shared");
+
+ releaseDistinctSystemPrincipalLoader(requester);
+ ok(loader2.loader, "Loader isn't destroy on the first call to destroy");
+
+ releaseDistinctSystemPrincipalLoader(requester2);
+ ok(!loader2.loader, "Loader is destroyed on the second call to destroy");
+}
diff --git a/devtools/shared/tests/xpcshell/test_natural-sort.js b/devtools/shared/tests/xpcshell/test_natural-sort.js
new file mode 100644
index 0000000000..d01b984c73
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_natural-sort.js
@@ -0,0 +1,959 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+const {
+ naturalSortCaseSensitive,
+ naturalSortCaseInsensitive,
+} = require("resource://devtools/shared/natural-sort.js");
+
+function run_test() {
+ test("different values types", function () {
+ runTest(["a", 1], [1, "a"], "number always comes first");
+ runTest(
+ ["1", 1],
+ ["1", 1],
+ "number vs numeric string - should remain unchanged (error in chrome)"
+ );
+ runTest(
+ ["02", 3, 2, "01"],
+ ["01", "02", 2, 3],
+ "padding numeric string vs number"
+ );
+ });
+
+ test("datetime", function () {
+ runTest(
+ ["10/12/2008", "10/11/2008", "10/11/2007", "10/12/2007"],
+ ["10/11/2007", "10/12/2007", "10/11/2008", "10/12/2008"],
+ "similar dates"
+ );
+ runTest(
+ ["01/01/2008", "01/10/2008", "01/01/1992", "01/01/1991"],
+ ["01/01/1991", "01/01/1992", "01/01/2008", "01/10/2008"],
+ "similar dates"
+ );
+ // Years should expand to 0100, 2001, 2010
+ runTest(
+ ["1/1/100", "1/1/1", "1/1/10"],
+ ["1/1/100", "1/1/1", "1/1/10"],
+ "dates with short year formatting"
+ );
+ runTest(
+ [
+ "Wed Jan 01 2010 00:00:00 GMT-0800 (Pacific Standard Time)",
+ "Thu Dec 31 2009 00:00:00 GMT-0800 (Pacific Standard Time)",
+ "Wed Jan 01 2010 00:00:00 GMT-0500 (Eastern Standard Time)",
+ ],
+ [
+ "Thu Dec 31 2009 00:00:00 GMT-0800 (Pacific Standard Time)",
+ "Wed Jan 01 2010 00:00:00 GMT-0500 (Eastern Standard Time)",
+ "Wed Jan 01 2010 00:00:00 GMT-0800 (Pacific Standard Time)",
+ ],
+ "javascript toString(), different timezones"
+ );
+ runTest(
+ [
+ "Saturday, July 3, 2010",
+ "Monday, August 2, 2010",
+ "Monday, May 3, 2010",
+ ],
+ [
+ "Monday, May 3, 2010",
+ "Saturday, July 3, 2010",
+ "Monday, August 2, 2010",
+ ],
+ "Date.toString(), Date.toLocaleString()"
+ );
+ runTest(
+ [
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 3 May 2010 17:45:30 GMT",
+ "Mon, 15 Jun 2009 17:45:30 GMT",
+ ],
+ [
+ "Mon, 15 Jun 2009 17:45:30 GMT",
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 3 May 2010 17:45:30 GMT",
+ ],
+ "Date.toUTCString()"
+ );
+ runTest(
+ [
+ "Saturday, July 3, 2010 1:45 PM",
+ "Saturday, July 3, 2010 1:45 AM",
+ "Monday, August 2, 2010 1:45 PM",
+ "Monday, May 3, 2010 1:45 PM",
+ ],
+ [
+ "Monday, May 3, 2010 1:45 PM",
+ "Saturday, July 3, 2010 1:45 AM",
+ "Saturday, July 3, 2010 1:45 PM",
+ "Monday, August 2, 2010 1:45 PM",
+ ],
+ ""
+ );
+ runTest(
+ [
+ "Saturday, July 3, 2010 1:45:30 PM",
+ "Saturday, July 3, 2010 1:45:29 PM",
+ "Monday, August 2, 2010 1:45:01 PM",
+ "Monday, May 3, 2010 1:45:00 PM",
+ ],
+ [
+ "Monday, May 3, 2010 1:45:00 PM",
+ "Saturday, July 3, 2010 1:45:29 PM",
+ "Saturday, July 3, 2010 1:45:30 PM",
+ "Monday, August 2, 2010 1:45:01 PM",
+ ],
+ ""
+ );
+ runTest(
+ ["2/15/2009 1:45 PM", "1/15/2009 1:45 PM", "2/15/2009 1:45 AM"],
+ ["1/15/2009 1:45 PM", "2/15/2009 1:45 AM", "2/15/2009 1:45 PM"],
+ ""
+ );
+ runTest(
+ [
+ "2010-06-15T13:45:30",
+ "2009-06-15T13:45:30",
+ "2009-06-15T01:45:30.2",
+ "2009-01-15T01:45:30",
+ ],
+ [
+ "2009-01-15T01:45:30",
+ "2009-06-15T01:45:30.2",
+ "2009-06-15T13:45:30",
+ "2010-06-15T13:45:30",
+ ],
+ "ISO8601 Dates"
+ );
+ runTest(
+ ["2010-06-15 13:45:30", "2009-06-15 13:45:30", "2009-01-15 01:45:30"],
+ ["2009-01-15 01:45:30", "2009-06-15 13:45:30", "2010-06-15 13:45:30"],
+ "ISO8601-ish YYYY-MM-DDThh:mm:ss - which does not parse into a Date instance"
+ );
+ runTest(
+ [
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 15 Jun 2009 20:45:30 PDT",
+ "Mon, 15 Jun 2009 20:45:30 EST",
+ ],
+ [
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Mon, 15 Jun 2009 20:45:30 EST",
+ "Mon, 15 Jun 2009 20:45:30 PDT",
+ ],
+ "RFC1123 testing different timezones"
+ );
+ runTest(
+ ["1245098730000", "14330728000", "1245098728000"],
+ ["14330728000", "1245098728000", "1245098730000"],
+ "unix epoch, Date.getTime()"
+ );
+ runTest(
+ [
+ new Date("2001-01-10"),
+ "2015-01-01",
+ new Date("2001-01-01"),
+ "1998-01-01",
+ ],
+ [
+ "1998-01-01",
+ new Date("2001-01-01"),
+ new Date("2001-01-10"),
+ "2015-01-01",
+ ],
+ "mixed Date types"
+ );
+ runTest(
+ [
+ "Tue, 29 Jun 2021 11:31:17 GMT",
+ "Sun, 14 Jun 2009 11:11:15 GMT",
+ sessionString,
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ ],
+ [
+ sessionString,
+ "Sun, 14 Jun 2009 11:11:15 GMT",
+ "Mon, 15 Jun 2009 20:45:30 GMT",
+ "Tue, 29 Jun 2021 11:31:17 GMT",
+ ],
+ `"${sessionString}" amongst date strings`
+ );
+ runTest(
+ [
+ "Wed, 04 Sep 2024 09:11:44 GMT",
+ sessionString,
+ "Tue, 06 Sep 2022 09:11:44 GMT",
+ sessionString,
+ "Mon, 05 Sep 2022 09:12:41 GMT",
+ ],
+ [
+ sessionString,
+ sessionString,
+ "Mon, 05 Sep 2022 09:12:41 GMT",
+ "Tue, 06 Sep 2022 09:11:44 GMT",
+ "Wed, 04 Sep 2024 09:11:44 GMT",
+ ],
+ `"${sessionString}" amongst date strings (complex)`
+ );
+
+ runTest(
+ [
+ "Madras",
+ "Jalfrezi",
+ "Rogan Josh",
+ "Vindaloo",
+ "Tikka Masala",
+ sessionString,
+ "Masala",
+ "Korma",
+ ],
+ [
+ "Jalfrezi",
+ "Korma",
+ "Madras",
+ "Masala",
+ "Rogan Josh",
+ sessionString,
+ "Tikka Masala",
+ "Vindaloo",
+ ],
+ `"${sessionString}" amongst strings`
+ );
+ });
+
+ test("version number strings", function () {
+ runTest(
+ ["1.0.2", "1.0.1", "1.0.0", "1.0.9"],
+ ["1.0.0", "1.0.1", "1.0.2", "1.0.9"],
+ "close version numbers"
+ );
+ runTest(
+ ["1.1.100", "1.1.1", "1.1.10", "1.1.54"],
+ ["1.1.1", "1.1.10", "1.1.54", "1.1.100"],
+ "more version numbers"
+ );
+ runTest(
+ ["1.0.03", "1.0.003", "1.0.002", "1.0.0001"],
+ ["1.0.0001", "1.0.002", "1.0.003", "1.0.03"],
+ "multi-digit branch release"
+ );
+ runTest(
+ [
+ "1.1beta",
+ "1.1.2alpha3",
+ "1.0.2alpha3",
+ "1.0.2alpha1",
+ "1.0.1alpha4",
+ "2.1.2",
+ "2.1.1",
+ ],
+ [
+ "1.0.1alpha4",
+ "1.0.2alpha1",
+ "1.0.2alpha3",
+ "1.1.2alpha3",
+ "1.1beta",
+ "2.1.1",
+ "2.1.2",
+ ],
+ "close version numbers"
+ );
+ runTest(
+ [
+ "myrelease-1.1.3",
+ "myrelease-1.2.3",
+ "myrelease-1.1.4",
+ "myrelease-1.1.1",
+ "myrelease-1.0.5",
+ ],
+ [
+ "myrelease-1.0.5",
+ "myrelease-1.1.1",
+ "myrelease-1.1.3",
+ "myrelease-1.1.4",
+ "myrelease-1.2.3",
+ ],
+ "string first"
+ );
+ runTest(
+ [
+ "1.1.3",
+ "a-release-1.1.3",
+ "b-release-1.1.3",
+ "1.2.3",
+ "a-release-1.2.3",
+ "b-release-1.2.3",
+ "1.1.4",
+ "a-release-1.1.4",
+ "b-release-1.1.4",
+ "1.1.1",
+ "a-release-1.1.1",
+ "b-release-1.1.1",
+ "1.0.5",
+ "a-release-1.0.5",
+ "b-release-1.0.5",
+ ],
+ [
+ "1.0.5",
+ "1.1.1",
+ "1.1.3",
+ "1.1.4",
+ "1.2.3",
+ "a-release-1.0.5",
+ "a-release-1.1.1",
+ "a-release-1.1.3",
+ "a-release-1.1.4",
+ "a-release-1.2.3",
+ "b-release-1.0.5",
+ "b-release-1.1.1",
+ "b-release-1.1.3",
+ "b-release-1.1.4",
+ "b-release-1.2.3",
+ ],
+ "string first, different names"
+ );
+ runTest(
+ ["zstring", "astring", "release-1.1.3"],
+ ["astring", "release-1.1.3", "zstring"],
+ "string first, mixed with regular strings"
+ );
+ });
+
+ test("numerics", function () {
+ runTest(["10", 9, 2, "1", "4"], ["1", 2, "4", 9, "10"], "string vs number");
+ runTest(
+ ["0001", "002", "001"],
+ ["0001", "001", "002"],
+ "0 left-padded numbers"
+ );
+ runTest(
+ [2, 1, "1", "0001", "002", "02", "001"],
+ [1, "1", "0001", "001", 2, "002", "02"],
+ "0 left-padded numbers and regular numbers"
+ );
+ runTest(
+ ["10.0401", 10.022, 10.042, "10.021999"],
+ ["10.021999", 10.022, "10.0401", 10.042],
+ "decimal string vs decimal, different precision"
+ );
+ runTest(
+ ["10.04", 10.02, 10.03, "10.01"],
+ ["10.01", 10.02, 10.03, "10.04"],
+ "decimal string vs decimal, same precision"
+ );
+ runTest(
+ ["10.04f", "10.039F", "10.038d", "10.037D"],
+ ["10.037D", "10.038d", "10.039F", "10.04f"],
+ "float/decimal with 'F' or 'D' notation"
+ );
+ runTest(
+ ["10.004Z", "10.039T", "10.038ooo", "10.037g"],
+ ["10.004Z", "10.037g", "10.038ooo", "10.039T"],
+ "not foat/decimal notation"
+ );
+ runTest(
+ ["1.528535047e5", "1.528535047e7", "1.52e15", "1.528535047e3", "1.59e-3"],
+ ["1.59e-3", "1.528535047e3", "1.528535047e5", "1.528535047e7", "1.52e15"],
+ "scientific notation"
+ );
+ runTest(
+ ["-1", "-2", "4", "-3", "0", "-5"],
+ ["-5", "-3", "-2", "-1", "0", "4"],
+ "negative numbers as strings"
+ );
+ runTest(
+ [-1, "-2", 4, -3, "0", "-5"],
+ ["-5", -3, "-2", -1, "0", 4],
+ "negative numbers as strings - mixed input type, string + numeric"
+ );
+ runTest(
+ [-2.01, -2.1, 4.144, 4.1, -2.001, -5],
+ [-5, -2.1, -2.01, -2.001, 4.1, 4.144],
+ "negative floats - all numeric"
+ );
+ });
+
+ test("IP addresses", function () {
+ runTest(
+ [
+ "192.168.0.100",
+ "192.168.0.1",
+ "192.168.1.1",
+ "192.168.0.250",
+ "192.168.1.123",
+ "10.0.0.2",
+ "10.0.0.1",
+ ],
+ [
+ "10.0.0.1",
+ "10.0.0.2",
+ "192.168.0.1",
+ "192.168.0.100",
+ "192.168.0.250",
+ "192.168.1.1",
+ "192.168.1.123",
+ ]
+ );
+ });
+
+ test("filenames", function () {
+ runTest(
+ ["img12.png", "img10.png", "img2.png", "img1.png"],
+ ["img1.png", "img2.png", "img10.png", "img12.png"],
+ "simple image filenames"
+ );
+ runTest(
+ [
+ "car.mov",
+ "01alpha.sgi",
+ "001alpha.sgi",
+ "my.string_41299.tif",
+ "organic2.0001.sgi",
+ ],
+ [
+ "001alpha.sgi",
+ "01alpha.sgi",
+ "car.mov",
+ "my.string_41299.tif",
+ "organic2.0001.sgi",
+ ],
+ "complex filenames"
+ );
+ runTest(
+ [
+ "./system/kernel/js/01_ui.core.js",
+ "./system/kernel/js/00_jquery-1.3.2.js",
+ "./system/kernel/js/02_my.desktop.js",
+ ],
+ [
+ "./system/kernel/js/00_jquery-1.3.2.js",
+ "./system/kernel/js/01_ui.core.js",
+ "./system/kernel/js/02_my.desktop.js",
+ ],
+ "unix filenames"
+ );
+ });
+
+ test("space(s) as first character(s)", function () {
+ runTest(["alpha", " 1", " 3", " 2", 0], [0, " 1", " 2", " 3", "alpha"]);
+ });
+
+ test("empty strings and space character", function () {
+ runTest(
+ ["10023", "999", "", 2, 5.663, 5.6629],
+ ["", 2, 5.6629, 5.663, "999", "10023"]
+ );
+ runTest([0, "0", ""], [0, "0", ""]);
+ });
+
+ test("hex", function () {
+ runTest(["0xA", "0x9", "0x99"], ["0x9", "0xA", "0x99"], "real hex numbers");
+ runTest(
+ ["0xZZ", "0xVVV", "0xVEV", "0xUU"],
+ ["0xUU", "0xVEV", "0xVVV", "0xZZ"],
+ "fake hex numbers"
+ );
+ });
+
+ test("unicode", function () {
+ runTest(
+ ["\u0044", "\u0055", "\u0054", "\u0043"],
+ ["\u0043", "\u0044", "\u0054", "\u0055"],
+ "basic latin"
+ );
+ });
+
+ test("sparse array sort", function () {
+ const sarray = [3, 2];
+ const sarrayOutput = [1, 2, 3];
+
+ sarray[10] = 1;
+ for (let i = 0; i < 8; i++) {
+ sarrayOutput.push(undefined);
+ }
+ runTest(sarray, sarrayOutput, "simple sparse array");
+ });
+
+ test("case insensitive support", function () {
+ runTest(
+ ["A", "b", "C", "d", "E", "f"],
+ ["A", "b", "C", "d", "E", "f"],
+ "case sensitive pre-sorted array",
+ true
+ );
+ runTest(
+ ["A", "C", "E", "b", "d", "f"],
+ ["A", "b", "C", "d", "E", "f"],
+ "case sensitive un-sorted array",
+ true
+ );
+ runTest(
+ ["A", "C", "E", "b", "d", "f"],
+ ["A", "C", "E", "b", "d", "f"],
+ "case sensitive pre-sorted array"
+ );
+ runTest(
+ ["A", "b", "C", "d", "E", "f"],
+ ["A", "C", "E", "b", "d", "f"],
+ "case sensitive un-sorted array"
+ );
+ });
+
+ test("rosetta code natural sort small test set", function () {
+ runTest(
+ [
+ "ignore leading spaces: 2-2",
+ " ignore leading spaces: 2-1",
+ " ignore leading spaces: 2+0",
+ " ignore leading spaces: 2+1",
+ ],
+ [
+ " ignore leading spaces: 2+0",
+ " ignore leading spaces: 2+1",
+ " ignore leading spaces: 2-1",
+ "ignore leading spaces: 2-2",
+ ],
+ "Ignoring leading spaces"
+ );
+ runTest(
+ [
+ "ignore m.a.s spaces: 2-2",
+ "ignore m.a.s spaces: 2-1",
+ "ignore m.a.s spaces: 2+0",
+ "ignore m.a.s spaces: 2+1",
+ ],
+ [
+ "ignore m.a.s spaces: 2+0",
+ "ignore m.a.s spaces: 2+1",
+ "ignore m.a.s spaces: 2-1",
+ "ignore m.a.s spaces: 2-2",
+ ],
+ "Ignoring multiple adjacent spaces (m.a.s)"
+ );
+ runTest(
+ [
+ "Equiv. spaces: 3-3",
+ "Equiv.\rspaces: 3-2",
+ "Equiv.\x0cspaces: 3-1",
+ "Equiv.\x0bspaces: 3+0",
+ "Equiv.\nspaces: 3+1",
+ "Equiv.\tspaces: 3+2",
+ ],
+ [
+ "Equiv.\x0bspaces: 3+0",
+ "Equiv.\nspaces: 3+1",
+ "Equiv.\tspaces: 3+2",
+ "Equiv.\x0cspaces: 3-1",
+ "Equiv.\rspaces: 3-2",
+ "Equiv. spaces: 3-3",
+ ],
+ "Equivalent whitespace characters"
+ );
+ runTest(
+ [
+ "cASE INDEPENENT: 3-2",
+ "caSE INDEPENENT: 3-1",
+ "casE INDEPENENT: 3+0",
+ "case INDEPENENT: 3+1",
+ ],
+ [
+ "casE INDEPENENT: 3+0",
+ "case INDEPENENT: 3+1",
+ "caSE INDEPENENT: 3-1",
+ "cASE INDEPENENT: 3-2",
+ ],
+ "Case Indepenent sort (naturalSort.insensitive = true)",
+ true
+ );
+ runTest(
+ [
+ "foo100bar99baz0.txt",
+ "foo100bar10baz0.txt",
+ "foo1000bar99baz10.txt",
+ "foo1000bar99baz9.txt",
+ ],
+ [
+ "foo100bar10baz0.txt",
+ "foo100bar99baz0.txt",
+ "foo1000bar99baz9.txt",
+ "foo1000bar99baz10.txt",
+ ],
+ "Numeric fields as numerics"
+ );
+ runTest(
+ [
+ "The Wind in the Willows",
+ "The 40th step more",
+ "The 39 steps",
+ "Wanda",
+ ],
+ [
+ "The 39 steps",
+ "The 40th step more",
+ "The Wind in the Willows",
+ "Wanda",
+ ],
+ "Title sorts"
+ );
+ runTest(
+ [
+ "Equiv. \xfd accents: 2-2",
+ "Equiv. \xdd accents: 2-1",
+ "Equiv. y accents: 2+0",
+ "Equiv. Y accents: 2+1",
+ ],
+ [
+ "Equiv. y accents: 2+0",
+ "Equiv. Y accents: 2+1",
+ "Equiv. \xfd accents: 2-2",
+ "Equiv. \xdd accents: 2-1",
+ ],
+ "Equivalent accented characters (and case) (naturalSort.insensitive = true)",
+ true
+ );
+ // This is not a valuable unicode ordering test
+ // runTest(
+ // ['Start with an \u0292: 2-2', 'Start with an \u017f: 2-1', 'Start with an \xdf: 2+0', 'Start with an s: 2+1'],
+ // ['Start with an s: 2+1', 'Start with an \xdf: 2+0', 'Start with an \u017f: 2-1', 'Start with an \u0292: 2-2'],
+ // 'Character replacements');
+ });
+
+ test("contributed tests", function () {
+ runTest(
+ [
+ "T78",
+ "U17",
+ "U10",
+ "U12",
+ "U14",
+ "745",
+ "U7",
+ "485",
+ "S16",
+ "S2",
+ "S22",
+ "1081",
+ "S25",
+ "1055",
+ "779",
+ "776",
+ "771",
+ "44",
+ "4",
+ "87",
+ "1091",
+ "42",
+ "480",
+ "952",
+ "951",
+ "756",
+ "1000",
+ "824",
+ "770",
+ "666",
+ "633",
+ "619",
+ "1",
+ "991",
+ "77H",
+ "PIER-7",
+ "47",
+ "29",
+ "9",
+ "77L",
+ "433",
+ ],
+ [
+ "1",
+ "4",
+ "9",
+ "29",
+ "42",
+ "44",
+ "47",
+ "77H",
+ "77L",
+ "87",
+ "433",
+ "480",
+ "485",
+ "619",
+ "633",
+ "666",
+ "745",
+ "756",
+ "770",
+ "771",
+ "776",
+ "779",
+ "824",
+ "951",
+ "952",
+ "991",
+ "1000",
+ "1055",
+ "1081",
+ "1091",
+ "PIER-7",
+ "S2",
+ "S16",
+ "S22",
+ "S25",
+ "T78",
+ "U7",
+ "U10",
+ "U12",
+ "U14",
+ "U17",
+ ],
+ "contributed by Bob Zeiner (Chrome not stable sort)"
+ );
+ runTest(
+ [
+ "FSI stop, Position: 5",
+ "Mail Group stop, Position: 5",
+ "Mail Group stop, Position: 5",
+ "FSI stop, Position: 6",
+ "FSI stop, Position: 6",
+ "Newsstand stop, Position: 4",
+ "Newsstand stop, Position: 4",
+ "FSI stop, Position: 5",
+ ],
+ [
+ "FSI stop, Position: 5",
+ "FSI stop, Position: 5",
+ "FSI stop, Position: 6",
+ "FSI stop, Position: 6",
+ "Mail Group stop, Position: 5",
+ "Mail Group stop, Position: 5",
+ "Newsstand stop, Position: 4",
+ "Newsstand stop, Position: 4",
+ ],
+ "contributed by Scott"
+ );
+ runTest(
+ [2, 10, 1, "azd", undefined, "asd"],
+ [1, 2, 10, "asd", "azd", undefined],
+ "issue #2 - undefined support - jarvinen pekka"
+ );
+ runTest(
+ [undefined, undefined, undefined, 1, undefined],
+ [1, undefined, undefined, undefined],
+ "issue #2 - undefined support - jarvinen pekka"
+ );
+ runTest(
+ ["-1", "-2", "4", "-3", "0", "-5"],
+ ["-5", "-3", "-2", "-1", "0", "4"],
+ "issue #3 - invalid numeric string sorting - guilermo.dev"
+ );
+ // native sort implementations are not guaranteed to be stable (i.e. Chrome)
+ // runTest(
+ // ['9','11','22','99','A','aaaa','bbbb','Aaaa','aAaa','aa','AA','Aa','aA','BB','bB','aaA','AaA','aaa'],
+ // ['9', '11', '22', '99', 'A', 'aa', 'AA', 'Aa', 'aA', 'aaA', 'AaA', 'aaa', 'aaaa', 'Aaaa', 'aAaa', 'BB', 'bB', 'bbbb'],
+ // 'issue #5 - invalid sort order - Howie Schecter (naturalSort.insensitive = true)'m true);
+ runTest(
+ [
+ "9",
+ "11",
+ "22",
+ "99",
+ "A",
+ "aaaa",
+ "bbbb",
+ "Aaaa",
+ "aAaa",
+ "aa",
+ "AA",
+ "Aa",
+ "aA",
+ "BB",
+ "bB",
+ "aaA",
+ "AaA",
+ "aaa",
+ ],
+ [
+ "9",
+ "11",
+ "22",
+ "99",
+ "A",
+ "AA",
+ "Aa",
+ "AaA",
+ "Aaaa",
+ "BB",
+ "aA",
+ "aAaa",
+ "aa",
+ "aaA",
+ "aaa",
+ "aaaa",
+ "bB",
+ "bbbb",
+ ],
+ "issue #5 - invalid sort order - Howie Schecter (naturalSort.insensitive = false)"
+ );
+ runTest(
+ [
+ "5D",
+ "1A",
+ "2D",
+ "33A",
+ "5E",
+ "33K",
+ "33D",
+ "5S",
+ "2C",
+ "5C",
+ "5F",
+ "1D",
+ "2M",
+ ],
+ [
+ "1A",
+ "1D",
+ "2C",
+ "2D",
+ "2M",
+ "5C",
+ "5D",
+ "5E",
+ "5F",
+ "5S",
+ "33A",
+ "33D",
+ "33K",
+ ],
+ "alphanumeric - number first"
+ );
+ runTest(
+ ["img 99", "img199", "imga99", "imgz99"],
+ ["img 99", "img199", "imga99", "imgz99"],
+ "issue #16 - Sorting incorrect when there is a space - adrien-be"
+ );
+ runTest(
+ ["img199", "img 99", "imga99", "imgz 99", "imgb99", "imgz199"],
+ ["img 99", "img199", "imga99", "imgb99", "imgz 99", "imgz199"],
+ "issue #16 - expanded test"
+ );
+ runTest(
+ ["1", "02", "3"],
+ ["1", "02", "3"],
+ "issue #18 - Any zeros that precede a number messes up the sorting - menixator"
+ );
+ // strings are coerced as floats/ints if possible and sorted accordingly - e.g. they are not chunked
+ runTest(
+ ["1.100", "1.1", "1.10", "1.54"],
+ ["1.100", "1.1", "1.10", "1.54"],
+ "issue #13 - ['1.100', '1.10', '1.1', '1.54'] etc do not sort properly... - rubenstolk"
+ );
+ runTest(
+ ["v1.100", "v1.1", "v1.10", "v1.54"],
+ ["v1.1", "v1.10", "v1.54", "v1.100"],
+ "issue #13 - ['v1.100', 'v1.10', 'v1.1', 'v1.54'] etc do not sort properly... - rubenstolk (bypass float coercion)"
+ );
+ runTest(
+ [
+ "MySnmp 1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 4234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 2234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 3234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ ],
+ [
+ "MySnmp 1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 2234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 3234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ "MySnmp 4234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567",
+ ],
+ "issue #14 - Very large numbers make sorting very slow - Mottie"
+ );
+ runTest(
+ ["bar.1-2", "bar.1"],
+ ["bar.1", "bar.1-2"],
+ "issue #21 - javascript error"
+ );
+ runTest(
+ ["SomeString", "SomeString 1"],
+ ["SomeString", "SomeString 1"],
+ "PR #19 - ['SomeString', 'SomeString 1'] bombing on 'undefined is not an object' - dannycochran"
+ );
+ runTest(
+ [
+ "Udet",
+ "\xDCbelacker",
+ "Uell",
+ "\xDClle",
+ "Ueve",
+ "\xDCxk\xFCll",
+ "Uffenbach",
+ ],
+ [
+ "\xDCbelacker",
+ "Udet",
+ "Uell",
+ "Ueve",
+ "Uffenbach",
+ "\xDClle",
+ "\xDCxk\xFCll",
+ ],
+ "issue #9 - Sorting umlauts characters \xC4, \xD6, \xDC - diogoalves"
+ );
+ runTest(
+ ["2.2 sec", "1.9 sec", "1.53 sec"],
+ ["1.53 sec", "1.9 sec", "2.2 sec"],
+ "https://github.com/overset/javascript-natural-sort/issues/13 - ['2.2 sec','1.9 sec','1.53 sec'] - padded by spaces - harisb"
+ );
+ runTest(
+ ["2.2sec", "1.9sec", "1.53sec"],
+ ["1.53sec", "1.9sec", "2.2sec"],
+ "https://github.com/overset/javascript-natural-sort/issues/13 - ['2.2sec','1.9sec','1.53sec'] - no padding - harisb"
+ );
+ });
+}
+
+function test(description, testFunc) {
+ info(description);
+ testFunc();
+}
+
+function runTest(testArray, expected, description, caseInsensitive = false) {
+ let actual = null;
+
+ if (caseInsensitive) {
+ actual = testArray.sort((a, b) =>
+ naturalSortCaseInsensitive(a, b, sessionString)
+ );
+ } else {
+ actual = testArray.sort((a, b) =>
+ naturalSortCaseSensitive(a, b, sessionString)
+ );
+ }
+
+ compareOptions(actual, expected, description);
+}
+
+// deepEqual() doesn't work well for testing arrays containing `undefined` so
+// we need to use a custom method.
+function compareOptions(actual, expected, description) {
+ let match = true;
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i] + "" !== expected[i] + "") {
+ ok(
+ false,
+ `${description}\nElement ${i} does not match:\n[${i}] ${actual[i]}\n[${i}] ${expected[i]}`
+ );
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ ok(true, description);
+ }
+}
diff --git a/devtools/shared/tests/xpcshell/test_pluralForm-english.js b/devtools/shared/tests/xpcshell/test_pluralForm-english.js
new file mode 100644
index 0000000000..2d2f29d47e
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_pluralForm-english.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";
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+
+function run_test() {
+ // English has 2 plural forms
+ Assert.equal(2, PluralForm.numForms());
+
+ // Make sure for good inputs, things work as expected
+ for (let num = 0; num <= 200; num++) {
+ Assert.equal(
+ num == 1 ? "word" : "words",
+ PluralForm.get(num, "word;words")
+ );
+ }
+
+ // Not having enough plural forms defaults to the first form
+ Assert.equal("word", PluralForm.get(2, "word"));
+
+ // Empty forms defaults to the first form
+ Assert.equal("word", PluralForm.get(2, "word;"));
+}
diff --git a/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js b/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js
new file mode 100644
index 0000000000..d9d1facca2
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.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";
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+
+function run_test() {
+ // Irish is plural rule #11
+ const [get, numForms] = PluralForm.makeGetter(11);
+
+ // Irish has 5 plural forms
+ Assert.equal(5, numForms());
+
+ // I don't really know Irish, so I'll stick in some dummy text
+ const words = "is 1;is 2;is 3-6;is 7-10;everything else";
+
+ const test = function (text, low, high) {
+ for (let num = low; num <= high; num++) {
+ Assert.equal(text, get(num, words));
+ }
+ };
+
+ // Make sure for good inputs, things work as expected
+ test("everything else", 0, 0);
+ test("is 1", 1, 1);
+ test("is 2", 2, 2);
+ test("is 3-6", 3, 6);
+ test("is 7-10", 7, 10);
+ test("everything else", 11, 200);
+}
diff --git a/devtools/shared/tests/xpcshell/test_prettifyCSS.js b/devtools/shared/tests/xpcshell/test_prettifyCSS.js
new file mode 100644
index 0000000000..1839c6a253
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_prettifyCSS.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test prettifyCSS.
+
+"use strict";
+
+const {
+ prettifyCSS,
+} = require("resource://devtools/shared/inspector/css-logic.js");
+
+const EXPAND_TAB = "devtools.editor.expandtab";
+const TAB_SIZE = "devtools.editor.tabsize";
+
+const TESTS_TAB_INDENT = [
+ {
+ name: "simple test. indent using tabs",
+ input: "div { font-family:'Arial Black', Arial, sans-serif; }",
+ expected: ["div {", "\tfont-family:'Arial Black', Arial, sans-serif;", "}"],
+ },
+
+ {
+ name: "whitespace before open brace. indent using tabs",
+ input: "div{}",
+ expected: ["div {", "}"],
+ },
+
+ {
+ name: "minified with trailing newline. indent using tabs",
+ input:
+ "\nbody{background:white;}div{font-size:4em;color:red}span{color:green;}\n",
+ expected: [
+ "body {",
+ "\tbackground:white;",
+ "}",
+ "div {",
+ "\tfont-size:4em;",
+ "\tcolor:red",
+ "}",
+ "span {",
+ "\tcolor:green;",
+ "}",
+ ],
+ },
+
+ {
+ name: "leading whitespace. indent using tabs",
+ input: "\n div{color: red;}",
+ expected: ["div {", "\tcolor: red;", "}"],
+ },
+
+ {
+ name: "CSS with extra closing brace. indent using tabs",
+ input: "body{margin:0}} div{color:red}",
+ expected: ["body {", "\tmargin:0", "}", "}", "div {", "\tcolor:red", "}"],
+ },
+];
+
+const TESTS_SPACE_INDENT = [
+ {
+ name: "simple test. indent using spaces",
+ input: "div { font-family:'Arial Black', Arial, sans-serif; }",
+ expected: [
+ "div {",
+ " font-family:'Arial Black', Arial, sans-serif;",
+ "}",
+ ],
+ },
+
+ {
+ name: "whitespace before open brace. indent using spaces",
+ input: "div{}",
+ expected: ["div {", "}"],
+ },
+
+ {
+ name: "minified with trailing newline. indent using spaces",
+ input:
+ "\nbody{background:white;}div{font-size:4em;color:red}span{color:green;}\n",
+ expected: [
+ "body {",
+ " background:white;",
+ "}",
+ "div {",
+ " font-size:4em;",
+ " color:red",
+ "}",
+ "span {",
+ " color:green;",
+ "}",
+ ],
+ },
+
+ {
+ name: "leading whitespace. indent using spaces",
+ input: "\n div{color: red;}",
+ expected: ["div {", " color: red;", "}"],
+ },
+
+ {
+ name: "CSS with extra closing brace. indent using spaces",
+ input: "body{margin:0}} div{color:red}",
+ expected: [
+ "body {",
+ " margin:0",
+ "}",
+ "}",
+ "div {",
+ " color:red",
+ "}",
+ ],
+ },
+
+ {
+ name: "HTML comments with some whitespace padding",
+ input: " \n\n\t <!--\n\n\t body {color:red} \n\n--> \t\n",
+ expected: ["body {", " color:red", "}"],
+ },
+
+ {
+ name: "HTML comments without whitespace padding",
+ input: "<!--body {color:red}-->",
+ expected: ["body {", " color:red", "}"],
+ },
+
+ {
+ name: "Breaking after commas in selectors",
+ input:
+ "@media screen, print {div, span, input {color: red;}}" +
+ "div, div, input, pre, table {color: blue;}",
+ expected: [
+ "@media screen, print {",
+ " div,",
+ " span,",
+ " input {",
+ " color: red;",
+ " }",
+ "}",
+ "div,",
+ "div,",
+ "input,",
+ "pre,",
+ "table {",
+ " color: blue;",
+ "}",
+ ],
+ },
+
+ {
+ name: "Multiline comment in CSS",
+ input: "/*\n * comment\n */\n#example{display:grid;}",
+ expected: [
+ "/*",
+ " * comment",
+ " */",
+ "#example {",
+ " display:grid;",
+ "}",
+ ],
+ },
+];
+
+function run_test() {
+ // Note that prettifyCSS.LINE_SEPARATOR is computed lazily, so we
+ // ensure it is set.
+ prettifyCSS("");
+
+ Services.prefs.setBoolPref(EXPAND_TAB, true);
+ Services.prefs.setIntPref(TAB_SIZE, 4);
+
+ for (const test of TESTS_SPACE_INDENT) {
+ info(test.name);
+
+ const input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR);
+ const { result: output } = prettifyCSS(input);
+ const expected =
+ test.expected.join(prettifyCSS.LINE_SEPARATOR) +
+ prettifyCSS.LINE_SEPARATOR;
+ equal(output, expected, test.name);
+ }
+
+ Services.prefs.setBoolPref(EXPAND_TAB, false);
+ for (const test of TESTS_TAB_INDENT) {
+ info(test.name);
+
+ const input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR);
+ const { result: output } = prettifyCSS(input);
+ const expected =
+ test.expected.join(prettifyCSS.LINE_SEPARATOR) +
+ prettifyCSS.LINE_SEPARATOR;
+ equal(output, expected, test.name);
+ }
+ Services.prefs.clearUserPref(EXPAND_TAB);
+ Services.prefs.clearUserPref(TAB_SIZE);
+}
diff --git a/devtools/shared/tests/xpcshell/test_require.js b/devtools/shared/tests/xpcshell/test_require.js
new file mode 100644
index 0000000000..b94aca23e7
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_require.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test require
+
+// Ensure that DevtoolsLoader.require doesn't spawn multiple
+// loader/modules when early cached
+function testBug1091706() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ const indent1 = require("resource://devtools/shared/indentation.js");
+ const indent2 = require("resource://devtools/shared/indentation.js");
+
+ Assert.ok(indent1 === indent2);
+}
+
+function testInvalidModule() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ try {
+ // This will result in an invalid URL with no scheme and mae loadSubScript
+ // throws "Error creating URI" error
+ require("foo");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(error.message, "Module `foo` is not found at foo.js");
+ Assert.ok(
+ error.stack.includes("testInvalidModule"),
+ "Exception's stack includes the test function"
+ );
+ }
+
+ try {
+ // But when using devtools prefix, the URL is going to be correct but the file
+ // doesn't exists, leading to "Error opening input stream (invalid filename?)" error
+ require("resource://devtools/foo.js");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(
+ error.message,
+ "Module `resource://devtools/foo.js` is not found at resource://devtools/foo.js"
+ );
+ Assert.ok(
+ error.stack.includes("testInvalidModule"),
+ "Exception's stack includes the test function"
+ );
+ }
+}
+
+function testThrowingModule() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ try {
+ // Require a test module that is throwing an Error object
+ require("xpcshell-test/throwing-module-1.js");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(error.message, "my-exception");
+ Assert.ok(
+ error.stack.includes("testThrowingModule"),
+ "Exception's stack includes the test function"
+ );
+ Assert.ok(
+ error.stack.includes("throwingMethod"),
+ "Exception's stack also includes the module function that throws"
+ );
+ }
+ try {
+ // Require a test module that is throwing a string
+ require("xpcshell-test/throwing-module-2.js");
+ Assert.ok(false, "require should throw");
+ } catch (error) {
+ Assert.equal(
+ error.message,
+ "Error while loading module `xpcshell-test/throwing-module-2.js` at " +
+ "resource://test/throwing-module-2.js:\nmy-exception"
+ );
+ Assert.ok(
+ error.stack.includes("testThrowingModule"),
+ "Exception's stack includes the test function"
+ );
+ Assert.ok(
+ !error.stack.includes("throwingMethod"),
+ "Exception's stack also includes the module function that throws"
+ );
+ }
+}
+
+function run_test() {
+ testBug1091706();
+
+ testInvalidModule();
+
+ testThrowingModule();
+}
diff --git a/devtools/shared/tests/xpcshell/test_require_lazy.js b/devtools/shared/tests/xpcshell/test_require_lazy.js
new file mode 100644
index 0000000000..deba7e2128
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_require_lazy.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { loader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+// Test devtools.lazyRequireGetter
+
+function run_test() {
+ const name = "asyncUtils";
+ const path = "devtools/shared/async-utils";
+ const o = {};
+ loader.lazyRequireGetter(o, name, path);
+ const asyncUtils = require(path);
+ // XXX: do_check_eq only works on primitive types, so we have this
+ // do_check_true of an equality expression.
+ Assert.ok(o.asyncUtils === asyncUtils);
+
+ // A non-main loader should get a new object via |lazyRequireGetter|, just
+ // as it would via a direct |require|.
+ const o2 = {};
+ const loader2 = new DevToolsLoader();
+
+ // We have to init the loader by loading any module before
+ // lazyRequireGetter is available
+ loader2.require("resource://devtools/shared/DevToolsUtils.js");
+
+ loader2.lazyRequireGetter(o2, name, path);
+ Assert.ok(o2.asyncUtils !== asyncUtils);
+
+ // A module required via a non-main loader that then uses |lazyRequireGetter|
+ // should also get the same object from that non-main loader.
+ const exposeLoader = loader2.require("xpcshell-test/exposeLoader");
+ const o3 = exposeLoader.exerciseLazyRequire(name, path);
+ Assert.ok(o3.asyncUtils === o2.asyncUtils);
+}
diff --git a/devtools/shared/tests/xpcshell/test_require_raw.js b/devtools/shared/tests/xpcshell/test_require_raw.js
new file mode 100644
index 0000000000..acd0e374b1
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_require_raw.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test require using "raw!".
+
+function run_test() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ const variableFileContents = require("raw!chrome://devtools/skin/variables.css");
+ ok(!!variableFileContents.length, "raw browserRequire worked");
+
+ const propertiesFileContents = require("raw!devtools/client/locales/shared.properties");
+ ok(
+ !!propertiesFileContents.length,
+ "unprefixed properties raw require worked"
+ );
+
+ const chromePropertiesFileContents = require("raw!chrome://devtools/locale/shared.properties");
+ ok(
+ !!chromePropertiesFileContents.length,
+ "prefixed properties raw require worked"
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_safeErrorString.js b/devtools/shared/tests/xpcshell/test_safeErrorString.js
new file mode 100644
index 0000000000..1d2e5431ed
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_safeErrorString.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test DevToolsUtils.safeErrorString
+
+function run_test() {
+ test_with_error();
+ test_with_tricky_error();
+ test_with_string();
+ test_with_thrower();
+ test_with_psychotic();
+}
+
+function test_with_error() {
+ const s = DevToolsUtils.safeErrorString(new Error("foo bar"));
+ // Got the message.
+ Assert.ok(s.includes("foo bar"));
+ // Got the stack.
+ Assert.ok(s.includes("test_with_error"));
+ Assert.ok(s.includes("test_safeErrorString.js"));
+ // Got the lineNumber and columnNumber.
+ Assert.ok(s.includes("Line"));
+ Assert.ok(s.includes("column"));
+}
+
+function test_with_tricky_error() {
+ const e = new Error("batman");
+ e.stack = { toString: Object.create(null) };
+ const s = DevToolsUtils.safeErrorString(e);
+ // Still got the message, despite a bad stack property.
+ Assert.ok(s.includes("batman"));
+}
+
+function test_with_string() {
+ const s = DevToolsUtils.safeErrorString("not really an error");
+ // Still get the message.
+ Assert.ok(s.includes("not really an error"));
+}
+
+function test_with_thrower() {
+ const s = DevToolsUtils.safeErrorString({
+ toString: () => {
+ throw new Error("Muahahaha");
+ },
+ });
+ // Still don't fail, get string back.
+ Assert.equal(typeof s, "string");
+}
+
+function test_with_psychotic() {
+ const s = DevToolsUtils.safeErrorString({
+ toString: () => Object.create(null),
+ });
+ // Still get a string out, and no exceptions thrown
+ Assert.equal(typeof s, "string");
+ Assert.equal(s, "[object Object]");
+}
diff --git a/devtools/shared/tests/xpcshell/test_sprintfjs.js b/devtools/shared/tests/xpcshell/test_sprintfjs.js
new file mode 100644
index 0000000000..29f7754896
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_sprintfjs.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";
+
+/**
+ * This unit test checks that our string formatter works with different patterns and
+ * arguments.
+ * Initially copied from unit tests at https://github.com/alexei/sprintf.js
+ */
+
+const { sprintf } = require("resource://devtools/shared/sprintfjs/sprintf.js");
+const PI = 3.141592653589793;
+
+function run_test() {
+ // Simple patterns
+ equal("%", sprintf("%%"));
+ equal("10", sprintf("%b", 2));
+ equal("A", sprintf("%c", 65));
+ equal("2", sprintf("%d", 2));
+ equal("2", sprintf("%i", 2));
+ equal("2", sprintf("%d", "2"));
+ equal("2", sprintf("%i", "2"));
+ equal('{"foo":"bar"}', sprintf("%j", { foo: "bar" }));
+ equal('["foo","bar"]', sprintf("%j", ["foo", "bar"]));
+ equal("2e+0", sprintf("%e", 2));
+ equal("2", sprintf("%u", 2));
+ equal("4294967294", sprintf("%u", -2));
+ equal("2.2", sprintf("%f", 2.2));
+ equal("3.141592653589793", sprintf("%g", PI));
+ equal("10", sprintf("%o", 8));
+ equal("%s", sprintf("%s", "%s"));
+ equal("ff", sprintf("%x", 255));
+ equal("FF", sprintf("%X", 255));
+ equal(
+ "Polly wants a cracker",
+ sprintf("%2$s %3$s a %1$s", "cracker", "Polly", "wants")
+ );
+ equal("Hello world!", sprintf("Hello %(who)s!", { who: "world" }));
+ equal("true", sprintf("%t", true));
+ equal("t", sprintf("%.1t", true));
+ equal("true", sprintf("%t", "true"));
+ equal("true", sprintf("%t", 1));
+ equal("false", sprintf("%t", false));
+ equal("f", sprintf("%.1t", false));
+ equal("false", sprintf("%t", ""));
+ equal("false", sprintf("%t", 0));
+
+ equal("undefined", sprintf("%T", undefined));
+ equal("null", sprintf("%T", null));
+ equal("boolean", sprintf("%T", true));
+ equal("number", sprintf("%T", 42));
+ equal("string", sprintf("%T", "This is a string"));
+ equal("function", sprintf("%T", Math.log));
+ equal("array", sprintf("%T", [1, 2, 3]));
+ equal("object", sprintf("%T", { foo: "bar" }));
+
+ equal("regexp", sprintf("%T", /<("[^"]*"|"[^"]*"|[^"">])*>/));
+
+ equal("true", sprintf("%v", true));
+ equal("42", sprintf("%v", 42));
+ equal("This is a string", sprintf("%v", "This is a string"));
+ equal("1,2,3", sprintf("%v", [1, 2, 3]));
+ equal("[object Object]", sprintf("%v", { foo: "bar" }));
+ equal(
+ "/<(\"[^\"]*\"|'[^']*'|[^'\">])*>/",
+ sprintf("%v", /<("[^"]*"|'[^']*'|[^'">])*>/)
+ );
+
+ // sign
+ equal("2", sprintf("%d", 2));
+ equal("-2", sprintf("%d", -2));
+ equal("+2", sprintf("%+d", 2));
+ equal("-2", sprintf("%+d", -2));
+ equal("2", sprintf("%i", 2));
+ equal("-2", sprintf("%i", -2));
+ equal("+2", sprintf("%+i", 2));
+ equal("-2", sprintf("%+i", -2));
+ equal("2.2", sprintf("%f", 2.2));
+ equal("-2.2", sprintf("%f", -2.2));
+ equal("+2.2", sprintf("%+f", 2.2));
+ equal("-2.2", sprintf("%+f", -2.2));
+ equal("-2.3", sprintf("%+.1f", -2.34));
+ equal("-0.0", sprintf("%+.1f", -0.01));
+ equal("3.14159", sprintf("%.6g", PI));
+ equal("3.14", sprintf("%.3g", PI));
+ equal("3", sprintf("%.1g", PI));
+ equal("-000000123", sprintf("%+010d", -123));
+ equal("______-123", sprintf("%+'_10d", -123));
+ equal("-234.34 123.2", sprintf("%f %f", -234.34, 123.2));
+
+ // padding
+ equal("-0002", sprintf("%05d", -2));
+ equal("-0002", sprintf("%05i", -2));
+ equal(" <", sprintf("%5s", "<"));
+ equal("0000<", sprintf("%05s", "<"));
+ equal("____<", sprintf("%'_5s", "<"));
+ equal("> ", sprintf("%-5s", ">"));
+ equal(">0000", sprintf("%0-5s", ">"));
+ equal(">____", sprintf("%'_-5s", ">"));
+ equal("xxxxxx", sprintf("%5s", "xxxxxx"));
+ equal("1234", sprintf("%02u", 1234));
+ equal(" -10.235", sprintf("%8.3f", -10.23456));
+ equal("-12.34 xxx", sprintf("%f %s", -12.34, "xxx"));
+ equal('{\n "foo": "bar"\n}', sprintf("%2j", { foo: "bar" }));
+ equal('[\n "foo",\n "bar"\n]', sprintf("%2j", ["foo", "bar"]));
+
+ // precision
+ equal("2.3", sprintf("%.1f", 2.345));
+ equal("xxxxx", sprintf("%5.5s", "xxxxxx"));
+ equal(" x", sprintf("%5.1s", "xxxxxx"));
+
+ equal(
+ "foobar",
+ sprintf("%s", function () {
+ return "foobar";
+ })
+ );
+}
diff --git a/devtools/shared/tests/xpcshell/test_stack.js b/devtools/shared/tests/xpcshell/test_stack.js
new file mode 100644
index 0000000000..a95d28c57d
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_stack.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test stack.js.
+
+function run_test() {
+ const loader = new DevToolsLoader();
+ const require = loader.require;
+
+ const {
+ StackFrameCache,
+ } = require("resource://devtools/server/actors/utils/stack.js");
+
+ const cache = new StackFrameCache();
+ cache.initFrames();
+ const baseFrame = {
+ line: 23,
+ column: 77,
+ source: "nowhere",
+ functionDisplayName: "nobody",
+ parent: null,
+ asyncParent: null,
+ asyncCause: null,
+ };
+ cache.addFrame(baseFrame);
+
+ let event = cache.makeEvent();
+ Assert.equal(event[0], null);
+ Assert.equal(event[1].functionDisplayName, "nobody");
+ Assert.equal(event.length, 2);
+
+ cache.addFrame({
+ line: 24,
+ column: 78,
+ source: "nowhere",
+ functionDisplayName: "still nobody",
+ parent: null,
+ asyncParent: baseFrame,
+ asyncCause: "async",
+ });
+
+ event = cache.makeEvent();
+ Assert.equal(event[0].functionDisplayName, "still nobody");
+ Assert.equal(event[0].parent, 0);
+ Assert.equal(event[0].asyncParent, 1);
+ Assert.equal(event.length, 1);
+}
diff --git a/devtools/shared/tests/xpcshell/throwing-module-1.js b/devtools/shared/tests/xpcshell/throwing-module-1.js
new file mode 100644
index 0000000000..cc7e159a76
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/throwing-module-1.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function throwingMethod() {
+ throw new Error("my-exception");
+}
+
+throwingMethod();
diff --git a/devtools/shared/tests/xpcshell/throwing-module-2.js b/devtools/shared/tests/xpcshell/throwing-module-2.js
new file mode 100644
index 0000000000..3e723844ec
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/throwing-module-2.js
@@ -0,0 +1,8 @@
+"use strict";
+
+function throwingMethod() {
+ // eslint-disable-next-line no-throw-literal
+ throw "my-exception";
+}
+
+throwingMethod();
diff --git a/devtools/shared/tests/xpcshell/xpcshell.toml b/devtools/shared/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..360f42a480
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,72 @@
+[DEFAULT]
+tags = "devtools"
+head = "head_devtools.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = [
+ "exposeLoader.js",
+ "throwing-module-1.js",
+ "throwing-module-2.js",
+]
+
+["test_assert.js"]
+
+["test_console_filtering.js"]
+
+["test_csslexer.js"]
+
+["test_debugger_client.js"]
+
+["test_defineLazyPrototypeGetter.js"]
+
+["test_eventemitter_abort_controller.js"]
+
+["test_eventemitter_basic.js"]
+
+["test_eventemitter_destroy.js"]
+
+["test_eventemitter_static.js"]
+
+["test_executeSoon.js"]
+
+["test_fetch-bom.js"]
+
+["test_fetch-chrome.js"]
+
+["test_fetch-file.js"]
+
+["test_fetch-http.js"]
+
+["test_fetch-resource.js"]
+
+["test_flatten.js"]
+
+["test_indentation.js"]
+
+["test_independent_loaders.js"]
+
+["test_invisible_loader.js"]
+
+["test_isSet.js"]
+
+["test_loader.js"]
+
+["test_natural-sort.js"]
+
+["test_pluralForm-english.js"]
+
+["test_pluralForm-makeGetter.js"]
+
+["test_prettifyCSS.js"]
+
+["test_require.js"]
+
+["test_require_lazy.js"]
+
+["test_require_raw.js"]
+
+["test_safeErrorString.js"]
+
+["test_sprintfjs.js"]
+
+["test_stack.js"]
diff --git a/devtools/shared/throttle.js b/devtools/shared/throttle.js
new file mode 100644
index 0000000000..85d0514f98
--- /dev/null
+++ b/devtools/shared/throttle.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";
+
+/**
+ * From underscore's `_.throttle`
+ * http://underscorejs.org
+ * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Underscore may be freely distributed under the MIT license.
+ *
+ * Returns a function, that, when invoked, will only be triggered at most once during a
+ * given window of time. The throttled function will run as much as it can, without ever
+ * going more than once per wait duration.
+ *
+ * @param {Function} func
+ * The function to throttle
+ * @param {number} wait
+ * The wait period
+ * @param {Object} scope
+ * The scope to use for func
+ * @return {Function} The throttled function
+ */
+function throttle(func, wait, scope) {
+ let args, result;
+ let timeout = null;
+ let previous = 0;
+
+ const later = function () {
+ previous = Date.now();
+ timeout = null;
+ result = func.apply(scope, args);
+ args = null;
+ };
+
+ const throttledFunction = function () {
+ const now = Date.now();
+ const remaining = wait - (now - previous);
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(scope, args);
+ args = null;
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+
+ function cancel() {
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ previous = 0;
+ args = undefined;
+ result = undefined;
+ }
+
+ function flush() {
+ if (!timeout) {
+ return result;
+ }
+ previous = 0;
+ return throttledFunction();
+ }
+
+ throttledFunction.cancel = cancel;
+ throttledFunction.flush = flush;
+
+ return throttledFunction;
+}
+
+exports.throttle = throttle;
diff --git a/devtools/shared/transport/child-transport.js b/devtools/shared/transport/child-transport.js
new file mode 100644
index 0000000000..52511432e5
--- /dev/null
+++ b/devtools/shared/transport/child-transport.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 flags = require("resource://devtools/shared/flags.js");
+
+/**
+ * A transport for the debugging protocol that uses nsIMessageManagers to
+ * exchange packets with servers running in child processes.
+ *
+ * In the parent process, |mm| should be the nsIMessageSender for the
+ * child process. In a child process, |mm| should be the child process
+ * message manager, which sends packets to the parent.
+ *
+ * |prefix| is a string included in the message names, to distinguish
+ * multiple servers running in the same child process.
+ *
+ * This transport exchanges messages named 'debug:<prefix>:packet', where
+ * <prefix> is |prefix|, whose data is the protocol packet.
+ */
+function ChildDebuggerTransport(mm, prefix) {
+ this._mm = mm;
+ this._messageName = "debug:" + prefix + ":packet";
+}
+
+/*
+ * To avoid confusion, we use 'message' to mean something that
+ * nsIMessageSender conveys, and 'packet' to mean a remote debugging
+ * protocol packet.
+ */
+ChildDebuggerTransport.prototype = {
+ constructor: ChildDebuggerTransport,
+
+ hooks: null,
+
+ _addListener() {
+ this._mm.addMessageListener(this._messageName, this);
+ },
+
+ _removeListener() {
+ try {
+ this._mm.removeMessageListener(this._messageName, this);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NULL_POINTER) {
+ throw e;
+ }
+ // In some cases, especially when using messageManagers in non-e10s mode, we reach
+ // this point with a dead messageManager which only throws errors but does not
+ // seem to indicate in any other way that it is dead.
+ }
+ },
+
+ ready() {
+ this._addListener();
+ },
+
+ close(options) {
+ this._removeListener();
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed(null, options);
+ }
+ },
+
+ receiveMessage({ data }) {
+ this.hooks.onPacket(data);
+ },
+
+ /**
+ * Helper method to ensure a given `object` can be sent across message manager
+ * without being serialized to JSON.
+ * See https://searchfox.org/mozilla-central/rev/6bfadf95b4a6aaa8bb3b2a166d6c3545983e179a/dom/base/nsFrameMessageManager.cpp#458-469
+ */
+ _canBeSerialized(object) {
+ try {
+ const holder = new StructuredCloneHolder(
+ "ChildDebuggerTransport._canBeSerialized",
+ null,
+ object
+ );
+ holder.deserialize(this);
+ } catch (e) {
+ return false;
+ }
+ return true;
+ },
+
+ pathToUnserializable(object) {
+ for (const key in object) {
+ const value = object[key];
+ if (!this._canBeSerialized(value)) {
+ if (typeof value == "object") {
+ return [key].concat(this.pathToUnserializable(value));
+ }
+ return [key];
+ }
+ }
+ return [];
+ },
+
+ send(packet) {
+ if (flags.testing && !this._canBeSerialized(packet)) {
+ const attributes = this.pathToUnserializable(packet);
+ let msg =
+ "Following packet can't be serialized: " + JSON.stringify(packet);
+ msg += "\nBecause of attributes: " + attributes.join(", ") + "\n";
+ msg += "Did you pass a function or an XPCOM object in it?";
+ throw new Error(msg);
+ }
+ try {
+ this._mm.sendAsyncMessage(this._messageName, packet);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NULL_POINTER) {
+ throw e;
+ }
+ // In some cases, especially when using messageManagers in non-e10s mode, we reach
+ // this point with a dead messageManager which only throws errors but does not
+ // seem to indicate in any other way that it is dead.
+ }
+ },
+
+ startBulkSend() {
+ throw new Error("Can't send bulk data to child processes.");
+ },
+};
+
+exports.ChildDebuggerTransport = ChildDebuggerTransport;
diff --git a/devtools/shared/transport/js-window-actor-transport.js b/devtools/shared/transport/js-window-actor-transport.js
new file mode 100644
index 0000000000..9f0fb82497
--- /dev/null
+++ b/devtools/shared/transport/js-window-actor-transport.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";
+
+/**
+ * DevTools transport relying on JS Window Actors. This is an experimental
+ * transport. It is only used when using the JS Window Actor based frame
+ * connector. In that case this transport will be used to communicate between
+ * the DevToolsServer living in the parent process and the DevToolsServer
+ * living in the process of the target frame.
+ *
+ * This is intended to be a replacement for child-transport.js which is a
+ * message-manager based transport.
+ */
+class JsWindowActorTransport {
+ constructor(jsWindowActor, prefix) {
+ this.hooks = null;
+ this._jsWindowActor = jsWindowActor;
+ this._prefix = prefix;
+
+ this._onPacketReceived = this._onPacketReceived.bind(this);
+ }
+
+ _addListener() {
+ this._jsWindowActor.on("packet-received", this._onPacketReceived);
+ }
+
+ _removeListener() {
+ this._jsWindowActor.off("packet-received", this._onPacketReceived);
+ }
+
+ ready() {
+ this._addListener();
+ }
+
+ /**
+ * @param {object} options
+ * @param {boolean} options.isModeSwitching
+ * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
+ */
+ close(options) {
+ this._removeListener();
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed(null, options);
+ }
+ }
+
+ _onPacketReceived(eventName, { data }) {
+ const { prefix, packet } = data;
+ if (prefix === this._prefix) {
+ this.hooks.onPacket(packet);
+ }
+ }
+
+ send(packet) {
+ this._jsWindowActor.sendPacket(packet, this._prefix);
+ }
+
+ startBulkSend() {
+ throw new Error("startBulkSend not implemented for JsWindowActorTransport");
+ }
+}
+
+exports.JsWindowActorTransport = JsWindowActorTransport;
diff --git a/devtools/shared/transport/local-transport.js b/devtools/shared/transport/local-transport.js
new file mode 100644
index 0000000000..bff83b7666
--- /dev/null
+++ b/devtools/shared/transport/local-transport.js
@@ -0,0 +1,204 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const { dumpn } = DevToolsUtils;
+const flags = require("resource://devtools/shared/flags.js");
+const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js");
+
+loader.lazyGetter(this, "Pipe", () => {
+ return Components.Constructor("@mozilla.org/pipe;1", "nsIPipe", "init");
+});
+
+/**
+ * An adapter that handles data transfers between the devtools client and
+ * server when they both run in the same process. It presents the same API as
+ * DebuggerTransport, but instead of transmitting serialized messages across a
+ * connection it merely calls the packet dispatcher of the other side.
+ *
+ * @param other LocalDebuggerTransport
+ * The other endpoint for this debugger connection.
+ *
+ * @see DebuggerTransport
+ */
+function LocalDebuggerTransport(other) {
+ this.other = other;
+ this.hooks = null;
+
+ // A packet number, shared between this and this.other. This isn't used by the
+ // protocol at all, but it makes the packet traces a lot easier to follow.
+ this._serial = this.other ? this.other._serial : { count: 0 };
+ this.close = this.close.bind(this);
+}
+
+LocalDebuggerTransport.prototype = {
+ /**
+ * Transmit a message by directly calling the onPacket handler of the other
+ * endpoint.
+ */
+ send(packet) {
+ const serial = this._serial.count++;
+ if (flags.wantLogging) {
+ // Check 'from' first, as 'echo' packets have both.
+ if (packet.from) {
+ dumpn("Packet " + serial + " sent from " + JSON.stringify(packet.from));
+ } else if (packet.to) {
+ dumpn("Packet " + serial + " sent to " + JSON.stringify(packet.to));
+ }
+ }
+ this._deepFreeze(packet);
+ const other = this.other;
+ if (other) {
+ DevToolsUtils.executeSoon(
+ DevToolsUtils.makeInfallible(() => {
+ // Avoid the cost of JSON.stringify() when logging is disabled.
+ if (flags.wantLogging) {
+ dumpn(
+ "Received packet " +
+ serial +
+ ": " +
+ JSON.stringify(packet, null, 2)
+ );
+ }
+ if (other.hooks) {
+ other.hooks.onPacket(packet);
+ }
+ }, "LocalDebuggerTransport instance's this.other.hooks.onPacket")
+ );
+ }
+ },
+
+ /**
+ * Send a streaming bulk packet directly to the onBulkPacket handler of the
+ * other endpoint.
+ *
+ * This case is much simpler than the full DebuggerTransport, since there is
+ * no primary stream we have to worry about managing while we hand it off to
+ * others temporarily. Instead, we can just make a single use pipe and be
+ * done with it.
+ */
+ startBulkSend({ actor, type, length }) {
+ const serial = this._serial.count++;
+
+ dumpn("Sent bulk packet " + serial + " for actor " + actor);
+ if (!this.other) {
+ const error = new Error("startBulkSend: other side of transport missing");
+ return Promise.reject(error);
+ }
+
+ const pipe = new Pipe(true, true, 0, 0, null);
+
+ DevToolsUtils.executeSoon(
+ DevToolsUtils.makeInfallible(() => {
+ dumpn("Received bulk packet " + serial);
+ if (!this.other.hooks) {
+ return;
+ }
+
+ // Receiver
+ new Promise(receiverResolve => {
+ const packet = {
+ actor,
+ type,
+ length,
+ copyTo: output => {
+ const copying = StreamUtils.copyStream(
+ pipe.inputStream,
+ output,
+ length
+ );
+ receiverResolve(copying);
+ return copying;
+ },
+ stream: pipe.inputStream,
+ done: receiverResolve,
+ };
+
+ this.other.hooks.onBulkPacket(packet);
+ })
+ // Await the result of reading from the stream
+ .then(() => pipe.inputStream.close(), this.close);
+ }, "LocalDebuggerTransport instance's this.other.hooks.onBulkPacket")
+ );
+
+ // Sender
+ return new Promise(senderResolve => {
+ // The remote transport is not capable of resolving immediately here, so we
+ // shouldn't be able to either.
+ DevToolsUtils.executeSoon(() => {
+ return (
+ new Promise(copyResolve => {
+ senderResolve({
+ copyFrom: input => {
+ const copying = StreamUtils.copyStream(
+ input,
+ pipe.outputStream,
+ length
+ );
+ copyResolve(copying);
+ return copying;
+ },
+ stream: pipe.outputStream,
+ done: copyResolve,
+ });
+ })
+ // Await the result of writing to the stream
+ .then(() => pipe.outputStream.close(), this.close)
+ );
+ });
+ });
+ },
+
+ /**
+ * Close the transport.
+ */
+ close() {
+ if (this.other) {
+ // Remove the reference to the other endpoint before calling close(), to
+ // avoid infinite recursion.
+ const other = this.other;
+ this.other = null;
+ other.close();
+ }
+ if (this.hooks) {
+ try {
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed();
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ this.hooks = null;
+ }
+ },
+
+ /**
+ * An empty method for emulating the DebuggerTransport API.
+ */
+ ready() {},
+
+ /**
+ * Helper function that makes an object fully immutable.
+ */
+ _deepFreeze(object) {
+ Object.freeze(object);
+ for (const prop in object) {
+ // Freeze the properties that are objects, not on the prototype, and not
+ // already frozen. Note that this might leave an unfrozen reference
+ // somewhere in the object if there is an already frozen object containing
+ // an unfrozen object.
+ if (
+ object.hasOwnProperty(prop) &&
+ typeof object === "object" &&
+ !Object.isFrozen(object)
+ ) {
+ this._deepFreeze(object[prop]);
+ }
+ }
+ },
+};
+
+exports.LocalDebuggerTransport = LocalDebuggerTransport;
diff --git a/devtools/shared/transport/moz.build b/devtools/shared/transport/moz.build
new file mode 100644
index 0000000000..8b1527d5ca
--- /dev/null
+++ b/devtools/shared/transport/moz.build
@@ -0,0 +1,18 @@
+# -*- 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+DevToolsModules(
+ "child-transport.js",
+ "js-window-actor-transport.js",
+ "local-transport.js",
+ "packets.js",
+ "stream-utils.js",
+ "transport.js",
+ "websocket-transport.js",
+ "worker-transport.js",
+)
diff --git a/devtools/shared/transport/packets.js b/devtools/shared/transport/packets.js
new file mode 100644
index 0000000000..9f9409a123
--- /dev/null
+++ b/devtools/shared/transport/packets.js
@@ -0,0 +1,440 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Packets contain read / write functionality for the different packet types
+ * supported by the debugging protocol, so that a transport can focus on
+ * delivery and queue management without worrying too much about the specific
+ * packet types.
+ *
+ * They are intended to be "one use only", so a new packet should be
+ * instantiated for each incoming or outgoing packet.
+ *
+ * A complete Packet type should expose at least the following:
+ * * read(stream, scriptableStream)
+ * Called when the input stream has data to read
+ * * write(stream)
+ * Called when the output stream is ready to write
+ * * get done()
+ * Returns true once the packet is done being read / written
+ * * destroy()
+ * Called to clean up at the end of use
+ */
+
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const { dumpn, dumpv } = DevToolsUtils;
+const flags = require("resource://devtools/shared/flags.js");
+const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js");
+
+DevToolsUtils.defineLazyGetter(this, "unicodeConverter", () => {
+ // eslint-disable-next-line no-shadow
+ const unicodeConverter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = "UTF-8";
+ return unicodeConverter;
+});
+
+// The transport's previous check ensured the header length did not exceed 20
+// characters. Here, we opt for the somewhat smaller, but still large limit of
+// 1 TiB.
+const PACKET_LENGTH_MAX = Math.pow(2, 40);
+
+/**
+ * A generic Packet processing object (extended by two subtypes below).
+ */
+function Packet(transport) {
+ this._transport = transport;
+ this._length = 0;
+}
+
+/**
+ * Attempt to initialize a new Packet based on the incoming packet header we've
+ * received so far. We try each of the types in succession, trying JSON packets
+ * first since they are much more common.
+ * @param header string
+ * The packet header string to attempt parsing.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @return Packet
+ * The parsed packet of the matching type, or null if no types matched.
+ */
+Packet.fromHeader = function (header, transport) {
+ return (
+ JSONPacket.fromHeader(header, transport) ||
+ BulkPacket.fromHeader(header, transport)
+ );
+};
+
+Packet.prototype = {
+ get length() {
+ return this._length;
+ },
+
+ set length(length) {
+ if (length > PACKET_LENGTH_MAX) {
+ throw Error(
+ "Packet length " +
+ length +
+ " exceeds the max length of " +
+ PACKET_LENGTH_MAX
+ );
+ }
+ this._length = length;
+ },
+
+ destroy() {
+ this._transport = null;
+ },
+};
+
+exports.Packet = Packet;
+
+/**
+ * With a JSON packet (the typical packet type sent via the transport), data is
+ * transferred as a JSON packet serialized into a string, with the string length
+ * prepended to the packet, followed by a colon ([length]:[packet]). The
+ * contents of the JSON packet are specified in the Remote Debugging Protocol
+ * specification.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ */
+function JSONPacket(transport) {
+ Packet.call(this, transport);
+ this._data = "";
+ this._done = false;
+}
+
+/**
+ * Attempt to initialize a new JSONPacket based on the incoming packet header
+ * we've received so far.
+ * @param header string
+ * The packet header string to attempt parsing.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @return JSONPacket
+ * The parsed packet, or null if it's not a match.
+ */
+JSONPacket.fromHeader = function (header, transport) {
+ const match = this.HEADER_PATTERN.exec(header);
+
+ if (!match) {
+ return null;
+ }
+
+ dumpv("Header matches JSON packet");
+ const packet = new JSONPacket(transport);
+ packet.length = +match[1];
+ return packet;
+};
+
+JSONPacket.HEADER_PATTERN = /^(\d+):$/;
+
+JSONPacket.prototype = Object.create(Packet.prototype);
+
+Object.defineProperty(JSONPacket.prototype, "object", {
+ /**
+ * Gets the object (not the serialized string) being read or written.
+ */
+ get() {
+ return this._object;
+ },
+
+ /**
+ * Sets the object to be sent when write() is called.
+ */
+ set(object) {
+ this._object = object;
+ const data = JSON.stringify(object);
+ this._data = unicodeConverter.ConvertFromUnicode(data);
+ this.length = this._data.length;
+ },
+});
+
+JSONPacket.prototype.read = function (stream, scriptableStream) {
+ dumpv("Reading JSON packet");
+
+ // Read in more packet data.
+ this._readData(stream, scriptableStream);
+
+ if (!this.done) {
+ // Don't have a complete packet yet.
+ return;
+ }
+
+ let json = this._data;
+ try {
+ json = unicodeConverter.ConvertToUnicode(json);
+ this._object = JSON.parse(json);
+ } catch (e) {
+ const msg =
+ "Error parsing incoming packet: " +
+ json +
+ " (" +
+ e +
+ " - " +
+ e.stack +
+ ")";
+ console.error(msg);
+ dumpn(msg);
+ return;
+ }
+
+ this._transport._onJSONObjectReady(this._object);
+};
+
+JSONPacket.prototype._readData = function (stream, scriptableStream) {
+ if (flags.wantVerbose) {
+ dumpv(
+ "Reading JSON data: _l: " +
+ this.length +
+ " dL: " +
+ this._data.length +
+ " sA: " +
+ stream.available()
+ );
+ }
+ const bytesToRead = Math.min(
+ this.length - this._data.length,
+ stream.available()
+ );
+ this._data += scriptableStream.readBytes(bytesToRead);
+ this._done = this._data.length === this.length;
+};
+
+JSONPacket.prototype.write = function (stream) {
+ dumpv("Writing JSON packet");
+
+ if (this._outgoing === undefined) {
+ // Format the serialized packet to a buffer
+ this._outgoing = this.length + ":" + this._data;
+ }
+
+ const written = stream.write(this._outgoing, this._outgoing.length);
+ this._outgoing = this._outgoing.slice(written);
+ this._done = !this._outgoing.length;
+};
+
+Object.defineProperty(JSONPacket.prototype, "done", {
+ get() {
+ return this._done;
+ },
+});
+
+JSONPacket.prototype.toString = function () {
+ return JSON.stringify(this._object, null, 2);
+};
+
+exports.JSONPacket = JSONPacket;
+
+/**
+ * With a bulk packet, data is transferred by temporarily handing over the
+ * transport's input or output stream to the application layer for writing data
+ * directly. This can be much faster for large data sets, and avoids various
+ * stages of copies and data duplication inherent in the JSON packet type. The
+ * bulk packet looks like:
+ *
+ * bulk [actor] [type] [length]:[data]
+ *
+ * The interpretation of the data portion depends on the kind of actor and the
+ * packet's type. See the Remote Debugging Protocol Stream Transport spec for
+ * more details.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ */
+function BulkPacket(transport) {
+ Packet.call(this, transport);
+ this._done = false;
+ let _resolve;
+ this._readyForWriting = new Promise(resolve => {
+ _resolve = resolve;
+ });
+ this._readyForWriting.resolve = _resolve;
+}
+
+/**
+ * Attempt to initialize a new BulkPacket based on the incoming packet header
+ * we've received so far.
+ * @param header string
+ * The packet header string to attempt parsing.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @return BulkPacket
+ * The parsed packet, or null if it's not a match.
+ */
+BulkPacket.fromHeader = function (header, transport) {
+ const match = this.HEADER_PATTERN.exec(header);
+
+ if (!match) {
+ return null;
+ }
+
+ dumpv("Header matches bulk packet");
+ const packet = new BulkPacket(transport);
+ packet.header = {
+ actor: match[1],
+ type: match[2],
+ length: +match[3],
+ };
+ return packet;
+};
+
+BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
+
+BulkPacket.prototype = Object.create(Packet.prototype);
+
+BulkPacket.prototype.read = function (stream) {
+ dumpv("Reading bulk packet, handing off input stream");
+
+ // Temporarily pause monitoring of the input stream
+ this._transport.pauseIncoming();
+
+ new Promise(resolve => {
+ this._transport._onBulkReadReady({
+ actor: this.actor,
+ type: this.type,
+ length: this.length,
+ copyTo: output => {
+ dumpv("CT length: " + this.length);
+ const copying = StreamUtils.copyStream(stream, output, this.length);
+ resolve(copying);
+ return copying;
+ },
+ stream,
+ done: resolve,
+ });
+ // Await the result of reading from the stream
+ }).then(() => {
+ dumpv("onReadDone called, ending bulk mode");
+ this._done = true;
+ this._transport.resumeIncoming();
+ }, this._transport.close);
+
+ // Ensure this is only done once
+ this.read = () => {
+ throw new Error("Tried to read() a BulkPacket's stream multiple times.");
+ };
+};
+
+BulkPacket.prototype.write = function (stream) {
+ dumpv("Writing bulk packet");
+
+ if (this._outgoingHeader === undefined) {
+ dumpv("Serializing bulk packet header");
+ // Format the serialized packet header to a buffer
+ this._outgoingHeader =
+ "bulk " + this.actor + " " + this.type + " " + this.length + ":";
+ }
+
+ // Write the header, or whatever's left of it to write.
+ if (this._outgoingHeader.length) {
+ dumpv("Writing bulk packet header");
+ const written = stream.write(
+ this._outgoingHeader,
+ this._outgoingHeader.length
+ );
+ this._outgoingHeader = this._outgoingHeader.slice(written);
+ return;
+ }
+
+ dumpv("Handing off output stream");
+
+ // Temporarily pause the monitoring of the output stream
+ this._transport.pauseOutgoing();
+
+ new Promise(resolve => {
+ this._readyForWriting.resolve({
+ copyFrom: input => {
+ dumpv("CF length: " + this.length);
+ const copying = StreamUtils.copyStream(input, stream, this.length);
+ resolve(copying);
+ return copying;
+ },
+ stream,
+ done: resolve,
+ });
+ // Await the result of writing to the stream
+ }).then(() => {
+ dumpv("onWriteDone called, ending bulk mode");
+ this._done = true;
+ this._transport.resumeOutgoing();
+ }, this._transport.close);
+
+ // Ensure this is only done once
+ this.write = () => {
+ throw new Error("Tried to write() a BulkPacket's stream multiple times.");
+ };
+};
+
+Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
+ get() {
+ return this._readyForWriting;
+ },
+});
+
+Object.defineProperty(BulkPacket.prototype, "header", {
+ get() {
+ return {
+ actor: this.actor,
+ type: this.type,
+ length: this.length,
+ };
+ },
+
+ set(header) {
+ this.actor = header.actor;
+ this.type = header.type;
+ this.length = header.length;
+ },
+});
+
+Object.defineProperty(BulkPacket.prototype, "done", {
+ get() {
+ return this._done;
+ },
+});
+
+BulkPacket.prototype.toString = function () {
+ return "Bulk: " + JSON.stringify(this.header, null, 2);
+};
+
+exports.BulkPacket = BulkPacket;
+
+/**
+ * RawPacket is used to test the transport's error handling of malformed
+ * packets, by writing data directly onto the stream.
+ * @param transport DebuggerTransport
+ * The transport instance that will own the packet.
+ * @param data string
+ * The raw string to send out onto the stream.
+ */
+function RawPacket(transport, data) {
+ Packet.call(this, transport);
+ this._data = data;
+ this.length = data.length;
+ this._done = false;
+}
+
+RawPacket.prototype = Object.create(Packet.prototype);
+
+RawPacket.prototype.read = function (stream) {
+ // This hasn't yet been needed for testing.
+ throw Error("Not implmented.");
+};
+
+RawPacket.prototype.write = function (stream) {
+ const written = stream.write(this._data, this._data.length);
+ this._data = this._data.slice(written);
+ this._done = !this._data.length;
+};
+
+Object.defineProperty(RawPacket.prototype, "done", {
+ get() {
+ return this._done;
+ },
+});
+
+exports.RawPacket = RawPacket;
diff --git a/devtools/shared/transport/stream-utils.js b/devtools/shared/transport/stream-utils.js
new file mode 100644
index 0000000000..a3efd1d2aa
--- /dev/null
+++ b/devtools/shared/transport/stream-utils.js
@@ -0,0 +1,254 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const { dumpv } = DevToolsUtils;
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+DevToolsUtils.defineLazyGetter(this, "IOUtil", () => {
+ return Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
+});
+
+DevToolsUtils.defineLazyGetter(this, "ScriptableInputStream", () => {
+ return Components.Constructor(
+ "@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream",
+ "init"
+ );
+});
+
+const BUFFER_SIZE = 0x8000;
+
+/**
+ * This helper function (and its companion object) are used by bulk senders and
+ * receivers to read and write data in and out of other streams. Functions that
+ * make use of this tool are passed to callers when it is time to read or write
+ * bulk data. It is highly recommended to use these copier functions instead of
+ * the stream directly because the copier enforces the agreed upon length.
+ * Since bulk mode reuses an existing stream, the sender and receiver must write
+ * and read exactly the agreed upon amount of data, or else the entire transport
+ * will be left in a invalid state. Additionally, other methods of stream
+ * copying (such as NetUtil.asyncCopy) close the streams involved, which would
+ * terminate the debugging transport, and so it is avoided here.
+ *
+ * Overall, this *works*, but clearly the optimal solution would be able to just
+ * use the streams directly. If it were possible to fully implement
+ * nsIInputStream / nsIOutputStream in JS, wrapper streams could be created to
+ * enforce the length and avoid closing, and consumers could use familiar stream
+ * utilities like NetUtil.asyncCopy.
+ *
+ * The function takes two async streams and copies a precise number of bytes
+ * from one to the other. Copying begins immediately, but may complete at some
+ * future time depending on data size. Use the returned promise to know when
+ * it's complete.
+ *
+ * @param input nsIAsyncInputStream
+ * The stream to copy from.
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @param length Integer
+ * The amount of data that needs to be copied.
+ * @return Promise
+ * The promise is resolved when copying completes or rejected if any
+ * (unexpected) errors occur.
+ */
+function copyStream(input, output, length) {
+ const copier = new StreamCopier(input, output, length);
+ return copier.copy();
+}
+
+function StreamCopier(input, output, length) {
+ EventEmitter.decorate(this);
+ this._id = StreamCopier._nextId++;
+ this.input = input;
+ // Save off the base output stream, since we know it's async as we've required
+ this.baseAsyncOutput = output;
+ if (IOUtil.outputStreamIsBuffered(output)) {
+ this.output = output;
+ } else {
+ this.output = Cc[
+ "@mozilla.org/network/buffered-output-stream;1"
+ ].createInstance(Ci.nsIBufferedOutputStream);
+ this.output.init(output, BUFFER_SIZE);
+ }
+ this._length = length;
+ this._amountLeft = length;
+ let _resolve;
+ let _reject;
+ this._deferred = new Promise((resolve, reject) => {
+ _resolve = resolve;
+ _reject = reject;
+ });
+ this._deferred.resolve = _resolve;
+ this._deferred.reject = _reject;
+
+ this._copy = this._copy.bind(this);
+ this._flush = this._flush.bind(this);
+ this._destroy = this._destroy.bind(this);
+
+ // Copy promise's then method up to this object.
+ // Allows the copier to offer a promise interface for the simple succeed or
+ // fail scenarios, but also emit events (due to the EventEmitter) for other
+ // states, like progress.
+ this.then = this._deferred.then.bind(this._deferred);
+ this.then(this._destroy, this._destroy);
+
+ // Stream ready callback starts as |_copy|, but may switch to |_flush| at end
+ // if flushing would block the output stream.
+ this._streamReadyCallback = this._copy;
+}
+StreamCopier._nextId = 0;
+
+StreamCopier.prototype = {
+ copy() {
+ // Dispatch to the next tick so that it's possible to attach a progress
+ // event listener, even for extremely fast copies (like when testing).
+ Services.tm.dispatchToMainThread(() => {
+ try {
+ this._copy();
+ } catch (e) {
+ this._deferred.reject(e);
+ }
+ });
+ return this;
+ },
+
+ _copy() {
+ const bytesAvailable = this.input.available();
+ const amountToCopy = Math.min(bytesAvailable, this._amountLeft);
+ this._debug("Trying to copy: " + amountToCopy);
+
+ let bytesCopied;
+ try {
+ bytesCopied = this.output.writeFrom(this.input, amountToCopy);
+ } catch (e) {
+ if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this._debug("Base stream would block, will retry");
+ this._debug("Waiting for output stream");
+ this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
+ return;
+ }
+ throw e;
+ }
+
+ this._amountLeft -= bytesCopied;
+ this._debug("Copied: " + bytesCopied + ", Left: " + this._amountLeft);
+ this._emitProgress();
+
+ if (this._amountLeft === 0) {
+ this._debug("Copy done!");
+ this._flush();
+ return;
+ }
+
+ this._debug("Waiting for input stream");
+ this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
+ },
+
+ _emitProgress() {
+ this.emit("progress", {
+ bytesSent: this._length - this._amountLeft,
+ totalBytes: this._length,
+ });
+ },
+
+ _flush() {
+ try {
+ this.output.flush();
+ } catch (e) {
+ if (
+ e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK ||
+ e.result == Cr.NS_ERROR_FAILURE
+ ) {
+ this._debug("Flush would block, will retry");
+ this._streamReadyCallback = this._flush;
+ this._debug("Waiting for output stream");
+ this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
+ return;
+ }
+ throw e;
+ }
+ this._deferred.resolve();
+ },
+
+ _destroy() {
+ this._destroy = null;
+ this._copy = null;
+ this._flush = null;
+ this.input = null;
+ this.output = null;
+ },
+
+ // nsIInputStreamCallback
+ onInputStreamReady() {
+ this._streamReadyCallback();
+ },
+
+ // nsIOutputStreamCallback
+ onOutputStreamReady() {
+ this._streamReadyCallback();
+ },
+
+ _debug(msg) {
+ // Prefix logs with the copier ID, which makes logs much easier to
+ // understand when several copiers are running simultaneously
+ dumpv("Copier: " + this._id + " " + msg);
+ },
+};
+
+/**
+ * Read from a stream, one byte at a time, up to the next |delimiter|
+ * character, but stopping if we've read |count| without finding it. Reading
+ * also terminates early if there are less than |count| bytes available on the
+ * stream. In that case, we only read as many bytes as the stream currently has
+ * to offer.
+ * TODO: This implementation could be removed if bug 984651 is fixed, which
+ * provides a native version of the same idea.
+ * @param stream nsIInputStream
+ * The input stream to read from.
+ * @param delimiter string
+ * The character we're trying to find.
+ * @param count integer
+ * The max number of characters to read while searching.
+ * @return string
+ * The data collected. If the delimiter was found, this string will
+ * end with it.
+ */
+function delimitedRead(stream, delimiter, count) {
+ dumpv(
+ "Starting delimited read for " + delimiter + " up to " + count + " bytes"
+ );
+
+ let scriptableStream;
+ if (stream instanceof Ci.nsIScriptableInputStream) {
+ scriptableStream = stream;
+ } else {
+ scriptableStream = new ScriptableInputStream(stream);
+ }
+
+ let data = "";
+
+ // Don't exceed what's available on the stream
+ count = Math.min(count, stream.available());
+
+ if (count <= 0) {
+ return data;
+ }
+
+ let char;
+ while (char !== delimiter && count > 0) {
+ char = scriptableStream.readBytes(1);
+ count--;
+ data += char;
+ }
+
+ return data;
+}
+
+module.exports = {
+ copyStream,
+ delimitedRead,
+};
diff --git a/devtools/shared/transport/tests/xpcshell/.eslintrc.js b/devtools/shared/transport/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/transport/tests/xpcshell/head_dbg.js b/devtools/shared/transport/tests/xpcshell/head_dbg.js
new file mode 100644
index 0000000000..18e235f17e
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/head_dbg.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* exported Cr, CC, NetUtil, errorCount, initTestDevToolsServer,
+ writeTestTempFile, socket_transport, local_transport, really_long
+*/
+
+var CC = Components.Constructor;
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+// We do not want to log packets by default, because in some tests,
+// we can be sending large amounts of data. The test harness has
+// trouble dealing with logging all the data, and we end up with
+// intermittent time outs (e.g. bug 775924).
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// Services.prefs.setBoolPref("devtools.debugger.log.verbose", true);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+
+const {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ SocketListener,
+} = require("resource://devtools/shared/security/socket.js");
+
+// Convert an nsIScriptError 'logLevel' value into an appropriate string.
+function scriptErrorLogLevel(message) {
+ switch (message.logLevel) {
+ case Ci.nsIConsoleMessage.info:
+ return "info";
+ case Ci.nsIConsoleMessage.warn:
+ return "warning";
+ default:
+ Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error);
+ return "error";
+ }
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+var errorCount = 0;
+var listener = {
+ observe(message) {
+ errorCount++;
+ let string = "";
+ try {
+ // If we've been given an nsIScriptError, then we can print out
+ // something nicely formatted, for tools like Emacs to pick up.
+ message.QueryInterface(Ci.nsIScriptError);
+ dump(
+ message.sourceName +
+ ":" +
+ message.lineNumber +
+ ": " +
+ scriptErrorLogLevel(message) +
+ ": " +
+ message.errorMessage +
+ "\n"
+ );
+ string = message.errorMessage;
+ } catch (x) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ string = message.message;
+ } catch (e) {
+ string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (DevToolsServer.xpcInspector.eventLoopNestLevel > 0) {
+ DevToolsServer.xpcInspector.exitNestedEventLoop();
+ }
+
+ do_throw("head_dbg.js got console message: " + string + "\n");
+ },
+};
+
+Services.console.registerListener(listener);
+
+/**
+ * Initialize the testing devtools server.
+ */
+function initTestDevToolsServer() {
+ ActorRegistry.registerModule("devtools/server/actors/thread", {
+ prefix: "script",
+ constructor: "ScriptActor",
+ type: { global: true, target: true },
+ });
+ const { createRootActor } = require("xpcshell-test/testactors");
+ DevToolsServer.setRootActor(createRootActor);
+ // Allow incoming connections.
+ DevToolsServer.init();
+ // Avoid the server from being destroyed when the last connection closes
+ DevToolsServer.keepAlive = true;
+}
+
+/**
+ * Wrapper around do_get_file to prefix files with the name of current test to
+ * avoid collisions when running in parallel.
+ */
+function getTestTempFile(fileName, allowMissing) {
+ let thisTest = _TEST_FILE.toString().replace(/\\/g, "/");
+ thisTest = thisTest.substring(thisTest.lastIndexOf("/") + 1);
+ thisTest = thisTest.replace(/\..*$/, "");
+ return do_get_file(fileName + "-" + thisTest, allowMissing);
+}
+
+function writeTestTempFile(fileName, content) {
+ const file = getTestTempFile(fileName, true);
+ const stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ stream.init(file, -1, -1, 0);
+ try {
+ do {
+ const numWritten = stream.write(content, content.length);
+ content = content.slice(numWritten);
+ } while (content.length);
+ } finally {
+ stream.close();
+ }
+}
+
+/** * Transport Factories ***/
+
+var socket_transport = async function () {
+ if (!DevToolsServer.listeningSockets) {
+ const AuthenticatorType = DevToolsServer.Authenticators.get("PROMPT");
+ const authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ const socketOptions = {
+ authenticator,
+ portOrPath: -1,
+ };
+ const debuggerListener = new SocketListener(DevToolsServer, socketOptions);
+ await debuggerListener.open();
+ }
+ const port = DevToolsServer._listeners[0].port;
+ info("DevTools server port is " + port);
+ return DevToolsClient.socketConnect({ host: "127.0.0.1", port });
+};
+
+function local_transport() {
+ return Promise.resolve(DevToolsServer.connectPipe());
+}
+
+/** * Sample Data ***/
+
+var gReallyLong;
+function really_long() {
+ if (gReallyLong) {
+ return gReallyLong;
+ }
+ let ret = "0123456789";
+ for (let i = 0; i < 18; i++) {
+ ret += ret;
+ }
+ gReallyLong = ret;
+ return ret;
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_bulk_error.js b/devtools/shared/transport/tests/xpcshell/test_bulk_error.js
new file mode 100644
index 0000000000..52cc826e51
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_bulk_error.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+ initTestDevToolsServer();
+ add_test_bulk_actor();
+
+ add_task(async function () {
+ await test_string_error(socket_transport, json_reply);
+ await test_string_error(local_transport, json_reply);
+ DevToolsServer.destroy();
+ });
+
+ run_next_test();
+}
+
+/** * Sample Bulk Actor ***/
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+class TestBulkActor extends Actor {
+ constructor(conn) {
+ super(conn);
+
+ this.typeName = "testBulk";
+ this.requestTypes = {
+ jsonReply: this.jsonReply,
+ };
+ }
+
+ jsonReply({ length, reader, reply, done }) {
+ Assert.equal(length, really_long().length);
+
+ return {
+ allDone: true,
+ };
+ }
+}
+
+function add_test_bulk_actor() {
+ ActorRegistry.addGlobalActor(
+ {
+ constructorName: "TestBulkActor",
+ constructorFun: TestBulkActor,
+ },
+ "testBulk"
+ );
+}
+
+/** * Tests ***/
+
+var test_string_error = async function (transportFactory, onReady) {
+ const transport = await transportFactory();
+
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ const response = await client.mainRoot.rootForm;
+
+ await onReady(client, response);
+ client.close();
+ transport.close();
+};
+
+/** * Reply Types ***/
+
+function json_reply(client, response) {
+ const reallyLong = really_long();
+
+ const request = client.startBulkRequest({
+ actor: response.testBulk,
+ type: "jsonReply",
+ length: reallyLong.length,
+ });
+
+ // Send bulk data to server
+ return new Promise(resolve => {
+ request.on("bulk-send-ready", ({ writer, done }) => {
+ const input = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ input.setData(reallyLong, reallyLong.length);
+ try {
+ writer.copyFrom(input, () => {
+ input.close();
+ done();
+ });
+ do_throw(new Error("Copying should fail, the stream is not async."));
+ } catch (e) {
+ Assert.ok(true);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_client_server_bulk.js b/devtools/shared/transport/tests/xpcshell/test_client_server_bulk.js
new file mode 100644
index 0000000000..22efdc6eba
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_client_server_bulk.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var Pipe = Components.Constructor("@mozilla.org/pipe;1", "nsIPipe", "init");
+
+function run_test() {
+ initTestDevToolsServer();
+ add_test_bulk_actor();
+
+ add_task(async function () {
+ await test_bulk_request_cs(socket_transport, "jsonReply", "json");
+ await test_bulk_request_cs(local_transport, "jsonReply", "json");
+ await test_bulk_request_cs(socket_transport, "bulkEcho", "bulk");
+ await test_bulk_request_cs(local_transport, "bulkEcho", "bulk");
+ await test_json_request_cs(socket_transport, "bulkReply", "bulk");
+ await test_json_request_cs(local_transport, "bulkReply", "bulk");
+ DevToolsServer.destroy();
+ });
+
+ run_next_test();
+}
+
+/** * Sample Bulk Actor ***/
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+class TestBulkActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "testBulk", methods: [] });
+
+ this.requestTypes = {
+ bulkEcho: this.bulkEcho,
+ bulkReply: this.bulkReply,
+ jsonReply: this.jsonReply,
+ };
+ }
+
+ bulkEcho({ actor, type, length, copyTo }) {
+ Assert.equal(length, really_long().length);
+ this.conn
+ .startBulkSend({
+ actor,
+ type,
+ length,
+ })
+ .then(({ copyFrom }) => {
+ // We'll just echo back the same thing
+ const pipe = new Pipe(true, true, 0, 0, null);
+ copyTo(pipe.outputStream).then(() => {
+ pipe.outputStream.close();
+ });
+ copyFrom(pipe.inputStream).then(() => {
+ pipe.inputStream.close();
+ });
+ });
+ }
+
+ bulkReply({ to, type }) {
+ this.conn
+ .startBulkSend({
+ actor: to,
+ type,
+ length: really_long().length,
+ })
+ .then(({ copyFrom }) => {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-input")),
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ copyFrom(input).then(() => {
+ input.close();
+ });
+ }
+ );
+ });
+ }
+
+ jsonReply({ length, copyTo }) {
+ Assert.equal(length, really_long().length);
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ outputFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ const output = FileUtils.openSafeFileOutputStream(outputFile);
+
+ return copyTo(output)
+ .then(() => {
+ FileUtils.closeSafeFileOutputStream(output);
+ return verify_files();
+ })
+ .then(() => {
+ return { allDone: true };
+ }, do_throw);
+ }
+}
+
+function add_test_bulk_actor() {
+ ActorRegistry.addGlobalActor(
+ {
+ constructorName: "TestBulkActor",
+ constructorFun: TestBulkActor,
+ },
+ "testBulk"
+ );
+}
+
+/** * Reply Handlers ***/
+
+var replyHandlers = {
+ json(request) {
+ // Receive JSON reply from server
+ return new Promise(resolve => {
+ request.on("json-reply", reply => {
+ Assert.ok(reply.allDone);
+ resolve();
+ });
+ });
+ },
+
+ bulk(request) {
+ // Receive bulk data reply from server
+ return new Promise(resolve => {
+ request.on("bulk-reply", ({ length, copyTo }) => {
+ Assert.equal(length, really_long().length);
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ outputFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ const output = FileUtils.openSafeFileOutputStream(outputFile);
+
+ copyTo(output).then(() => {
+ FileUtils.closeSafeFileOutputStream(output);
+ resolve(verify_files());
+ });
+ });
+ });
+ },
+};
+
+/** * Tests ***/
+
+var test_bulk_request_cs = async function (
+ transportFactory,
+ actorType,
+ replyType
+) {
+ // Ensure test files are not present from a failed run
+ cleanup_files();
+ writeTestTempFile("bulk-input", really_long());
+
+ let clientResolve;
+ const clientDeferred = new Promise(resolve => {
+ clientResolve = resolve;
+ });
+
+ let serverResolve;
+ const serverDeferred = new Promise(resolve => {
+ serverResolve = resolve;
+ });
+
+ let bulkCopyResolve;
+ const bulkCopyDeferred = new Promise(resolve => {
+ bulkCopyResolve = resolve;
+ });
+
+ const transport = await transportFactory();
+
+ const client = new DevToolsClient(transport);
+ client.connect().then(() => {
+ client.mainRoot.rootForm.then(clientResolve);
+ });
+
+ function bulkSendReadyCallback({ copyFrom }) {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-input")),
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ copyFrom(input).then(() => {
+ input.close();
+ bulkCopyResolve();
+ });
+ }
+ );
+ }
+
+ clientDeferred
+ .then(response => {
+ const request = client.startBulkRequest({
+ actor: response.testBulk,
+ type: actorType,
+ length: really_long().length,
+ });
+
+ // Send bulk data to server
+ request.on("bulk-send-ready", bulkSendReadyCallback);
+
+ // Set up reply handling for this type
+ replyHandlers[replyType](request).then(() => {
+ client.close();
+ transport.close();
+ });
+ })
+ .catch(do_throw);
+
+ DevToolsServer.on("connectionchange", type => {
+ if (type === "closed") {
+ serverResolve();
+ }
+ });
+
+ return Promise.all([clientDeferred, bulkCopyDeferred, serverDeferred]);
+};
+
+var test_json_request_cs = async function (
+ transportFactory,
+ actorType,
+ replyType
+) {
+ // Ensure test files are not present from a failed run
+ cleanup_files();
+ writeTestTempFile("bulk-input", really_long());
+
+ let clientResolve;
+ const clientDeferred = new Promise(resolve => {
+ clientResolve = resolve;
+ });
+
+ let serverResolve;
+ const serverDeferred = new Promise(resolve => {
+ serverResolve = resolve;
+ });
+
+ const transport = await transportFactory();
+
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ client.mainRoot.rootForm.then(clientResolve);
+
+ clientDeferred
+ .then(response => {
+ const request = client.request({
+ to: response.testBulk,
+ type: actorType,
+ });
+
+ // Set up reply handling for this type
+ replyHandlers[replyType](request).then(() => {
+ client.close();
+ transport.close();
+ });
+ })
+ .catch(do_throw);
+
+ DevToolsServer.on("connectionchange", type => {
+ if (type === "closed") {
+ serverResolve();
+ }
+ });
+
+ return Promise.all([clientDeferred, serverDeferred]);
+};
+
+/** * Test Utils ***/
+
+function verify_files() {
+ const reallyLong = really_long();
+
+ const inputFile = getTestTempFile("bulk-input");
+ const outputFile = getTestTempFile("bulk-output");
+
+ Assert.equal(inputFile.fileSize, reallyLong.length);
+ Assert.equal(outputFile.fileSize, reallyLong.length);
+
+ // Ensure output file contents actually match
+ return new Promise(resolve => {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-output")),
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ const outputData = NetUtil.readInputStreamToString(
+ input,
+ reallyLong.length
+ );
+ // Avoid do_check_eq here so we don't log the contents
+ Assert.ok(outputData === reallyLong);
+ input.close();
+ resolve();
+ }
+ );
+ }).then(cleanup_files);
+}
+
+function cleanup_files() {
+ const inputFile = getTestTempFile("bulk-input", true);
+ if (inputFile.exists()) {
+ inputFile.remove(false);
+ }
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ if (outputFile.exists()) {
+ outputFile.remove(false);
+ }
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_dbgsocket.js b/devtools/shared/transport/tests/xpcshell/test_dbgsocket.js
new file mode 100644
index 0000000000..535431aa38
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_dbgsocket.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* global structuredClone */
+
+var gPort;
+var gExtraListener;
+
+function run_test() {
+ info("Starting test at " + new Date().toTimeString());
+ initTestDevToolsServer();
+
+ add_task(test_socket_conn);
+ add_task(test_socket_shutdown);
+ add_test(test_pipe_conn);
+
+ run_next_test();
+}
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+class EchoTestActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "EchoTestActor", methods: [] });
+
+ this.requestTypes = {
+ echo: EchoTestActor.prototype.onEcho,
+ };
+ }
+
+ onEcho(request) {
+ /*
+ * Request packets are frozen. Copy request, so that
+ * DevToolsServerConnection.onPacket can attach a 'from' property.
+ */
+ return structuredClone(request);
+ }
+}
+
+async function test_socket_conn() {
+ Assert.equal(DevToolsServer.listeningSockets, 0);
+ const AuthenticatorType = DevToolsServer.Authenticators.get("PROMPT");
+ const authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ const socketOptions = {
+ authenticator,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ Assert.ok(listener);
+ listener.open();
+ Assert.equal(DevToolsServer.listeningSockets, 1);
+ gPort = DevToolsServer._listeners[0].port;
+ info("DevTools server port is " + gPort);
+ // Open a second, separate listener
+ gExtraListener = new SocketListener(DevToolsServer, socketOptions);
+ gExtraListener.open();
+ Assert.equal(DevToolsServer.listeningSockets, 2);
+ Assert.ok(!DevToolsServer.hasConnection());
+
+ info("Starting long and unicode tests at " + new Date().toTimeString());
+ // We can't use EventEmitter.once as this is the second argument we care about...
+ const onConnectionChange = new Promise(res => {
+ DevToolsServer.once("connectionchange", (type, conn) => res(conn));
+ });
+
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: gPort,
+ });
+ Assert.ok(DevToolsServer.hasConnection());
+ info("Wait for server connection");
+ const conn = await onConnectionChange;
+
+ // Register a custom actor to do echo requests
+ const actor = new EchoTestActor(conn);
+ actor.actorID = "echo-actor";
+ conn.addActor(actor);
+
+ // Assert that connection settings are available on transport object
+ const settings = transport.connectionSettings;
+ Assert.equal(settings.host, "127.0.0.1");
+ Assert.equal(settings.port, gPort);
+
+ const onDebuggerConnectionClosed = DevToolsServer.once("connectionchange");
+ const unicodeString = "(╯°□°)╯︵ ┻━┻";
+ await new Promise(resolve => {
+ transport.hooks = {
+ onPacket(packet) {
+ this.onPacket = function ({ unicode }) {
+ Assert.equal(unicode, unicodeString);
+ transport.close();
+ };
+ // Verify that things work correctly when bigger than the output
+ // transport buffers and when transporting unicode...
+ transport.send({
+ to: "echo-actor",
+ type: "echo",
+ reallylong: really_long(),
+ unicode: unicodeString,
+ });
+ Assert.equal(packet.from, "root");
+ },
+ onTransportClosed(status) {
+ resolve();
+ },
+ };
+ transport.ready();
+ });
+ const type = await onDebuggerConnectionClosed;
+ Assert.equal(type, "closed");
+ Assert.ok(!DevToolsServer.hasConnection());
+}
+
+async function test_socket_shutdown() {
+ Assert.equal(DevToolsServer.listeningSockets, 2);
+ gExtraListener.close();
+ Assert.equal(DevToolsServer.listeningSockets, 1);
+ Assert.ok(DevToolsServer.closeAllSocketListeners());
+ Assert.equal(DevToolsServer.listeningSockets, 0);
+ // Make sure closing the listener twice does nothing.
+ Assert.ok(!DevToolsServer.closeAllSocketListeners());
+ Assert.equal(DevToolsServer.listeningSockets, 0);
+
+ info("Connecting to a server socket at " + new Date().toTimeString());
+ try {
+ await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: gPort,
+ });
+ } catch (e) {
+ if (
+ e.result == Cr.NS_ERROR_CONNECTION_REFUSED ||
+ e.result == Cr.NS_ERROR_NET_TIMEOUT
+ ) {
+ // The connection should be refused here, but on slow or overloaded
+ // machines it may just time out.
+ Assert.ok(true);
+ return;
+ }
+ throw e;
+ }
+
+ // Shouldn't reach this, should never connect.
+ Assert.ok(false);
+}
+
+function test_pipe_conn() {
+ const transport = DevToolsServer.connectPipe();
+ transport.hooks = {
+ onPacket(packet) {
+ Assert.equal(packet.from, "root");
+ transport.close();
+ },
+ onTransportClosed(status) {
+ run_next_test();
+ },
+ };
+
+ transport.ready();
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_dbgsocket_connection_drop.js b/devtools/shared/transport/tests/xpcshell/test_dbgsocket_connection_drop.js
new file mode 100644
index 0000000000..e08c2380fb
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_dbgsocket_connection_drop.js
@@ -0,0 +1,86 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Bug 755412 - checks if the server drops the connection on an improperly
+ * framed packet, i.e. when the length header is invalid.
+ */
+"use strict";
+
+const {
+ RawPacket,
+} = require("resource://devtools/shared/transport/packets.js");
+
+function run_test() {
+ info("Starting test at " + new Date().toTimeString());
+ initTestDevToolsServer();
+
+ add_task(test_socket_conn_drops_after_invalid_header);
+ add_task(test_socket_conn_drops_after_invalid_header_2);
+ add_task(test_socket_conn_drops_after_too_large_length);
+ add_task(test_socket_conn_drops_after_too_long_header);
+ run_next_test();
+}
+
+function test_socket_conn_drops_after_invalid_header() {
+ return test_helper('fluff30:27:{"to":"root","type":"echo"}');
+}
+
+function test_socket_conn_drops_after_invalid_header_2() {
+ return test_helper('27asd:{"to":"root","type":"echo"}');
+}
+
+function test_socket_conn_drops_after_too_large_length() {
+ // Packet length is limited (semi-arbitrarily) to 1 TiB (2^40)
+ return test_helper("4305724038957487634549823475894325:");
+}
+
+function test_socket_conn_drops_after_too_long_header() {
+ // The packet header is currently limited to no more than 200 bytes
+ let rawPacket = "4305724038957487634549823475894325";
+ for (let i = 0; i < 8; i++) {
+ rawPacket += rawPacket;
+ }
+ return test_helper(rawPacket + ":");
+}
+
+var test_helper = async function (payload) {
+ const AuthenticatorType = DevToolsServer.Authenticators.get("PROMPT");
+ const authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ const socketOptions = {
+ authenticator,
+ portOrPath: -1,
+ };
+
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ listener.open();
+
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ });
+ return new Promise(resolve => {
+ transport.hooks = {
+ onPacket(packet) {
+ this.onPacket = function () {
+ do_throw(new Error("This connection should be dropped."));
+ transport.close();
+ };
+
+ // Inject the payload directly into the stream.
+ transport._outgoing.push(new RawPacket(transport, payload));
+ transport._flushOutgoing();
+ },
+ onTransportClosed(status) {
+ Assert.ok(true);
+ resolve();
+ },
+ };
+ transport.ready();
+ });
+};
diff --git a/devtools/shared/transport/tests/xpcshell/test_delimited_read.js b/devtools/shared/transport/tests/xpcshell/test_delimited_read.js
new file mode 100644
index 0000000000..fe94f81bbf
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_delimited_read.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js");
+
+const StringInputStream = Components.Constructor(
+ "@mozilla.org/io/string-input-stream;1",
+ "nsIStringInputStream",
+ "setData"
+);
+
+function run_test() {
+ add_task(async function () {
+ await test_delimited_read("0123:", "0123:");
+ await test_delimited_read("0123:4567:", "0123:");
+ await test_delimited_read("012345678901:", "0123456789");
+ await test_delimited_read("0123/0123", "0123/0123");
+ });
+
+ run_next_test();
+}
+
+/** * Tests ***/
+
+function test_delimited_read(input, expected) {
+ input = new StringInputStream(input, input.length);
+ const result = StreamUtils.delimitedRead(input, ":", 10);
+ Assert.equal(result, expected);
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_packet.js b/devtools/shared/transport/tests/xpcshell/test_packet.js
new file mode 100644
index 0000000000..459a7a8211
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_packet.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ JSONPacket,
+ BulkPacket,
+} = require("resource://devtools/shared/transport/packets.js");
+
+function run_test() {
+ add_test(test_packet_done);
+ run_next_test();
+}
+
+// Ensure done can be checked without getting an error
+function test_packet_done() {
+ const json = new JSONPacket();
+ Assert.ok(!json.done);
+
+ const bulk = new BulkPacket();
+ Assert.ok(!bulk.done);
+
+ run_next_test();
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_queue.js b/devtools/shared/transport/tests/xpcshell/test_queue.js
new file mode 100644
index 0000000000..603640e34d
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_queue.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test verifies that the transport's queue operates correctly when various
+ * packets are scheduled simultaneously.
+ */
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+function run_test() {
+ initTestDevToolsServer();
+
+ add_task(async function () {
+ await test_transport(socket_transport);
+ await test_transport(local_transport);
+ DevToolsServer.destroy();
+ });
+
+ run_next_test();
+}
+
+/** * Tests ***/
+
+var test_transport = async function (transportFactory) {
+ let clientResolve;
+ const clientDeferred = new Promise(resolve => {
+ clientResolve = resolve;
+ });
+
+ let serverResolve;
+ const serverDeferred = new Promise(resolve => {
+ serverResolve = resolve;
+ });
+
+ // Ensure test files are not present from a failed run
+ cleanup_files();
+ const reallyLong = really_long();
+ writeTestTempFile("bulk-input", reallyLong);
+
+ Assert.equal(Object.keys(DevToolsServer._connections).length, 0);
+
+ const transport = await transportFactory();
+
+ // Sending from client to server
+ function write_data({ copyFrom }) {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-input")),
+ loadUsingSystemPrincipal: true,
+ },
+ function (input, status) {
+ copyFrom(input).then(() => {
+ input.close();
+ });
+ }
+ );
+ }
+
+ // Receiving on server from client
+ function on_bulk_packet({ actor, type, length, copyTo }) {
+ Assert.equal(actor, "root");
+ Assert.equal(type, "file-stream");
+ Assert.equal(length, reallyLong.length);
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ outputFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ const output = FileUtils.openSafeFileOutputStream(outputFile);
+
+ copyTo(output)
+ .then(() => {
+ FileUtils.closeSafeFileOutputStream(output);
+ return verify();
+ })
+ .then(() => {
+ // It's now safe to close
+ transport.hooks.onTransportClosed = () => {
+ clientResolve();
+ };
+ transport.close();
+ });
+ }
+
+ // Client
+
+ function send_packets() {
+ // Specifically, we want to ensure that multiple send()s proceed without
+ // causing the transport to die.
+ transport.send({
+ actor: "root",
+ type: "explode",
+ });
+
+ transport
+ .startBulkSend({
+ actor: "root",
+ type: "file-stream",
+ length: reallyLong.length,
+ })
+ .then(write_data);
+ }
+
+ transport.hooks = {
+ onPacket(packet) {
+ if (packet.error) {
+ transport.hooks.onError(packet);
+ } else if (packet.applicationType) {
+ transport.hooks.onServerHello(packet);
+ } else {
+ do_throw("Unexpected server reply");
+ }
+ },
+
+ onServerHello(packet) {
+ // We've received the initial start up packet
+ Assert.equal(packet.from, "root");
+ Assert.equal(packet.applicationType, "xpcshell-tests");
+
+ // Server
+ Assert.equal(Object.keys(DevToolsServer._connections).length, 1);
+ info(Object.keys(DevToolsServer._connections));
+ for (const connId in DevToolsServer._connections) {
+ DevToolsServer._connections[connId].onBulkPacket = on_bulk_packet;
+ }
+
+ DevToolsServer.on("connectionchange", type => {
+ if (type === "closed") {
+ serverResolve();
+ }
+ });
+
+ send_packets();
+ },
+
+ onError(packet) {
+ // The explode actor doesn't exist
+ Assert.equal(packet.from, "root");
+ Assert.equal(packet.error, "noSuchActor");
+ },
+
+ onTransportClosed() {
+ do_throw("Transport closed before we expected");
+ },
+ };
+
+ transport.ready();
+
+ return Promise.all([clientDeferred, serverDeferred]);
+};
+
+/** * Test Utils ***/
+
+function verify() {
+ const reallyLong = really_long();
+
+ const inputFile = getTestTempFile("bulk-input");
+ const outputFile = getTestTempFile("bulk-output");
+
+ Assert.equal(inputFile.fileSize, reallyLong.length);
+ Assert.equal(outputFile.fileSize, reallyLong.length);
+
+ // Ensure output file contents actually match
+ return new Promise(resolve => {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-output")),
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ const outputData = NetUtil.readInputStreamToString(
+ input,
+ reallyLong.length
+ );
+ // Avoid do_check_eq here so we don't log the contents
+ Assert.ok(outputData === reallyLong);
+ input.close();
+ resolve();
+ }
+ );
+ }).then(cleanup_files);
+}
+
+function cleanup_files() {
+ const inputFile = getTestTempFile("bulk-input", true);
+ if (inputFile.exists()) {
+ inputFile.remove(false);
+ }
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ if (outputFile.exists()) {
+ outputFile.remove(false);
+ }
+}
diff --git a/devtools/shared/transport/tests/xpcshell/test_transport_bulk.js b/devtools/shared/transport/tests/xpcshell/test_transport_bulk.js
new file mode 100644
index 0000000000..137cbd2679
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/test_transport_bulk.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+function run_test() {
+ initTestDevToolsServer();
+
+ add_task(async function () {
+ await test_bulk_transfer_transport(socket_transport);
+ await test_bulk_transfer_transport(local_transport);
+ DevToolsServer.destroy();
+ });
+
+ run_next_test();
+}
+
+/** * Tests ***/
+
+/**
+ * This tests a one-way bulk transfer at the transport layer.
+ */
+var test_bulk_transfer_transport = async function (transportFactory) {
+ info("Starting bulk transfer test at " + new Date().toTimeString());
+
+ let clientResolve;
+ const clientDeferred = new Promise(resolve => {
+ clientResolve = resolve;
+ });
+
+ let serverResolve;
+ const serverDeferred = new Promise(resolve => {
+ serverResolve = resolve;
+ });
+
+ // Ensure test files are not present from a failed run
+ cleanup_files();
+ const reallyLong = really_long();
+ writeTestTempFile("bulk-input", reallyLong);
+
+ Assert.equal(Object.keys(DevToolsServer._connections).length, 0);
+
+ const transport = await transportFactory();
+
+ // Sending from client to server
+ function write_data({ copyFrom }) {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-input")),
+ loadUsingSystemPrincipal: true,
+ },
+ function (input, status) {
+ copyFrom(input).then(() => {
+ input.close();
+ });
+ }
+ );
+ }
+
+ // Receiving on server from client
+ function on_bulk_packet({ actor, type, length, copyTo }) {
+ Assert.equal(actor, "root");
+ Assert.equal(type, "file-stream");
+ Assert.equal(length, reallyLong.length);
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ outputFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ const output = FileUtils.openSafeFileOutputStream(outputFile);
+
+ copyTo(output)
+ .then(() => {
+ FileUtils.closeSafeFileOutputStream(output);
+ return verify();
+ })
+ .then(() => {
+ // It's now safe to close
+ transport.hooks.onTransportClosed = () => {
+ clientResolve();
+ };
+ transport.close();
+ });
+ }
+
+ // Client
+ transport.hooks = {
+ onPacket(packet) {
+ // We've received the initial start up packet
+ Assert.equal(packet.from, "root");
+
+ // Server
+ Assert.equal(Object.keys(DevToolsServer._connections).length, 1);
+ info(Object.keys(DevToolsServer._connections));
+ for (const connId in DevToolsServer._connections) {
+ DevToolsServer._connections[connId].onBulkPacket = on_bulk_packet;
+ }
+
+ DevToolsServer.on("connectionchange", type => {
+ if (type === "closed") {
+ serverResolve();
+ }
+ });
+
+ transport
+ .startBulkSend({
+ actor: "root",
+ type: "file-stream",
+ length: reallyLong.length,
+ })
+ .then(write_data);
+ },
+
+ onTransportClosed() {
+ do_throw("Transport closed before we expected");
+ },
+ };
+
+ transport.ready();
+
+ return Promise.all([clientDeferred, serverDeferred]);
+};
+
+/** * Test Utils ***/
+
+function verify() {
+ const reallyLong = really_long();
+
+ const inputFile = getTestTempFile("bulk-input");
+ const outputFile = getTestTempFile("bulk-output");
+
+ Assert.equal(inputFile.fileSize, reallyLong.length);
+ Assert.equal(outputFile.fileSize, reallyLong.length);
+
+ // Ensure output file contents actually match
+ return new Promise(resolve => {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(getTestTempFile("bulk-output")),
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ const outputData = NetUtil.readInputStreamToString(
+ input,
+ reallyLong.length
+ );
+ // Avoid do_check_eq here so we don't log the contents
+ Assert.ok(outputData === reallyLong);
+ input.close();
+ resolve();
+ }
+ );
+ }).then(cleanup_files);
+}
+
+function cleanup_files() {
+ const inputFile = getTestTempFile("bulk-input", true);
+ if (inputFile.exists()) {
+ inputFile.remove(false);
+ }
+
+ const outputFile = getTestTempFile("bulk-output", true);
+ if (outputFile.exists()) {
+ outputFile.remove(false);
+ }
+}
diff --git a/devtools/shared/transport/tests/xpcshell/testactors.js b/devtools/shared/transport/tests/xpcshell/testactors.js
new file mode 100644
index 0000000000..0a35f05287
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/testactors.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { RootActor } = require("resource://devtools/server/actors/root.js");
+const {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+
+exports.createRootActor = function createRootActor(connection) {
+ const root = new RootActor(connection, {
+ globalActorFactories: ActorRegistry.globalActorFactories,
+ });
+ root.applicationType = "xpcshell-tests";
+ return root;
+};
diff --git a/devtools/shared/transport/tests/xpcshell/xpcshell.toml b/devtools/shared/transport/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..38e49ab71c
--- /dev/null
+++ b/devtools/shared/transport/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = "devtools"
+head = "head_dbg.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = ["testactors.js"]
+
+["test_bulk_error.js"]
+
+["test_client_server_bulk.js"]
+
+["test_dbgsocket.js"]
+
+["test_dbgsocket_connection_drop.js"]
+
+["test_delimited_read.js"]
+
+["test_packet.js"]
+
+["test_queue.js"]
+
+["test_transport_bulk.js"]
diff --git a/devtools/shared/transport/transport.js b/devtools/shared/transport/transport.js
new file mode 100644
index 0000000000..460f45206f
--- /dev/null
+++ b/devtools/shared/transport/transport.js
@@ -0,0 +1,499 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const { dumpn, dumpv } = DevToolsUtils;
+const flags = require("resource://devtools/shared/flags.js");
+const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js");
+const {
+ Packet,
+ JSONPacket,
+ BulkPacket,
+} = require("resource://devtools/shared/transport/packets.js");
+
+loader.lazyGetter(this, "ScriptableInputStream", () => {
+ return Components.Constructor(
+ "@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream",
+ "init"
+ );
+});
+
+const PACKET_HEADER_MAX = 200;
+
+/**
+ * An adapter that handles data transfers between the devtools client and
+ * server. It can work with both nsIPipe and nsIServerSocket transports so
+ * long as the properly created input and output streams are specified.
+ * (However, for intra-process connections, LocalDebuggerTransport, below,
+ * is more efficient than using an nsIPipe pair with DebuggerTransport.)
+ *
+ * @param input nsIAsyncInputStream
+ * The input stream.
+ * @param output nsIAsyncOutputStream
+ * The output stream.
+ *
+ * Given a DebuggerTransport instance dt:
+ * 1) Set dt.hooks to a packet handler object (described below).
+ * 2) Call dt.ready() to begin watching for input packets.
+ * 3) Call dt.send() / dt.startBulkSend() to send packets.
+ * 4) Call dt.close() to close the connection, and disengage from the event
+ * loop.
+ *
+ * A packet handler is an object with the following methods:
+ *
+ * - onPacket(packet) - called when we have received a complete packet.
+ * |packet| is the parsed form of the packet --- a JavaScript value, not
+ * a JSON-syntax string.
+ *
+ * - onBulkPacket(packet) - called when we have switched to bulk packet
+ * receiving mode. |packet| is an object containing:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you can ensure
+ * that you will read exactly |length| bytes and will not close the
+ * stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo| below), you
+ * must signal completion by resolving / rejecting this deferred.
+ * If it's rejected, the transport will be closed. If an Error is
+ * supplied as a rejection value, it will be logged via |dumpn|.
+ * If you do use |copyTo|, resolving is taken care of for you when
+ * copying completes.
+ * * copyTo: A helper function for getting your data out of the stream that
+ * meets the stream handling requirements above, and has the
+ * following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or rejected if any
+ * (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk that is
+ * copied. See stream-utils.js.
+ *
+ * - onTransportClosed(reason) - called when the connection is closed. |reason| is
+ * an optional nsresult or object, typically passed when the transport is
+ * closed due to some error in a underlying stream.
+ *
+ * See ./packets.js and the Remote Debugging Protocol specification for more
+ * details on the format of these packets.
+ */
+function DebuggerTransport(input, output) {
+ this._input = input;
+ this._scriptableInput = new ScriptableInputStream(input);
+ this._output = output;
+
+ // The current incoming (possibly partial) header, which will determine which
+ // type of Packet |_incoming| below will become.
+ this._incomingHeader = "";
+ // The current incoming Packet object
+ this._incoming = null;
+ // A queue of outgoing Packet objects
+ this._outgoing = [];
+
+ this.hooks = null;
+ this.active = false;
+
+ this._incomingEnabled = true;
+ this._outgoingEnabled = true;
+
+ this.close = this.close.bind(this);
+}
+
+DebuggerTransport.prototype = {
+ /**
+ * Transmit an object as a JSON packet.
+ *
+ * This method returns immediately, without waiting for the entire
+ * packet to be transmitted, registering event handlers as needed to
+ * transmit the entire packet. Packets are transmitted in the order
+ * they are passed to this method.
+ */
+ send(object) {
+ const packet = new JSONPacket(this);
+ packet.object = object;
+ this._outgoing.push(packet);
+ this._flushOutgoing();
+ },
+
+ /**
+ * Transmit streaming data via a bulk packet.
+ *
+ * This method initiates the bulk send process by queuing up the header data.
+ * The caller receives eventual access to a stream for writing.
+ *
+ * N.B.: Do *not* attempt to close the stream handed to you, as it will
+ * continue to be used by this transport afterwards. Most users should
+ * instead use the provided |copyFrom| function instead.
+ *
+ * @param header Object
+ * This is modeled after the format of JSON packets above, but does not
+ * actually contain the data, but is instead just a routing header:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be sent
+ * @return Promise
+ * The promise will be resolved when you are allowed to write to the
+ * stream with an object containing:
+ * * stream: This output stream should only be used directly if
+ * you can ensure that you will write exactly |length|
+ * bytes and will not close the stream when writing is
+ * complete
+ * * done: If you use the stream directly (instead of |copyFrom|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as
+ * a rejection value, it will be logged via |dumpn|. If
+ * you do use |copyFrom|, resolving is taken care of for
+ * you when copying completes.
+ * * copyFrom: A helper function for getting your data onto the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param input nsIAsyncInputStream
+ * The stream to copy from.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ startBulkSend(header) {
+ const packet = new BulkPacket(this);
+ packet.header = header;
+ this._outgoing.push(packet);
+ this._flushOutgoing();
+ return packet.streamReadyForWriting;
+ },
+
+ /**
+ * Close the transport.
+ * @param reason nsresult / object (optional)
+ * The status code or error message that corresponds to the reason for
+ * closing the transport (likely because a stream closed or failed).
+ */
+ close(reason) {
+ this.active = false;
+ this._input.close();
+ this._scriptableInput.close();
+ this._output.close();
+ this._destroyIncoming();
+ this._destroyAllOutgoing();
+ if (this.hooks) {
+ this.hooks.onTransportClosed(reason);
+ this.hooks = null;
+ }
+ if (reason) {
+ dumpn("Transport closed: " + DevToolsUtils.safeErrorString(reason));
+ } else {
+ dumpn("Transport closed.");
+ }
+ },
+
+ /**
+ * The currently outgoing packet (at the top of the queue).
+ */
+ get _currentOutgoing() {
+ return this._outgoing[0];
+ },
+
+ /**
+ * Flush data to the outgoing stream. Waits until the output stream notifies
+ * us that it is ready to be written to (via onOutputStreamReady).
+ */
+ _flushOutgoing() {
+ if (!this._outgoingEnabled || this._outgoing.length === 0) {
+ return;
+ }
+
+ // If the top of the packet queue has nothing more to send, remove it.
+ if (this._currentOutgoing.done) {
+ this._finishCurrentOutgoing();
+ }
+
+ if (this._outgoing.length) {
+ const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+ this._output.asyncWait(this, 0, 0, threadManager.currentThread);
+ }
+ },
+
+ /**
+ * Pause this transport's attempts to write to the output stream. This is
+ * used when we've temporarily handed off our output stream for writing bulk
+ * data.
+ */
+ pauseOutgoing() {
+ this._outgoingEnabled = false;
+ },
+
+ /**
+ * Resume this transport's attempts to write to the output stream.
+ */
+ resumeOutgoing() {
+ this._outgoingEnabled = true;
+ this._flushOutgoing();
+ },
+
+ // nsIOutputStreamCallback
+ /**
+ * This is called when the output stream is ready for more data to be written.
+ * The current outgoing packet will attempt to write some amount of data, but
+ * may not complete.
+ */
+ onOutputStreamReady: DevToolsUtils.makeInfallible(function (stream) {
+ if (!this._outgoingEnabled || this._outgoing.length === 0) {
+ return;
+ }
+
+ try {
+ this._currentOutgoing.write(stream);
+ } catch (e) {
+ if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this.close(e.result);
+ return;
+ }
+ throw e;
+ }
+
+ this._flushOutgoing();
+ }, "DebuggerTransport.prototype.onOutputStreamReady"),
+
+ /**
+ * Remove the current outgoing packet from the queue upon completion.
+ */
+ _finishCurrentOutgoing() {
+ if (this._currentOutgoing) {
+ this._currentOutgoing.destroy();
+ this._outgoing.shift();
+ }
+ },
+
+ /**
+ * Clear the entire outgoing queue.
+ */
+ _destroyAllOutgoing() {
+ for (const packet of this._outgoing) {
+ packet.destroy();
+ }
+ this._outgoing = [];
+ },
+
+ /**
+ * Initialize the input stream for reading. Once this method has been called,
+ * we watch for packets on the input stream, and pass them to the appropriate
+ * handlers via this.hooks.
+ */
+ ready() {
+ this.active = true;
+ this._waitForIncoming();
+ },
+
+ /**
+ * Asks the input stream to notify us (via onInputStreamReady) when it is
+ * ready for reading.
+ */
+ _waitForIncoming() {
+ if (this._incomingEnabled) {
+ const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+ this._input.asyncWait(this, 0, 0, threadManager.currentThread);
+ }
+ },
+
+ /**
+ * Pause this transport's attempts to read from the input stream. This is
+ * used when we've temporarily handed off our input stream for reading bulk
+ * data.
+ */
+ pauseIncoming() {
+ this._incomingEnabled = false;
+ },
+
+ /**
+ * Resume this transport's attempts to read from the input stream.
+ */
+ resumeIncoming() {
+ this._incomingEnabled = true;
+ this._flushIncoming();
+ this._waitForIncoming();
+ },
+
+ // nsIInputStreamCallback
+ /**
+ * Called when the stream is either readable or closed.
+ */
+ onInputStreamReady: DevToolsUtils.makeInfallible(function (stream) {
+ try {
+ while (
+ stream.available() &&
+ this._incomingEnabled &&
+ this._processIncoming(stream, stream.available())
+ ) {
+ // Loop until there is nothing more to process
+ }
+ this._waitForIncoming();
+ } catch (e) {
+ if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this.close(e.result);
+ } else {
+ throw e;
+ }
+ }
+ }, "DebuggerTransport.prototype.onInputStreamReady"),
+
+ /**
+ * Process the incoming data. Will create a new currently incoming Packet if
+ * needed. Tells the incoming Packet to read as much data as it can, but
+ * reading may not complete. The Packet signals that its data is ready for
+ * delivery by calling one of this transport's _on*Ready methods (see
+ * ./packets.js and the _on*Ready methods below).
+ * @return boolean
+ * Whether incoming stream processing should continue for any
+ * remaining data.
+ */
+ _processIncoming(stream, count) {
+ dumpv("Data available: " + count);
+
+ if (!count) {
+ dumpv("Nothing to read, skipping");
+ return false;
+ }
+
+ try {
+ if (!this._incoming) {
+ dumpv("Creating a new packet from incoming");
+
+ if (!this._readHeader(stream)) {
+ // Not enough data to read packet type
+ return false;
+ }
+
+ // Attempt to create a new Packet by trying to parse each possible
+ // header pattern.
+ this._incoming = Packet.fromHeader(this._incomingHeader, this);
+ if (!this._incoming) {
+ throw new Error(
+ "No packet types for header: " + this._incomingHeader
+ );
+ }
+ }
+
+ if (!this._incoming.done) {
+ // We have an incomplete packet, keep reading it.
+ dumpv("Existing packet incomplete, keep reading");
+ this._incoming.read(stream, this._scriptableInput);
+ }
+ } catch (e) {
+ const msg =
+ "Error reading incoming packet: (" + e + " - " + e.stack + ")";
+ dumpn(msg);
+
+ // Now in an invalid state, shut down the transport.
+ this.close();
+ return false;
+ }
+
+ if (!this._incoming.done) {
+ // Still not complete, we'll wait for more data.
+ dumpv("Packet not done, wait for more");
+ return true;
+ }
+
+ // Ready for next packet
+ this._flushIncoming();
+ return true;
+ },
+
+ /**
+ * Read as far as we can into the incoming data, attempting to build up a
+ * complete packet header (which terminates with ":"). We'll only read up to
+ * PACKET_HEADER_MAX characters.
+ * @return boolean
+ * True if we now have a complete header.
+ */
+ _readHeader() {
+ const amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
+ this._incomingHeader += StreamUtils.delimitedRead(
+ this._scriptableInput,
+ ":",
+ amountToRead
+ );
+ if (flags.wantVerbose) {
+ dumpv("Header read: " + this._incomingHeader);
+ }
+
+ if (this._incomingHeader.endsWith(":")) {
+ if (flags.wantVerbose) {
+ dumpv("Found packet header successfully: " + this._incomingHeader);
+ }
+ return true;
+ }
+
+ if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
+ throw new Error("Failed to parse packet header!");
+ }
+
+ // Not enough data yet.
+ return false;
+ },
+
+ /**
+ * If the incoming packet is done, log it as needed and clear the buffer.
+ */
+ _flushIncoming() {
+ if (!this._incoming.done) {
+ return;
+ }
+ if (flags.wantLogging) {
+ dumpn("Got: " + this._incoming);
+ }
+ this._destroyIncoming();
+ },
+
+ /**
+ * Handler triggered by an incoming JSONPacket completing it's |read| method.
+ * Delivers the packet to this.hooks.onPacket.
+ */
+ _onJSONObjectReady(object) {
+ DevToolsUtils.executeSoon(
+ DevToolsUtils.makeInfallible(() => {
+ // Ensure the transport is still alive by the time this runs.
+ if (this.active) {
+ this.hooks.onPacket(object);
+ }
+ }, "DebuggerTransport instance's this.hooks.onPacket")
+ );
+ },
+
+ /**
+ * Handler triggered by an incoming BulkPacket entering the |read| phase for
+ * the stream portion of the packet. Delivers info about the incoming
+ * streaming data to this.hooks.onBulkPacket. See the main comment on the
+ * transport at the top of this file for more details.
+ */
+ _onBulkReadReady(...args) {
+ DevToolsUtils.executeSoon(
+ DevToolsUtils.makeInfallible(() => {
+ // Ensure the transport is still alive by the time this runs.
+ if (this.active) {
+ this.hooks.onBulkPacket(...args);
+ }
+ }, "DebuggerTransport instance's this.hooks.onBulkPacket")
+ );
+ },
+
+ /**
+ * Remove all handlers and references related to the current incoming packet,
+ * either because it is now complete or because the transport is closing.
+ */
+ _destroyIncoming() {
+ if (this._incoming) {
+ this._incoming.destroy();
+ }
+ this._incomingHeader = "";
+ this._incoming = null;
+ },
+};
+
+exports.DebuggerTransport = DebuggerTransport;
diff --git a/devtools/shared/transport/websocket-transport.js b/devtools/shared/transport/websocket-transport.js
new file mode 100644
index 0000000000..b8255e0067
--- /dev/null
+++ b/devtools/shared/transport/websocket-transport.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";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+function WebSocketDebuggerTransport(socket) {
+ EventEmitter.decorate(this);
+
+ this.active = false;
+ this.hooks = null;
+ this.socket = socket;
+}
+
+WebSocketDebuggerTransport.prototype = {
+ ready() {
+ if (this.active) {
+ return;
+ }
+
+ this.socket.addEventListener("message", this);
+ this.socket.addEventListener("close", this);
+
+ this.active = true;
+ },
+
+ send(object) {
+ this.emit("send", object);
+ if (this.socket) {
+ this.socket.send(JSON.stringify(object));
+ }
+ },
+
+ startBulkSend() {
+ throw new Error("Bulk send is not supported by WebSocket transport");
+ },
+
+ close() {
+ if (!this.socket) {
+ return;
+ }
+ this.emit("close");
+ this.active = false;
+
+ this.socket.removeEventListener("message", this);
+ this.socket.removeEventListener("close", this);
+ this.socket.close();
+ this.socket = null;
+
+ if (this.hooks) {
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed();
+ }
+ this.hooks = null;
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "message":
+ this.onMessage(event);
+ break;
+ case "close":
+ this.close();
+ break;
+ }
+ },
+
+ onMessage({ data }) {
+ if (typeof data !== "string") {
+ throw new Error(
+ "Binary messages are not supported by WebSocket transport"
+ );
+ }
+
+ const object = JSON.parse(data);
+ this.emit("packet", object);
+ if (this.hooks) {
+ this.hooks.onPacket(object);
+ }
+ },
+};
+
+module.exports = WebSocketDebuggerTransport;
diff --git a/devtools/shared/transport/worker-transport.js b/devtools/shared/transport/worker-transport.js
new file mode 100644
index 0000000000..903fd69cf4
--- /dev/null
+++ b/devtools/shared/transport/worker-transport.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Each worker debugger supports only a single connection to the main thread.
+// However, its theoretically possible for multiple servers to connect to the
+// same worker. Consequently, each transport has a connection id, to allow
+// messages from multiple connections to be multiplexed on a single channel.
+
+/**
+ * A transport that uses a WorkerDebugger to send packets from the main
+ * thread to a worker thread.
+ */
+class MainThreadWorkerDebuggerTransport {
+ constructor(dbg, id) {
+ this._dbg = dbg;
+ this._id = id;
+
+ this._dbgListener = {
+ onMessage: this._onMessage.bind(this),
+ };
+ }
+
+ ready() {
+ this._dbg.addListener(this._dbgListener);
+ }
+
+ close() {
+ if (this._dbgListener) {
+ this._dbg.removeListener(this._dbgListener);
+ }
+ this._dbgListener = null;
+ this.hooks?.onTransportClosed();
+ }
+
+ send(packet) {
+ this._dbg.postMessage(
+ JSON.stringify({
+ type: "message",
+ id: this._id,
+ message: packet,
+ })
+ );
+ }
+
+ startBulkSend() {
+ throw new Error("Can't send bulk data from worker threads!");
+ }
+
+ _onMessage(message) {
+ const packet = JSON.parse(message);
+ if (packet.type !== "message" || packet.id !== this._id || !this.hooks) {
+ return;
+ }
+
+ this.hooks.onPacket(packet.message);
+ }
+}
+
+exports.MainThreadWorkerDebuggerTransport = MainThreadWorkerDebuggerTransport;
+
+/**
+ * A transport that uses a WorkerDebuggerGlobalScope to send packets from a
+ * worker thread to the main thread.
+ */
+function WorkerThreadWorkerDebuggerTransport(scope, id) {
+ this._scope = scope;
+ this._id = id;
+ this._onMessage = this._onMessage.bind(this);
+}
+
+WorkerThreadWorkerDebuggerTransport.prototype = {
+ constructor: WorkerThreadWorkerDebuggerTransport,
+
+ ready() {
+ this._scope.addEventListener("message", this._onMessage);
+ },
+
+ close() {
+ this._scope.removeEventListener("message", this._onMessage);
+ this.hooks?.onTransportClosed();
+ },
+
+ send(packet) {
+ this._scope.postMessage(
+ JSON.stringify({
+ type: "message",
+ id: this._id,
+ message: packet,
+ })
+ );
+ },
+
+ startBulkSend() {
+ throw new Error("Can't send bulk data from worker threads!");
+ },
+
+ _onMessage(event) {
+ const packet = JSON.parse(event.data);
+ if (packet.type !== "message" || packet.id !== this._id) {
+ return;
+ }
+
+ if (this.hooks) {
+ this.hooks.onPacket(packet.message);
+ }
+ },
+};
+
+exports.WorkerThreadWorkerDebuggerTransport =
+ WorkerThreadWorkerDebuggerTransport;
diff --git a/devtools/shared/validate-breakpoint.jsm b/devtools/shared/validate-breakpoint.jsm
new file mode 100644
index 0000000000..b065ead7aa
--- /dev/null
+++ b/devtools/shared/validate-breakpoint.jsm
@@ -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/. */
+
+"use strict";
+
+// Because this function is used from SessionDataHelpers.jsm,
+// this has to be a JSM.
+
+var EXPORTED_SYMBOLS = ["validateBreakpointLocation"];
+
+/**
+ * Given a breakpoint location object, throws if the breakpoint look invalid
+ */
+function validateBreakpointLocation({ sourceUrl, sourceId, line, column }) {
+ if (!sourceUrl && !sourceId) {
+ throw new Error(
+ `Breakpoints expect to have either a sourceUrl or a sourceId.`
+ );
+ }
+ if (sourceUrl && typeof sourceUrl != "string") {
+ throw new Error(
+ `Breakpoints expect to have sourceUrl string, got ${typeof sourceUrl} instead.`
+ );
+ }
+ // sourceId may be undefined for some sources keyed by URL
+ if (sourceId && typeof sourceId != "string") {
+ throw new Error(
+ `Breakpoints expect to have sourceId string, got ${typeof sourceId} instead.`
+ );
+ }
+ if (typeof line != "number") {
+ throw new Error(
+ `Breakpoints expect to have line number, got ${typeof line} instead.`
+ );
+ }
+ if (typeof column != "number") {
+ throw new Error(
+ `Breakpoints expect to have column number, got ${typeof column} instead.`
+ );
+ }
+}
+
+// Allow this JSM to also be loaded as a CommonJS module
+// Because this module is used from the worker thread,
+// and workers can't load JSMs.
+if (typeof module == "object") {
+ module.exports.validateBreakpointLocation = validateBreakpointLocation;
+}
diff --git a/devtools/shared/wasm-source-map.js b/devtools/shared/wasm-source-map.js
new file mode 100644
index 0000000000..716c794db2
--- /dev/null
+++ b/devtools/shared/wasm-source-map.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/. */
+
+"use strict";
+
+/**
+ * SourceMapConsumer for WebAssembly source maps. It transposes columns with
+ * lines, which allows mapping data to be used with SpiderMonkey Debugger API.
+ */
+class WasmRemap {
+ /**
+ * @param map SourceMapConsumer
+ */
+ constructor(map) {
+ this._map = map;
+ this.version = map.version;
+ this.file = map.file;
+ this._computeColumnSpans = false;
+ }
+
+ get sources() {
+ return this._map.sources;
+ }
+
+ get sourceRoot() {
+ return this._map.sourceRoot;
+ }
+
+ /**
+ * @param url string
+ */
+ set sourceRoot(url) {
+ // important, since sources are using this.
+ this._map.sourceRoot = url;
+ }
+
+ get names() {
+ return this._map.names;
+ }
+
+ get sourcesContent() {
+ return this._map.sourcesContent;
+ }
+
+ get mappings() {
+ throw new Error("not supported");
+ }
+
+ computeColumnSpans() {
+ this._computeColumnSpans = true;
+ }
+
+ originalPositionFor(generatedPosition) {
+ const result = this._map.originalPositionFor({
+ line: 1,
+ column: generatedPosition.line,
+ bias: generatedPosition.bias,
+ });
+ return result;
+ }
+
+ _remapGeneratedPosition(position) {
+ const generatedPosition = {
+ line: position.column,
+ column: 0,
+ };
+ if (this._computeColumnSpans) {
+ generatedPosition.lastColumn = Infinity;
+ }
+ return generatedPosition;
+ }
+
+ generatedPositionFor(originalPosition) {
+ const position = this._map.generatedPositionFor(originalPosition);
+ return this._remapGeneratedPosition(position);
+ }
+
+ allGeneratedPositionsFor(originalPosition) {
+ const positions = this._map.allGeneratedPositionsFor(originalPosition);
+ return positions.map(position => {
+ return this._remapGeneratedPosition(position);
+ });
+ }
+
+ hasContentsOfAllSources() {
+ return this._map.hasContentsOfAllSources();
+ }
+
+ sourceContentFor(source, returnNullOnMissing) {
+ return this._map.sourceContentFor(source, returnNullOnMissing);
+ }
+
+ eachMapping(callback, context, order) {
+ this._map.eachMapping(
+ entry => {
+ const { source, generatedColumn, originalLine, originalColumn, name } =
+ entry;
+ callback({
+ source,
+ generatedLine: generatedColumn,
+ generatedColumn: 0,
+ originalLine,
+ originalColumn,
+ name,
+ });
+ },
+ context,
+ order
+ );
+ }
+}
+
+exports.WasmRemap = WasmRemap;
diff --git a/devtools/shared/webconsole/GenerateDataFromWebIdls.py b/devtools/shared/webconsole/GenerateDataFromWebIdls.py
new file mode 100644
index 0000000000..467f30faa8
--- /dev/null
+++ b/devtools/shared/webconsole/GenerateDataFromWebIdls.py
@@ -0,0 +1,176 @@
+"""
+This script parses mozilla-central's WebIDL bindings and writes a JSON-formatted
+subset of the function bindings to several files:
+- "devtools/server/actors/webconsole/webidl-pure-allowlist.js" (for eager evaluation processing)
+- "devtools/server/actors/webconsole/webidl-unsafe-getters-names.js" (for preventing automatically call getters that could emit warnings)
+
+Run this script via
+
+> ./mach python devtools/shared/webconsole/GenerateDataFromWebIdls.py
+
+with a mozconfig that references a built non-artifact build.
+"""
+
+from os import path, remove, system
+import json
+import WebIDL
+import buildconfig
+
+# This is an explicit list of interfaces to load [Pure] and [Constant]
+# annotation for. There are a bunch of things that are pure in other interfaces
+# that we don't care about in the context of the devtools.
+PURE_INTERFACE_ALLOWLIST = set(
+ [
+ "Document",
+ "Node",
+ "DOMTokenList",
+ "Element",
+ "Performance",
+ "URLSearchParams",
+ "FormData",
+ "Headers",
+ ]
+)
+
+# This is an explicit list of interfaces to exclude.
+DEPRECATED_INTERFACE__EXCLUDE_LIST = set(
+ [
+ "External",
+ "TestExampleInterface",
+ "TestInterface",
+ "TestJSImplInterface",
+ "TestingDeprecatedInterface",
+ ]
+)
+
+FILE_TEMPLATE = """\
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is automatically generated by the GenerateDataFromWebIdls.py
+// script. Do not modify it manually.
+"use strict";
+
+module.exports = %(data)s;
+"""
+
+pure_output_file = path.join(
+ buildconfig.topsrcdir, "devtools/server/actors/webconsole/webidl-pure-allowlist.js"
+)
+unsafe_getters_names_file = path.join(
+ buildconfig.topsrcdir,
+ "devtools/server/actors/webconsole/webidl-unsafe-getters-names.js",
+)
+
+input_file = path.join(buildconfig.topobjdir, "dom/bindings/file-lists.json")
+
+if not path.isfile(input_file):
+ raise Exception(
+ "Script must be run with a mozconfig referencing a non-artifact OBJDIR"
+ )
+
+file_list = json.load(open(input_file))
+
+parser = WebIDL.Parser()
+for filepath in file_list["webidls"]:
+ with open(filepath, "r", encoding="utf8") as f:
+ parser.parse(f.read(), filepath)
+results = parser.finish()
+
+# TODO: Bug 1616013 - Move more of these to be part of the pure list.
+pure_output = {
+ "Document": {
+ "prototype": [
+ "getSelection",
+ "hasStorageAccess",
+ ],
+ },
+ "Range": {
+ "prototype": [
+ "isPointInRange",
+ "comparePoint",
+ "intersectsNode",
+ # These two functions aren't pure because they do trigger
+ # layout when they are called, but in the context of eager
+ # evaluation, that should be a totally fine thing to do.
+ "getClientRects",
+ "getBoundingClientRect",
+ ],
+ },
+ "Selection": {
+ "prototype": ["getRangeAt", "containsNode"],
+ },
+}
+unsafe_getters_names = []
+for result in results:
+ if isinstance(result, WebIDL.IDLInterface):
+ iface = result.identifier.name
+
+ is_global = result.getExtendedAttribute("Global")
+
+ for member in result.members:
+ name = member.identifier.name
+
+ if member.isMethod() and member.affects == "Nothing":
+ if (
+ PURE_INTERFACE_ALLOWLIST and not iface in PURE_INTERFACE_ALLOWLIST
+ ) or name.startswith("_"):
+ continue
+
+ if is_global:
+ raise Exception(
+ "Global methods and accessors are not supported: " + iface
+ )
+
+ if iface not in pure_output:
+ pure_output[iface] = {}
+
+ if member.isStatic():
+ owner_type = "static"
+ else:
+ owner_type = "prototype"
+
+ if owner_type not in pure_output[iface]:
+ pure_output[iface][owner_type] = []
+
+ # All DOM getters are considered eagerly-evaluate-able.
+ # Collect methods only.
+ #
+ # NOTE: We still need to calculate unsafe_getters_names for
+ # object preview.
+ if member.isMethod():
+ pure_output[iface][owner_type].append(name)
+
+ if (
+ not iface in DEPRECATED_INTERFACE__EXCLUDE_LIST
+ and not name in unsafe_getters_names
+ and member.isAttr()
+ and (
+ member.getExtendedAttribute("Deprecated")
+ or member.getExtendedAttribute("LegacyLenientThis")
+ )
+ ):
+ unsafe_getters_names.append(name)
+
+
+with open(pure_output_file, "w") as f:
+ f.write(FILE_TEMPLATE % {"data": json.dumps(pure_output, indent=2, sort_keys=True)})
+print("Successfully generated", pure_output_file)
+
+unsafe_getters_names.sort()
+with open(unsafe_getters_names_file, "w") as f:
+ f.write(
+ FILE_TEMPLATE
+ % {"data": json.dumps(unsafe_getters_names, indent=2, sort_keys=True)}
+ )
+print("Successfully generated", unsafe_getters_names_file)
+
+print("Formatting files...")
+system("./mach eslint --fix " + pure_output_file + " " + unsafe_getters_names_file)
+print("Files are now properly formatted")
+
+# Parsing the idls generate a parser.out file that we don't have any use of.
+if path.exists("parser.out"):
+ remove("parser.out")
+print("DONE")
diff --git a/devtools/shared/webconsole/GenerateReservedWordsJS.py b/devtools/shared/webconsole/GenerateReservedWordsJS.py
new file mode 100644
index 0000000000..cd8a47b648
--- /dev/null
+++ b/devtools/shared/webconsole/GenerateReservedWordsJS.py
@@ -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/.
+
+import re
+import sys
+
+
+def read_reserved_word_list(filename):
+ macro_pat = re.compile(r"^\s*MACRO\(([^,]+), *[^,]+, *[^\)]+\)\s*\\?$")
+
+ reserved_word_list = []
+ with open(filename, "r") as f:
+ for line in f:
+ m = macro_pat.search(line)
+ if m:
+ reserved_word_list.append(m.group(1))
+
+ assert len(reserved_word_list) != 0
+
+ return reserved_word_list
+
+
+def line(opt, s):
+ opt["output"].write("{}\n".format(s))
+
+
+def main(output, reserved_words_h):
+ reserved_word_list = read_reserved_word_list(reserved_words_h)
+ opt = {"output": output}
+
+ line(opt, "const JS_RESERVED_WORDS = [")
+ for word in reserved_word_list:
+ line(opt, ' "{}",'.format(word))
+ line(opt, "];")
+ line(opt, "module.exports = JS_RESERVED_WORDS;")
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/devtools/shared/webconsole/analyze-input-string.js b/devtools/shared/webconsole/analyze-input-string.js
new file mode 100644
index 0000000000..a0e36247e3
--- /dev/null
+++ b/devtools/shared/webconsole/analyze-input-string.js
@@ -0,0 +1,406 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 STATE_NORMAL = Symbol("STATE_NORMAL");
+const STATE_QUOTE = Symbol("STATE_QUOTE");
+const STATE_DQUOTE = Symbol("STATE_DQUOTE");
+const STATE_TEMPLATE_LITERAL = Symbol("STATE_TEMPLATE_LITERAL");
+const STATE_ESCAPE_QUOTE = Symbol("STATE_ESCAPE_QUOTE");
+const STATE_ESCAPE_DQUOTE = Symbol("STATE_ESCAPE_DQUOTE");
+const STATE_ESCAPE_TEMPLATE_LITERAL = Symbol("STATE_ESCAPE_TEMPLATE_LITERAL");
+const STATE_SLASH = Symbol("STATE_SLASH");
+const STATE_INLINE_COMMENT = Symbol("STATE_INLINE_COMMENT");
+const STATE_MULTILINE_COMMENT = Symbol("STATE_MULTILINE_COMMENT");
+const STATE_MULTILINE_COMMENT_CLOSE = Symbol("STATE_MULTILINE_COMMENT_CLOSE");
+const STATE_QUESTION_MARK = Symbol("STATE_QUESTION_MARK");
+
+const OPEN_BODY = "{[(".split("");
+const CLOSE_BODY = "}])".split("");
+const OPEN_CLOSE_BODY = {
+ "{": "}",
+ "[": "]",
+ "(": ")",
+};
+
+const NO_AUTOCOMPLETE_PREFIXES = ["var", "const", "let", "function", "class"];
+const OPERATOR_CHARS_SET = new Set(";,:=<>+-*%|&^~!".split(""));
+
+/**
+ * Analyses a given string to find the last statement that is interesting for
+ * later completion.
+ *
+ * @param string str
+ * A string to analyse.
+ *
+ * @returns object
+ * If there was an error in the string detected, then a object like
+ *
+ * { err: "ErrorMesssage" }
+ *
+ * is returned, otherwise a object like
+ *
+ * {
+ * state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
+ * lastStatement: the last statement in the string,
+ * isElementAccess: boolean that indicates if the lastStatement has an open
+ * element access (e.g. `x["match`).
+ * isPropertyAccess: boolean indicating if we are accessing property
+ * (e.g `true` in `var a = {b: 1};a.b`)
+ * matchProp: The part of the expression that should match the properties
+ * on the mainExpression (e.g. `que` when expression is `document.body.que`)
+ * mainExpression: The part of the expression before any property access,
+ * (e.g. `a.b` if expression is `a.b.`)
+ * expressionBeforePropertyAccess: The part of the expression before property access
+ * (e.g `var a = {b: 1};a` if expression is `var a = {b: 1};a.b`)
+ * }
+ */
+// eslint-disable-next-line complexity
+exports.analyzeInputString = function (str, timeout = 2500) {
+ // work variables.
+ const bodyStack = [];
+ let state = STATE_NORMAL;
+ let previousNonWhitespaceChar;
+ let lastStatement = "";
+ let currentIndex = -1;
+ let dotIndex;
+ let pendingWhitespaceChars = "";
+ const startingTime = Date.now();
+
+ // Use a string iterator in order to handle character with a length >= 2 (e.g. 😎).
+ for (const c of str) {
+ // We are possibly dealing with a very large string that would take a long time to
+ // analyze (and freeze the process). If the function has been running for more than
+ // a given time, we stop the analysis (this isn't too bad because the only
+ // consequence is that we won't provide autocompletion items).
+ if (Date.now() - startingTime > timeout) {
+ return {
+ err: "timeout",
+ };
+ }
+
+ currentIndex += 1;
+ let resetLastStatement = false;
+ const isWhitespaceChar = c.trim() === "";
+ switch (state) {
+ // Normal JS state.
+ case STATE_NORMAL:
+ if (lastStatement.endsWith("?.") && /\d/.test(c)) {
+ // If the current char is a number, the engine will consider we're not
+ // performing an optional chaining, but a ternary (e.g. x ?.4 : 2).
+ lastStatement = "";
+ }
+
+ // Storing the index of dot of the input string
+ if (c === ".") {
+ dotIndex = currentIndex;
+ }
+
+ // If the last characters were spaces, and the current one is not.
+ if (pendingWhitespaceChars && !isWhitespaceChar) {
+ // If we have a legitimate property/element access, or potential optional
+ // chaining call, we append the spaces.
+ if (c === "[" || c === "." || c === "?") {
+ lastStatement = lastStatement + pendingWhitespaceChars;
+ } else {
+ // if not, we can be sure the statement was over, and we can start a new one.
+ lastStatement = "";
+ }
+ pendingWhitespaceChars = "";
+ }
+
+ if (c == '"') {
+ state = STATE_DQUOTE;
+ } else if (c == "'") {
+ state = STATE_QUOTE;
+ } else if (c == "`") {
+ state = STATE_TEMPLATE_LITERAL;
+ } else if (c == "/") {
+ state = STATE_SLASH;
+ } else if (c == "?") {
+ state = STATE_QUESTION_MARK;
+ } else if (OPERATOR_CHARS_SET.has(c)) {
+ // If the character is an operator, we can update the current statement.
+ resetLastStatement = true;
+ } else if (isWhitespaceChar) {
+ // If the previous char isn't a dot or opening bracket, and the current computed
+ // statement is not a variable/function/class declaration, we track the number
+ // of consecutive spaces, so we can re-use them at some point (or drop them).
+ if (
+ previousNonWhitespaceChar !== "." &&
+ previousNonWhitespaceChar !== "[" &&
+ !NO_AUTOCOMPLETE_PREFIXES.includes(lastStatement)
+ ) {
+ pendingWhitespaceChars += c;
+ continue;
+ }
+ } else if (OPEN_BODY.includes(c)) {
+ // When opening a bracket or a parens, we store the current statement, in order
+ // to be able to retrieve it later.
+ bodyStack.push({
+ token: c,
+ lastStatement,
+ index: currentIndex,
+ });
+ // And we compute a new statement.
+ resetLastStatement = true;
+ } else if (CLOSE_BODY.includes(c)) {
+ const last = bodyStack.pop();
+ if (!last || OPEN_CLOSE_BODY[last.token] != c) {
+ return {
+ err: "syntax error",
+ };
+ }
+ if (c == "}") {
+ resetLastStatement = true;
+ } else {
+ lastStatement = last.lastStatement;
+ }
+ }
+ break;
+
+ // Escaped quote
+ case STATE_ESCAPE_QUOTE:
+ state = STATE_QUOTE;
+ break;
+ case STATE_ESCAPE_DQUOTE:
+ state = STATE_DQUOTE;
+ break;
+ case STATE_ESCAPE_TEMPLATE_LITERAL:
+ state = STATE_TEMPLATE_LITERAL;
+ break;
+
+ // Double quote state > " <
+ case STATE_DQUOTE:
+ if (c == "\\") {
+ state = STATE_ESCAPE_DQUOTE;
+ } else if (c == "\n") {
+ return {
+ err: "unterminated string literal",
+ };
+ } else if (c == '"') {
+ state = STATE_NORMAL;
+ }
+ break;
+
+ // Template literal state > ` <
+ case STATE_TEMPLATE_LITERAL:
+ if (c == "\\") {
+ state = STATE_ESCAPE_TEMPLATE_LITERAL;
+ } else if (c == "`") {
+ state = STATE_NORMAL;
+ }
+ break;
+
+ // Single quote state > ' <
+ case STATE_QUOTE:
+ if (c == "\\") {
+ state = STATE_ESCAPE_QUOTE;
+ } else if (c == "\n") {
+ return {
+ err: "unterminated string literal",
+ };
+ } else if (c == "'") {
+ state = STATE_NORMAL;
+ }
+ break;
+ case STATE_SLASH:
+ if (c == "/") {
+ state = STATE_INLINE_COMMENT;
+ } else if (c == "*") {
+ state = STATE_MULTILINE_COMMENT;
+ } else {
+ lastStatement = "";
+ state = STATE_NORMAL;
+ }
+ break;
+
+ case STATE_INLINE_COMMENT:
+ if (c === "\n") {
+ state = STATE_NORMAL;
+ resetLastStatement = true;
+ }
+ break;
+
+ case STATE_MULTILINE_COMMENT:
+ if (c === "*") {
+ state = STATE_MULTILINE_COMMENT_CLOSE;
+ }
+ break;
+
+ case STATE_MULTILINE_COMMENT_CLOSE:
+ if (c === "/") {
+ state = STATE_NORMAL;
+ resetLastStatement = true;
+ } else {
+ state = STATE_MULTILINE_COMMENT;
+ }
+ break;
+
+ case STATE_QUESTION_MARK:
+ state = STATE_NORMAL;
+ if (c === "?") {
+ // If we have a nullish coalescing operator, we start a new statement
+ resetLastStatement = true;
+ } else if (c !== ".") {
+ // If we're not dealing with optional chaining (?.), it means we have a ternary,
+ // so we are starting a new statement that includes the current character.
+ lastStatement = "";
+ } else {
+ dotIndex = currentIndex;
+ }
+ break;
+ }
+
+ if (!isWhitespaceChar) {
+ previousNonWhitespaceChar = c;
+ }
+ if (resetLastStatement) {
+ lastStatement = "";
+ } else {
+ lastStatement = lastStatement + c;
+ }
+
+ // We update all the open stacks lastStatement so they are up-to-date.
+ bodyStack.forEach(stack => {
+ if (stack.token !== "}") {
+ stack.lastStatement = stack.lastStatement + c;
+ }
+ });
+ }
+
+ let isElementAccess = false;
+ let lastOpeningBracketIndex = -1;
+ if (bodyStack.length === 1 && bodyStack[0].token === "[") {
+ lastStatement = bodyStack[0].lastStatement;
+ lastOpeningBracketIndex = bodyStack[0].index;
+ isElementAccess = true;
+
+ if (
+ state === STATE_DQUOTE ||
+ state === STATE_QUOTE ||
+ state === STATE_TEMPLATE_LITERAL ||
+ state === STATE_ESCAPE_QUOTE ||
+ state === STATE_ESCAPE_DQUOTE ||
+ state === STATE_ESCAPE_TEMPLATE_LITERAL
+ ) {
+ state = STATE_NORMAL;
+ }
+ } else if (pendingWhitespaceChars) {
+ lastStatement = "";
+ }
+
+ const lastCompletionCharIndex = isElementAccess
+ ? lastOpeningBracketIndex
+ : dotIndex;
+
+ const stringBeforeLastCompletionChar = str.slice(0, lastCompletionCharIndex);
+
+ const isPropertyAccess =
+ lastCompletionCharIndex && lastCompletionCharIndex > 0;
+
+ // Compute `isOptionalAccess`, so that we can use it
+ // later for computing `expressionBeforePropertyAccess`.
+ //Check `?.` before `[` for element access ( e.g `a?.["b` or `a ?. ["b` )
+ // and `?` before `.` for regular property access ( e.g `a?.b` or `a ?. b` )
+ const optionalElementAccessRegex = /\?\.\s*$/;
+ const isOptionalAccess = isElementAccess
+ ? optionalElementAccessRegex.test(stringBeforeLastCompletionChar)
+ : isPropertyAccess &&
+ str.slice(lastCompletionCharIndex - 1, lastCompletionCharIndex + 1) ===
+ "?.";
+
+ // Get the filtered string for the properties (e.g if `document.qu` then `qu`)
+ const matchProp = isPropertyAccess
+ ? str.slice(lastCompletionCharIndex + 1).trimLeft()
+ : null;
+
+ const expressionBeforePropertyAccess = isPropertyAccess
+ ? str.slice(
+ 0,
+ // For optional access, we can take all the chars before the last "?" char.
+ isOptionalAccess
+ ? stringBeforeLastCompletionChar.lastIndexOf("?")
+ : lastCompletionCharIndex
+ )
+ : str;
+
+ let mainExpression = lastStatement;
+ if (isPropertyAccess) {
+ if (isOptionalAccess) {
+ // Strip anything before the last `?`.
+ mainExpression = mainExpression.slice(0, mainExpression.lastIndexOf("?"));
+ } else {
+ mainExpression = mainExpression.slice(
+ 0,
+ -1 * (str.length - lastCompletionCharIndex)
+ );
+ }
+ }
+
+ mainExpression = mainExpression.trim();
+
+ return {
+ state,
+ isElementAccess,
+ isPropertyAccess,
+ expressionBeforePropertyAccess,
+ lastStatement,
+ mainExpression,
+ matchProp,
+ };
+};
+
+/**
+ * Checks whether the analyzed input string is in an appropriate state to autocomplete, e.g. not
+ * inside a string, or declaring a variable.
+ * @param {object} inputAnalysisState The analyzed string to check
+ * @returns {boolean} Whether the input should be autocompleted
+ */
+exports.shouldInputBeAutocompleted = function (inputAnalysisState) {
+ const { err, state, lastStatement } = inputAnalysisState;
+
+ // There was an error analysing the string.
+ if (err) {
+ return false;
+ }
+
+ // If the current state is not STATE_NORMAL, then we are inside string,
+ // which means that no completion is possible.
+ if (state != STATE_NORMAL) {
+ return false;
+ }
+
+ // Don't complete on just an empty string.
+ if (lastStatement.trim() == "") {
+ return false;
+ }
+
+ if (
+ NO_AUTOCOMPLETE_PREFIXES.some(prefix =>
+ lastStatement.startsWith(prefix + " ")
+ )
+ ) {
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Checks whether the analyzed input string is in an appropriate state to be eagerly evaluated.
+ * @param {object} inputAnalysisState
+ * @returns {boolean} Whether the input should be eagerly evaluated
+ */
+exports.shouldInputBeEagerlyEvaluated = function ({ lastStatement }) {
+ const inComputedProperty =
+ lastStatement.lastIndexOf("[") !== -1 &&
+ lastStatement.lastIndexOf("[") > lastStatement.lastIndexOf("]");
+
+ const hasPropertyAccess =
+ lastStatement.includes(".") || lastStatement.includes("[");
+
+ return hasPropertyAccess && !inComputedProperty;
+};
diff --git a/devtools/shared/webconsole/js-property-provider.js b/devtools/shared/webconsole/js-property-provider.js
new file mode 100644
index 0000000000..ca30e8f57b
--- /dev/null
+++ b/devtools/shared/webconsole/js-property-provider.js
@@ -0,0 +1,803 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+
+const {
+ evalWithDebugger,
+} = require("resource://devtools/server/actors/webconsole/eval-with-debugger.js");
+
+if (!isWorker) {
+ loader.lazyRequireGetter(
+ this,
+ "getSyntaxTrees",
+ "resource://devtools/shared/webconsole/parser-helper.js",
+ true
+ );
+}
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ Reflect: "resource://gre/modules/reflect.sys.mjs",
+});
+loader.lazyRequireGetter(
+ this,
+ [
+ "analyzeInputString",
+ "shouldInputBeAutocompleted",
+ "shouldInputBeEagerlyEvaluated",
+ ],
+ "resource://devtools/shared/webconsole/analyze-input-string.js",
+ true
+);
+
+// Provide an easy way to bail out of even attempting an autocompletion
+// if an object has way too many properties. Protects against large objects
+// with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS.
+const MAX_AUTOCOMPLETE_ATTEMPTS = (exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000);
+// Prevent iterating over too many properties during autocomplete suggestions.
+const MAX_AUTOCOMPLETIONS = (exports.MAX_AUTOCOMPLETIONS = 1500);
+
+/**
+ * Provides a list of properties, that are possible matches based on the passed
+ * Debugger.Environment/Debugger.Object and inputValue.
+ *
+ * @param {Object} An object of the following shape:
+ * - {Object} dbgObject
+ * When the debugger is not paused this Debugger.Object wraps
+ * the scope for autocompletion.
+ * It is null if the debugger is paused.
+ * - {Object} environment
+ * When the debugger is paused this Debugger.Environment is the
+ * scope for autocompletion.
+ * It is null if the debugger is not paused.
+ * - {String} inputValue
+ * Value that should be completed.
+ * - {Number} cursor (defaults to inputValue.length).
+ * Optional offset in the input where the cursor is located. If this is
+ * omitted then the cursor is assumed to be at the end of the input
+ * value.
+ * - {Array} authorizedEvaluations (defaults to []).
+ * Optional array containing all the different properties access that the engine
+ * can execute in order to retrieve its result's properties.
+ * ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects
+ * in the content page ⚠️
+ * - {WebconsoleActor} webconsoleActor
+ * A reference to a webconsole actor which we can use to retrieve the last
+ * evaluation result or create a debuggee value.
+ * - {String}: selectedNodeActor
+ * The actor id of the selected node in the inspector.
+ * - {Array<string>}: expressionVars
+ * Optional array containing variable defined in the expression. Those variables
+ * are extracted from CodeMirror state.
+ * @returns null or object
+ * If the inputValue is an unsafe getter and invokeUnsafeGetter is false, the
+ * following form is returned:
+ *
+ * {
+ * isUnsafeGetter: true,
+ * getterPath: {Array<String>} An array of the property chain leading to the
+ * getter. Example: ["x", "myGetter"]
+ * }
+ *
+ * If no completion valued could be computed, and the input is not an unsafe
+ * getter, null is returned.
+ *
+ * Otherwise an object with the following form is returned:
+ * {
+ * matches: Set<string>
+ * matchProp: Last part of the inputValue that was used to find
+ * the matches-strings.
+ * isElementAccess: Boolean set to true if the evaluation is an element
+ * access (e.g. `window["addEvent`).
+ * }
+ */
+// eslint-disable-next-line complexity
+function jsPropertyProvider({
+ dbgObject,
+ environment,
+ frameActorId,
+ inputValue,
+ cursor,
+ authorizedEvaluations = [],
+ webconsoleActor,
+ selectedNodeActor,
+ expressionVars = [],
+}) {
+ if (cursor === undefined) {
+ cursor = inputValue.length;
+ }
+
+ inputValue = inputValue.substring(0, cursor);
+
+ // Analyse the inputValue and find the beginning of the last part that
+ // should be completed.
+ const inputAnalysis = analyzeInputString(inputValue);
+
+ if (!shouldInputBeAutocompleted(inputAnalysis)) {
+ return null;
+ }
+
+ let {
+ lastStatement,
+ isElementAccess,
+ mainExpression,
+ matchProp,
+ isPropertyAccess,
+ } = inputAnalysis;
+
+ // Eagerly evaluate the main expression and return the results properties.
+ // e.g. `obj.func().a` will evaluate `obj.func()` and return properties matching `a`.
+ // NOTE: this is only useful when the input has a property access.
+ if (webconsoleActor && shouldInputBeEagerlyEvaluated(inputAnalysis)) {
+ const eagerResponse = evalWithDebugger(
+ mainExpression,
+ { eager: true, selectedNodeActor, frameActor: frameActorId },
+ webconsoleActor
+ );
+
+ const ret = eagerResponse?.result?.return;
+
+ // Only send matches if eager evaluation returned something meaningful
+ if (ret && ret !== undefined) {
+ const matches =
+ typeof ret != "object"
+ ? getMatchedProps(ret, matchProp)
+ : getMatchedPropsInDbgObject(ret, matchProp);
+
+ return prepareReturnedObject({
+ matches,
+ search: matchProp,
+ isElementAccess,
+ });
+ }
+ }
+
+ // AST representation of the expression before the last access char (`.` or `[`).
+ let astExpression;
+ const startQuoteRegex = /^('|"|`)/;
+ const env = environment || dbgObject.asEnvironment();
+
+ // Catch literals like [1,2,3] or "foo" and return the matches from
+ // their prototypes.
+ // Don't run this is a worker, migrating to acorn should allow this
+ // to run in a worker - Bug 1217198.
+ if (!isWorker && isPropertyAccess) {
+ const syntaxTrees = getSyntaxTrees(mainExpression);
+ const lastTree = syntaxTrees[syntaxTrees.length - 1];
+ const lastBody = lastTree?.body[lastTree.body.length - 1];
+
+ // Finding the last expression since we've sliced up until the dot.
+ // If there were parse errors this won't exist.
+ if (lastBody) {
+ if (!lastBody.expression) {
+ return null;
+ }
+
+ astExpression = lastBody.expression;
+ let matchingObject;
+
+ if (astExpression.type === "ArrayExpression") {
+ matchingObject = getContentPrototypeObject(env, "Array");
+ } else if (
+ astExpression.type === "Literal" &&
+ typeof astExpression.value === "string"
+ ) {
+ matchingObject = getContentPrototypeObject(env, "String");
+ } else if (
+ astExpression.type === "Literal" &&
+ Number.isFinite(astExpression.value)
+ ) {
+ // The parser rightfuly indicates that we have a number in some cases (e.g. `1.`),
+ // but we don't want to return Number proto properties in that case since
+ // the result would be invalid (i.e. `1.toFixed()` throws).
+ // So if the expression value is an integer, it should not end with `{Number}.`
+ // (but the following are fine: `1..`, `(1.).`).
+ if (
+ !Number.isInteger(astExpression.value) ||
+ /\d[^\.]{0}\.$/.test(lastStatement) === false
+ ) {
+ matchingObject = getContentPrototypeObject(env, "Number");
+ } else {
+ return null;
+ }
+ }
+
+ if (matchingObject) {
+ let search = matchProp;
+
+ let elementAccessQuote;
+ if (isElementAccess && startQuoteRegex.test(matchProp)) {
+ elementAccessQuote = matchProp[0];
+ search = matchProp.replace(startQuoteRegex, "");
+ }
+
+ let props = getMatchedPropsInDbgObject(matchingObject, search);
+
+ if (isElementAccess) {
+ props = wrapMatchesInQuotes(props, elementAccessQuote);
+ }
+
+ return {
+ isElementAccess,
+ matchProp,
+ matches: props,
+ };
+ }
+ }
+ }
+
+ // We are completing a variable / a property lookup.
+ let properties = [];
+
+ if (astExpression) {
+ if (isPropertyAccess) {
+ properties = getPropertiesFromAstExpression(astExpression);
+
+ if (properties === null) {
+ return null;
+ }
+ }
+ } else {
+ properties = lastStatement.split(".");
+ if (isElementAccess) {
+ const lastPart = properties[properties.length - 1];
+ const openBracketIndex = lastPart.lastIndexOf("[");
+ matchProp = lastPart.substr(openBracketIndex + 1);
+ properties[properties.length - 1] = lastPart.substring(
+ 0,
+ openBracketIndex
+ );
+ } else {
+ matchProp = properties.pop().trimLeft();
+ }
+ }
+
+ let search = matchProp;
+ let elementAccessQuote;
+ if (isElementAccess && startQuoteRegex.test(search)) {
+ elementAccessQuote = search[0];
+ search = search.replace(startQuoteRegex, "");
+ }
+
+ let obj = dbgObject;
+ if (properties.length === 0) {
+ const environmentProperties = getMatchedPropsInEnvironment(env, search);
+ const expressionVariables = new Set(
+ expressionVars.filter(variableName => variableName.startsWith(matchProp))
+ );
+
+ for (const prop of environmentProperties) {
+ expressionVariables.add(prop);
+ }
+
+ return {
+ isElementAccess,
+ matchProp,
+ matches: expressionVariables,
+ };
+ }
+
+ let firstProp = properties.shift();
+ if (typeof firstProp == "string") {
+ firstProp = firstProp.trim();
+ }
+
+ if (firstProp === "this") {
+ // Special case for 'this' - try to get the Object from the Environment.
+ // No problem if it throws, we will just not autocomplete.
+ try {
+ obj = env.object;
+ } catch (e) {
+ // Ignore.
+ }
+ } else if (firstProp === "$_" && webconsoleActor) {
+ obj = webconsoleActor.getLastConsoleInputEvaluation();
+ } else if (firstProp === "$0" && selectedNodeActor && webconsoleActor) {
+ const actor = webconsoleActor.conn.getActor(selectedNodeActor);
+ if (actor) {
+ try {
+ obj = webconsoleActor.makeDebuggeeValue(actor.rawNode);
+ } catch (e) {
+ // Ignore.
+ }
+ }
+ } else if (hasArrayIndex(firstProp)) {
+ obj = getArrayMemberProperty(null, env, firstProp);
+ } else {
+ obj = getVariableInEnvironment(env, firstProp);
+ }
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+
+ // We get the rest of the properties recursively starting from the
+ // Debugger.Object that wraps the first property
+ for (let [index, prop] of properties.entries()) {
+ if (typeof prop === "string") {
+ prop = prop.trim();
+ }
+
+ if (prop === undefined || prop === null || prop === "") {
+ return null;
+ }
+
+ const propPath = [firstProp].concat(properties.slice(0, index + 1));
+ const authorized = authorizedEvaluations.some(
+ x => JSON.stringify(x) === JSON.stringify(propPath)
+ );
+
+ if (!authorized && DevToolsUtils.isUnsafeGetter(obj, prop)) {
+ // If we try to access an unsafe getter, return its name so we can consume that
+ // on the frontend.
+ return {
+ isUnsafeGetter: true,
+ getterPath: propPath,
+ };
+ }
+
+ if (hasArrayIndex(prop)) {
+ // The property to autocomplete is a member of array. For example
+ // list[i][j]..[n]. Traverse the array to get the actual element.
+ obj = getArrayMemberProperty(obj, null, prop);
+ } else {
+ obj = DevToolsUtils.getProperty(obj, prop, authorized);
+ }
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+ }
+
+ const matches =
+ typeof obj != "object"
+ ? getMatchedProps(obj, search)
+ : getMatchedPropsInDbgObject(obj, search);
+ return prepareReturnedObject({
+ matches,
+ search,
+ isElementAccess,
+ elementAccessQuote,
+ });
+}
+
+function hasArrayIndex(str) {
+ return /\[\d+\]$/.test(str);
+}
+
+/**
+ * For a given environment and constructor name, returns its Debugger.Object wrapped
+ * prototype.
+ *
+ * @param {Environment} env
+ * @param {String} name: Name of the constructor object we want the prototype of.
+ * @returns {Debugger.Object|null} the prototype, or null if it not found.
+ */
+function getContentPrototypeObject(env, name) {
+ // Retrieve the outermost environment to get the global object.
+ let outermostEnv = env;
+ while (outermostEnv?.parent) {
+ outermostEnv = outermostEnv.parent;
+ }
+
+ const constructorObj = DevToolsUtils.getProperty(outermostEnv.object, name);
+ if (!constructorObj) {
+ return null;
+ }
+
+ return DevToolsUtils.getProperty(constructorObj, "prototype");
+}
+
+/**
+ * @param {Object} ast: An AST representing a property access (e.g. `foo.bar["baz"].x`)
+ * @returns {Array|null} An array representing the property access
+ * (e.g. ["foo", "bar", "baz", "x"]).
+ */
+function getPropertiesFromAstExpression(ast) {
+ let result = [];
+ if (!ast) {
+ return result;
+ }
+ const { type, property, object, name, expression } = ast;
+ if (type === "ThisExpression") {
+ result.unshift("this");
+ } else if (type === "Identifier" && name) {
+ result.unshift(name);
+ } else if (type === "OptionalExpression" && expression) {
+ result = (getPropertiesFromAstExpression(expression) || []).concat(result);
+ } else if (
+ type === "MemberExpression" ||
+ type === "OptionalMemberExpression"
+ ) {
+ if (property) {
+ if (property.type === "Identifier" && property.name) {
+ result.unshift(property.name);
+ } else if (property.type === "Literal") {
+ result.unshift(property.value);
+ }
+ }
+ if (object) {
+ result = (getPropertiesFromAstExpression(object) || []).concat(result);
+ }
+ } else {
+ return null;
+ }
+ return result;
+}
+
+function wrapMatchesInQuotes(matches, quote = `"`) {
+ return new Set(
+ [...matches].map(p => {
+ // Escape as a double-quoted string literal
+ p = JSON.stringify(p);
+
+ // We don't have to do anything more when using double quotes
+ if (quote == `"`) {
+ return p;
+ }
+
+ // Remove surrounding double quotes
+ p = p.slice(1, -1);
+
+ // Unescape inner double quotes (all must be escaped, so no need to count backslashes)
+ p = p.replace(/\\(?=")/g, "");
+
+ // Escape the specified quote (assuming ' or `, which are treated literally in regex)
+ p = p.replace(new RegExp(quote, "g"), "\\$&");
+
+ // Template literals treat ${ specially, escape it
+ if (quote == "`") {
+ p = p.replace(/\${/g, "\\$&");
+ }
+
+ // Surround the result with quotes
+ return `${quote}${p}${quote}`;
+ })
+ );
+}
+
+/**
+ * Get the array member of obj for the given prop. For example, given
+ * prop='list[0][1]' the element at [0][1] of obj.list is returned.
+ *
+ * @param object obj
+ * The object to operate on. Should be null if env is passed.
+ * @param object env
+ * The Environment to operate in. Should be null if obj is passed.
+ * @param string prop
+ * The property to return.
+ * @return null or Object
+ * Returns null if the property couldn't be located. Otherwise the array
+ * member identified by prop.
+ */
+function getArrayMemberProperty(obj, env, prop) {
+ // First get the array.
+ const propWithoutIndices = prop.substr(0, prop.indexOf("["));
+
+ if (env) {
+ obj = getVariableInEnvironment(env, propWithoutIndices);
+ } else {
+ obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
+ }
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+
+ // Then traverse the list of indices to get the actual element.
+ let result;
+ const arrayIndicesRegex = /\[[^\]]*\]/g;
+ while ((result = arrayIndicesRegex.exec(prop)) !== null) {
+ const indexWithBrackets = result[0];
+ const indexAsText = indexWithBrackets.substr(
+ 1,
+ indexWithBrackets.length - 2
+ );
+ const index = parseInt(indexAsText, 10);
+
+ if (isNaN(index)) {
+ return null;
+ }
+
+ obj = DevToolsUtils.getProperty(obj, index);
+
+ if (!isObjectUsable(obj)) {
+ return null;
+ }
+ }
+
+ return obj;
+}
+
+/**
+ * Check if the given Debugger.Object can be used for autocomplete.
+ *
+ * @param Debugger.Object object
+ * The Debugger.Object to check.
+ * @return boolean
+ * True if further inspection into the object is possible, or false
+ * otherwise.
+ */
+function isObjectUsable(object) {
+ if (object == null) {
+ return false;
+ }
+
+ if (typeof object == "object" && object.class == "DeadObject") {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @see getExactMatchImpl()
+ */
+function getVariableInEnvironment(environment, name) {
+ return getExactMatchImpl(environment, name, DebuggerEnvironmentSupport);
+}
+
+function prepareReturnedObject({
+ matches,
+ search,
+ isElementAccess,
+ elementAccessQuote,
+}) {
+ if (isElementAccess) {
+ // If it's an element access, we need to wrap properties in quotes (either the one
+ // the user already typed, or `"`).
+
+ matches = wrapMatchesInQuotes(matches, elementAccessQuote);
+ } else if (!isWorker) {
+ // If we're not performing an element access, we need to check that the property
+ // are suited for a dot access. (reflect.sys.mjs is not available in worker context yet,
+ // see Bug 1507181).
+ for (const match of matches) {
+ try {
+ // In order to know if the property is suited for dot notation, we use Reflect
+ // to parse an expression where we try to access the property with a dot. If it
+ // throws, this means that we need to do an element access instead.
+ lazy.Reflect.parse(`({${match}: true})`);
+ } catch (e) {
+ matches.delete(match);
+ }
+ }
+ }
+
+ return { isElementAccess, matchProp: search, matches };
+}
+
+/**
+ * @see getMatchedPropsImpl()
+ */
+function getMatchedPropsInEnvironment(environment, match) {
+ return getMatchedPropsImpl(environment, match, DebuggerEnvironmentSupport);
+}
+
+/**
+ * @see getMatchedPropsImpl()
+ */
+function getMatchedPropsInDbgObject(dbgObject, match) {
+ return getMatchedPropsImpl(dbgObject, match, DebuggerObjectSupport);
+}
+
+/**
+ * @see getMatchedPropsImpl()
+ */
+function getMatchedProps(obj, match) {
+ if (typeof obj != "object") {
+ obj = obj.constructor.prototype;
+ }
+ return getMatchedPropsImpl(obj, match, JSObjectSupport);
+}
+
+/**
+ * Get all properties in the given object (and its parent prototype chain) that
+ * match a given prefix.
+ *
+ * @param {Mixed} obj
+ * Object whose properties we want to filter.
+ * @param {string} match
+ * Filter for properties that match this string.
+ * @returns {Set} List of matched properties.
+ */
+function getMatchedPropsImpl(obj, match, { chainIterator, getProperties }) {
+ const matches = new Set();
+ let numProps = 0;
+
+ const insensitiveMatching = match && match[0].toUpperCase() !== match[0];
+ const propertyMatches = prop => {
+ return insensitiveMatching
+ ? prop.toLocaleLowerCase().startsWith(match.toLocaleLowerCase())
+ : prop.startsWith(match);
+ };
+
+ // We need to go up the prototype chain.
+ const iter = chainIterator(obj);
+ for (obj of iter) {
+ const props = getProperties(obj);
+ if (!props) {
+ continue;
+ }
+ numProps += props.length;
+
+ // If there are too many properties to event attempt autocompletion,
+ // or if we have already added the max number, then stop looping
+ // and return the partial set that has already been discovered.
+ if (
+ numProps >= MAX_AUTOCOMPLETE_ATTEMPTS ||
+ matches.size >= MAX_AUTOCOMPLETIONS
+ ) {
+ break;
+ }
+
+ for (let i = 0; i < props.length; i++) {
+ const prop = props[i];
+ if (!propertyMatches(prop)) {
+ continue;
+ }
+
+ // If it is an array index, we can't take it.
+ // This uses a trick: converting a string to a number yields NaN if
+ // the operation failed, and NaN is not equal to itself.
+ // eslint-disable-next-line no-self-compare
+ if (+prop != +prop) {
+ matches.add(prop);
+ }
+
+ if (matches.size >= MAX_AUTOCOMPLETIONS) {
+ break;
+ }
+ }
+ }
+
+ return matches;
+}
+
+/**
+ * Returns a property value based on its name from the given object, by
+ * recursively checking the object's prototype.
+ *
+ * @param object obj
+ * An object to look the property into.
+ * @param string name
+ * The property that is looked up.
+ * @returns object|undefined
+ * A Debugger.Object if the property exists in the object's prototype
+ * chain, undefined otherwise.
+ */
+function getExactMatchImpl(obj, name, { chainIterator, getProperty }) {
+ // We need to go up the prototype chain.
+ const iter = chainIterator(obj);
+ for (obj of iter) {
+ const prop = getProperty(obj, name, obj);
+ if (prop) {
+ return prop.value;
+ }
+ }
+ return undefined;
+}
+
+var JSObjectSupport = {
+ *chainIterator(obj) {
+ while (obj) {
+ yield obj;
+ try {
+ obj = Object.getPrototypeOf(obj);
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return;
+ }
+ }
+ },
+
+ getProperties(obj) {
+ try {
+ return Object.getOwnPropertyNames(obj);
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return null;
+ }
+ },
+
+ getProperty() {
+ // getProperty is unsafe with raw JS objects.
+ throw new Error("Unimplemented!");
+ },
+};
+
+var DebuggerObjectSupport = {
+ *chainIterator(obj) {
+ while (obj) {
+ yield obj;
+ try {
+ // There could be transparent security wrappers, unwrap to check if it's a proxy.
+ const unwrapped = DevToolsUtils.unwrap(obj);
+ if (unwrapped === undefined) {
+ // Objects belonging to an invisible-to-debugger compartment can't be unwrapped.
+ return;
+ }
+
+ if (unwrapped.isProxy) {
+ // Proxies might have a `getPrototypeOf` method, which is triggered by `obj.proto`,
+ // but this does not impact the actual prototype chain.
+ // In such case, we need to use the proxy target prototype.
+ // We retrieve proxyTarget from `obj` (and not `unwrapped`) to avoid exposing
+ // the unwrapped target.
+ obj = unwrapped.proxyTarget;
+ }
+ obj = obj.proto;
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return;
+ }
+ }
+ },
+
+ getProperties(obj) {
+ try {
+ return obj.getOwnPropertyNames();
+ } catch (error) {
+ // The above can throw e.g. for some proxy objects.
+ return null;
+ }
+ },
+
+ getProperty(obj, name, rootObj) {
+ // This is left unimplemented in favor to DevToolsUtils.getProperty().
+ throw new Error("Unimplemented!");
+ },
+};
+
+var DebuggerEnvironmentSupport = {
+ *chainIterator(obj) {
+ while (obj) {
+ yield obj;
+ obj = obj.parent;
+ }
+ },
+
+ getProperties(obj) {
+ const names = obj.names();
+
+ // Include 'this' in results (in sorted order)
+ for (let i = 0; i < names.length; i++) {
+ if (i === names.length - 1 || names[i + 1] > "this") {
+ names.splice(i + 1, 0, "this");
+ break;
+ }
+ }
+
+ return names;
+ },
+
+ getProperty(obj, name) {
+ let result;
+ // Try/catch since name can be anything, and getVariable throws if
+ // it's not a valid ECMAScript identifier name
+ try {
+ // TODO: we should use getVariableDescriptor() here - bug 725815.
+ result = obj.getVariable(name);
+ } catch (e) {
+ // Ignore.
+ }
+
+ // FIXME: Need actual UI, bug 941287.
+ if (
+ result == null ||
+ (typeof result == "object" &&
+ (result.optimizedOut || result.missingArguments))
+ ) {
+ return null;
+ }
+ return { value: result };
+ },
+};
+
+exports.jsPropertyProvider = DevToolsUtils.makeInfallible(jsPropertyProvider);
+
+// Export a version that will throw (for tests)
+exports.fallibleJsPropertyProvider = jsPropertyProvider;
diff --git a/devtools/shared/webconsole/messages.js b/devtools/shared/webconsole/messages.js
new file mode 100644
index 0000000000..a0ec73b39e
--- /dev/null
+++ b/devtools/shared/webconsole/messages.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 getArrayTypeNames() {
+ return [
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+ "Uint8ClampedArray",
+ "BigInt64Array",
+ "BigUint64Array",
+ ];
+}
+
+/**
+ * Return true if the parameters passed to console.log is supported.
+ * The parameters can be either from server side (without getGrip) or client
+ * side (with getGrip).
+ *
+ * @param {Message} parameters
+ * @returns {Boolean}
+ */
+function isSupportedByConsoleTable(parameters) {
+ const supportedClasses = [
+ "Object",
+ "Map",
+ "Set",
+ "WeakMap",
+ "WeakSet",
+ ].concat(getArrayTypeNames());
+
+ if (!Array.isArray(parameters) || parameters.length === 0 || !parameters[0]) {
+ return false;
+ }
+
+ if (parameters[0].getGrip) {
+ return supportedClasses.includes(parameters[0].getGrip().class);
+ }
+
+ return supportedClasses.includes(parameters[0].class);
+}
+
+module.exports = {
+ getArrayTypeNames,
+ isSupportedByConsoleTable,
+};
diff --git a/devtools/shared/webconsole/moz.build b/devtools/shared/webconsole/moz.build
new file mode 100644
index 0000000000..a15db593b5
--- /dev/null
+++ b/devtools/shared/webconsole/moz.build
@@ -0,0 +1,33 @@
+# -*- 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/.
+
+if CONFIG["OS_TARGET"] != "Android":
+ MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.toml"]
+ XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
+ BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+
+# Compute where to put transpiled files into omni.ja package
+# All DevTools modules are used via resource://devtools/ URI
+# See devtools/shared/jar.mn for how this resource is mapped into jar package.
+base = FINAL_TARGET_FILES.chrome.devtools.modules
+
+# Now, navigate to the right sub-directory into devtools root modules folder
+for dir in RELATIVEDIR.split("/"):
+ base = base[dir]
+base += ["!reserved-js-words.js"]
+
+GeneratedFile(
+ "reserved-js-words.js",
+ script="GenerateReservedWordsJS.py",
+ inputs=["/js/src/frontend/ReservedWords.h"],
+)
+
+DevToolsModules(
+ "analyze-input-string.js",
+ "js-property-provider.js",
+ "messages.js",
+ "parser-helper.js",
+)
diff --git a/devtools/shared/webconsole/parser-helper.js b/devtools/shared/webconsole/parser-helper.js
new file mode 100644
index 0000000000..69981781b5
--- /dev/null
+++ b/devtools/shared/webconsole/parser-helper.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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ Reflect: "resource://gre/modules/reflect.sys.mjs",
+});
+
+/**
+ * Gets a collection of parser methods for a specified source.
+ *
+ * @param string source
+ * The source text content.
+ * @param boolean logExceptions
+ */
+function getSyntaxTrees(source, logExceptions) {
+ // The source may not necessarily be JS, in which case we need to extract
+ // all the scripts. Fastest/easiest way is with a regular expression.
+ // Don't worry, the rules of using a <script> tag are really strict,
+ // this will work.
+ const regexp = /<script[^>]*?(?:>([^]*?)<\/script\s*>|\/>)/gim;
+ const syntaxTrees = [];
+ const scriptMatches = [];
+ let scriptMatch;
+
+ if (source.match(/^\s*</)) {
+ // First non whitespace character is &lt, so most definitely HTML.
+ while ((scriptMatch = regexp.exec(source))) {
+ // Contents are captured at index 1 or nothing: Self-closing scripts
+ // won't capture code content
+ scriptMatches.push(scriptMatch[1] || "");
+ }
+ }
+
+ // If there are no script matches, send the whole source directly to the
+ // reflection API to generate the AST nodes.
+ if (!scriptMatches.length) {
+ // Reflect.parse throws when encounters a syntax error.
+ try {
+ syntaxTrees.push(lazy.Reflect.parse(source));
+ } catch (e) {
+ if (logExceptions) {
+ DevToolsUtils.reportException("Parser:get", e);
+ }
+ }
+ } else {
+ // Generate the AST nodes for each script.
+ for (const script of scriptMatches) {
+ // Reflect.parse throws when encounters a syntax error.
+ try {
+ syntaxTrees.push(lazy.Reflect.parse(script));
+ } catch (e) {
+ if (logExceptions) {
+ DevToolsUtils.reportException("Parser:get", e);
+ }
+ }
+ }
+ }
+
+ return syntaxTrees;
+}
+
+exports.getSyntaxTrees = getSyntaxTrees;
diff --git a/devtools/shared/webconsole/test/browser/browser.toml b/devtools/shared/webconsole/test/browser/browser.toml
new file mode 100644
index 0000000000..31c82d6307
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/browser.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "head.js",
+ "data.json",
+ "data.json^headers^",
+ "network_requests_iframe.html",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+]
+
+["browser_commands_registration.js"]
+
+["browser_network_longstring.js"]
diff --git a/devtools/shared/webconsole/test/browser/browser_commands_registration.js b/devtools/shared/webconsole/test/browser/browser_commands_registration.js
new file mode 100644
index 0000000000..e2c128df57
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/browser_commands_registration.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for Web Console commands registration.
+
+add_task(async function () {
+ const tab = await addTab("data:text/html,<div id=quack></div>");
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ // Fetch WebConsoleCommandsManager so that it is available for next Content Tasks
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ WebConsoleCommandsManager,
+ } = require("resource://devtools/server/actors/webconsole/commands/manager.js");
+
+ // Bind the symbol on this in order to make it available for next tasks
+ this.WebConsoleCommandsManager = WebConsoleCommandsManager;
+ });
+
+ await registerNewCommand(commands);
+ await registerAccessor(commands);
+});
+
+async function evaluateJSAndCheckResult(commands, input, expected) {
+ const response = await commands.scriptCommand.execute(input);
+ checkObject(response, expected);
+}
+
+async function registerNewCommand(commands) {
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ this.WebConsoleCommandsManager.register({
+ name: "setFoo",
+ isSideEffectFree: false,
+ command(owner, value) {
+ owner.window.foo = value;
+ return "ok";
+ },
+ });
+ });
+
+ const command = "setFoo('bar')";
+ await evaluateJSAndCheckResult(commands, command, {
+ input: command,
+ result: "ok",
+ });
+
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ is(content.top.foo, "bar", "top.foo should equal to 'bar'");
+ });
+}
+
+async function registerAccessor(commands) {
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ this.WebConsoleCommandsManager.register({
+ name: "$foo",
+ isSideEffectFree: true,
+ command: {
+ get(owner) {
+ const foo = owner.window.document.getElementById("quack");
+ return owner.makeDebuggeeValue(foo);
+ },
+ },
+ });
+ });
+
+ const command = "$foo.textContent = '>o_/'";
+ await evaluateJSAndCheckResult(commands, command, {
+ input: command,
+ result: ">o_/",
+ });
+
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ is(
+ content.document.getElementById("quack").textContent,
+ ">o_/",
+ '#foo textContent should equal to ">o_/"'
+ );
+ });
+}
diff --git a/devtools/shared/webconsole/test/browser/browser_network_longstring.js b/devtools/shared/webconsole/test/browser/browser_network_longstring.js
new file mode 100644
index 0000000000..6cb1850848
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/browser_network_longstring.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the network actor uses the LongStringActor
+
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const LONG_STRING_LENGTH = 400;
+const LONG_STRING_INITIAL_LENGTH = 400;
+let ORIGINAL_LONG_STRING_LENGTH, ORIGINAL_LONG_STRING_INITIAL_LENGTH;
+
+add_task(async function () {
+ const tab = await addTab(URL_ROOT + "network_requests_iframe.html");
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ // Override the default long string settings to lower values.
+ // This is done from the parent process's DevToolsServer as the LongString
+ // actor is being created from the parent process as network requests are
+ // watched from the parent process.
+ ORIGINAL_LONG_STRING_LENGTH = DevToolsServer.LONG_STRING_LENGTH;
+ ORIGINAL_LONG_STRING_INITIAL_LENGTH =
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH;
+
+ DevToolsServer.LONG_STRING_LENGTH = LONG_STRING_LENGTH;
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH = LONG_STRING_INITIAL_LENGTH;
+
+ info("test network POST request");
+ const networkResource = await new Promise(resolve => {
+ commands.resourceCommand
+ .watchResources([commands.resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable: () => {},
+ onUpdated: resourceUpdate => {
+ resolve(resourceUpdate[0].resource);
+ },
+ })
+ .then(() => {
+ // Spawn the network request after we started watching
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.wrappedJSObject.testXhrPost();
+ });
+ });
+ });
+
+ const netActor = networkResource.actor;
+ ok(netActor, "We have a netActor:" + netActor);
+
+ const { client } = commands;
+ const requestHeaders = await client.request({
+ to: netActor,
+ type: "getRequestHeaders",
+ });
+ assertRequestHeaders(requestHeaders);
+ const requestCookies = await client.request({
+ to: netActor,
+ type: "getRequestCookies",
+ });
+ assertRequestCookies(requestCookies);
+ const requestPostData = await client.request({
+ to: netActor,
+ type: "getRequestPostData",
+ });
+ assertRequestPostData(requestPostData);
+ const responseHeaders = await client.request({
+ to: netActor,
+ type: "getResponseHeaders",
+ });
+ assertResponseHeaders(responseHeaders);
+ const responseCookies = await client.request({
+ to: netActor,
+ type: "getResponseCookies",
+ });
+ assertResponseCookies(responseCookies);
+ const responseContent = await client.request({
+ to: netActor,
+ type: "getResponseContent",
+ });
+ assertResponseContent(responseContent);
+ const eventTimings = await client.request({
+ to: netActor,
+ type: "getEventTimings",
+ });
+ assertEventTimings(eventTimings);
+
+ await commands.destroy();
+
+ DevToolsServer.LONG_STRING_LENGTH = ORIGINAL_LONG_STRING_LENGTH;
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH =
+ ORIGINAL_LONG_STRING_INITIAL_LENGTH;
+});
+
+function assertRequestHeaders(response) {
+ info("checking request headers");
+
+ ok(!!response.headers.length, "request headers > 0");
+ Assert.greater(response.headersSize, 0, "request headersSize > 0");
+
+ checkHeadersOrCookies(response.headers, {
+ Referer: /network_requests_iframe\.html/,
+ Cookie: /bug768096/,
+ });
+}
+
+function assertRequestCookies(response) {
+ info("checking request cookies");
+
+ is(response.cookies.length, 3, "request cookies length");
+
+ checkHeadersOrCookies(response.cookies, {
+ foobar: "fooval",
+ omgfoo: "bug768096",
+ badcookie: "bug826798=st3fan",
+ });
+}
+
+function assertRequestPostData(response) {
+ info("checking request POST data");
+
+ checkObject(response, {
+ postData: {
+ text: {
+ type: "longString",
+ initial: /^Hello world! foobaz barr.+foobaz barrfo$/,
+ length: 563,
+ actor: /[a-z]/,
+ },
+ },
+ postDataDiscarded: false,
+ });
+
+ is(
+ response.postData.text.initial.length,
+ LONG_STRING_INITIAL_LENGTH,
+ "postData text initial length"
+ );
+}
+
+function assertResponseHeaders(response) {
+ info("checking response headers");
+
+ ok(!!response.headers.length, "response headers > 0");
+ Assert.greater(response.headersSize, 0, "response headersSize > 0");
+
+ checkHeadersOrCookies(response.headers, {
+ "content-type": /^application\/(json|octet-stream)$/,
+ "content-length": /^\d+$/,
+ "x-very-short": "hello world",
+ "x-very-long": {
+ type: "longString",
+ length: 521,
+ initial: /^Lorem ipsum.+\. Donec vitae d$/,
+ actor: /[a-z]/,
+ },
+ });
+}
+
+function assertResponseCookies(response) {
+ info("checking response cookies");
+
+ is(response.cookies.length, 0, "response cookies length");
+}
+
+function assertResponseContent(response) {
+ info("checking response content");
+
+ checkObject(response, {
+ content: {
+ text: {
+ type: "longString",
+ initial: /^\{ id: "test JSON data"(.|\r|\n)+ barfoo ba$/g,
+ length: 1070,
+ actor: /[a-z]/,
+ },
+ },
+ contentDiscarded: false,
+ });
+
+ is(
+ response.content.text.initial.length,
+ LONG_STRING_INITIAL_LENGTH,
+ "content initial length"
+ );
+}
+
+function assertEventTimings(response) {
+ info("checking event timings");
+
+ checkObject(response, {
+ timings: {
+ blocked: /^-1|\d+$/,
+ dns: /^-1|\d+$/,
+ connect: /^-1|\d+$/,
+ send: /^-1|\d+$/,
+ wait: /^-1|\d+$/,
+ receive: /^-1|\d+$/,
+ },
+ totalTime: /^\d+$/,
+ });
+}
diff --git a/devtools/shared/webconsole/test/browser/data.json b/devtools/shared/webconsole/test/browser/data.json
new file mode 100644
index 0000000000..d46085c124
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/data.json
@@ -0,0 +1,3 @@
+{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ],
+ veryLong: "foo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo bar"
+}
diff --git a/devtools/shared/webconsole/test/browser/data.json^headers^ b/devtools/shared/webconsole/test/browser/data.json^headers^
new file mode 100644
index 0000000000..bb6b45500f
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/data.json^headers^
@@ -0,0 +1,3 @@
+Content-Type: application/json
+x-very-long: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse a ipsum massa. Phasellus at elit dictum libero laoreet sagittis. Phasellus condimentum ultricies imperdiet. Nam eu ligula justo, ut tincidunt quam. Etiam sollicitudin, tortor sed egestas blandit, sapien sem tincidunt nulla, eu luctus libero odio quis leo. Nam elit massa, mattis quis blandit ac, facilisis vitae arcu. Donec vitae dictum neque. Proin ornare nisl at lectus commodo iaculis eget eget est. Quisque scelerisque vestibulum quam sed interdum.
+x-very-short: hello world
diff --git a/devtools/shared/webconsole/test/browser/head.js b/devtools/shared/webconsole/test/browser/head.js
new file mode 100644
index 0000000000..cdfc7ab59a
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/head.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";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+function checkObject(object, expected) {
+ for (const name of Object.keys(expected)) {
+ const expectedValue = expected[name];
+ const value = object[name];
+ checkValue(name, value, expectedValue);
+ }
+}
+
+function checkValue(name, value, expected) {
+ if (expected === null) {
+ is(value, null, "'" + name + "' is null");
+ } else if (value === null) {
+ ok(false, "'" + name + "' is null");
+ } else if (value === undefined) {
+ ok(false, "'" + name + "' is undefined");
+ } else if (
+ typeof expected == "string" ||
+ typeof expected == "number" ||
+ typeof expected == "boolean"
+ ) {
+ is(value, expected, "property '" + name + "'");
+ } else if (expected instanceof RegExp) {
+ ok(expected.test(value), name + ": " + expected + " matched " + value);
+ } else if (Array.isArray(expected)) {
+ info("checking array for property '" + name + "'");
+ checkObject(value, expected);
+ } else if (typeof expected == "object") {
+ info("checking object for property '" + name + "'");
+ checkObject(value, expected);
+ }
+}
+
+function checkHeadersOrCookies(array, expected) {
+ const foundHeaders = {};
+
+ for (const elem of array) {
+ if (!(elem.name in expected)) {
+ continue;
+ }
+ foundHeaders[elem.name] = true;
+ info("checking value of header " + elem.name);
+ checkValue(elem.name, elem.value, expected[elem.name]);
+ }
+
+ for (const header in expected) {
+ if (!(header in foundHeaders)) {
+ ok(false, header + " was not found");
+ }
+ }
+}
diff --git a/devtools/shared/webconsole/test/browser/network_requests_iframe.html b/devtools/shared/webconsole/test/browser/network_requests_iframe.html
new file mode 100644
index 0000000000..4e96364b06
--- /dev/null
+++ b/devtools/shared/webconsole/test/browser/network_requests_iframe.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Console HTTP test page</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript"><!--
+ "use strict";
+ let setAllowAllCookies = false;
+
+ function makeXhr(method, url, requestBody, callback) {
+ // On the first call, allow all cookies and set cookies, then resume the actual test
+ if (!setAllowAllCookies) {
+ SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]},
+ function() {
+ setAllowAllCookies = true;
+ setCookies();
+ makeXhrCallback(method, url, requestBody, callback);
+ });
+ } else {
+ makeXhrCallback(method, url, requestBody, callback);
+ }
+ }
+
+ function makeXhrCallback(method, url, requestBody, callback) {
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open(method, url, true);
+ if (callback) {
+ xmlhttp.onreadystatechange = function() {
+ if (xmlhttp.readyState == 4) {
+ callback();
+ }
+ };
+ }
+ xmlhttp.send(requestBody);
+ }
+
+ /* exported testXhrGet */
+ function testXhrGet(callback) {
+ makeXhr("get", "data.json", null, callback);
+ }
+
+ /* exported testXhrPost */
+ function testXhrPost(callback) {
+ const body = "Hello world! " + "foobaz barr".repeat(50);
+ makeXhr("post", "data.json", body, callback);
+ }
+
+ function setCookies() {
+ document.cookie = "foobar=fooval";
+ document.cookie = "omgfoo=bug768096";
+ document.cookie = "badcookie=bug826798=st3fan";
+ }
+ </script>
+ </head>
+ <body>
+ <h1>Web Console HTTP Logging Testpage</h1>
+ <h2>This page is used to test the HTTP logging.</h2>
+
+ <form action="?" method="post">
+ <input name="name" type="text" value="foo bar"><br>
+ <input name="age" type="text" value="144"><br>
+ </form>
+ </body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/chrome.toml b/devtools/shared/webconsole/test/chrome/chrome.toml
new file mode 100644
index 0000000000..629d3c7d5b
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/chrome.toml
@@ -0,0 +1,57 @@
+[DEFAULT]
+tags = "devtools"
+support-files = [
+ "common.js",
+ "data.json",
+ "data.json^headers^",
+ "helper_serviceworker.js",
+ "network_requests_iframe.html",
+ "sandboxed_iframe.html",
+ "console-test-worker.js",
+ "!/browser/base/content/test/general/browser_star_hsts.sjs",
+]
+
+["test_basics.html"]
+
+["test_cached_messages.html"]
+
+["test_console_assert.html"]
+
+["test_console_group_styling.html"]
+
+["test_console_serviceworker.html"]
+skip-if = ["release_or_beta"] # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled
+
+["test_console_serviceworker_cached.html"]
+skip-if = ["release_or_beta"] # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled
+
+["test_console_styling.html"]
+
+["test_console_timestamp.html"]
+
+["test_console_worker.html"]
+
+["test_consoleapi.html"]
+
+["test_consoleapi_innerID.html"]
+
+["test_file_uri.html"]
+
+["test_jsterm_autocomplete.html"]
+
+["test_network_get.html"]
+skip-if = ["verify"]
+
+["test_network_post.html"]
+
+["test_network_security-hsts.html"]
+
+["test_nsiconsolemessage.html"]
+
+["test_object_actor.html"]
+
+["test_object_actor_native_getters.html"]
+
+["test_object_actor_native_getters_lenient_this.html"]
+
+["test_page_errors.html"]
diff --git a/devtools/shared/webconsole/test/chrome/common.js b/devtools/shared/webconsole/test/chrome/common.js
new file mode 100644
index 0000000000..62878d7e60
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/common.js
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* exported attachConsole, attachConsoleToTab, attachConsoleToWorker,
+ closeDebugger, checkConsoleAPICalls, checkRawHeaders, runTests, nextTest, Ci, Cc,
+ withActiveServiceWorker, Services, consoleAPICall, createCommandsForTab, FRACTIONAL_NUMBER_REGEX, DevToolsServer */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+
+// timeStamp are the result of a number in microsecond divided by 1000.
+// so we can't expect a precise number of decimals, or even if there would
+// be decimals at all.
+const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;
+
+function attachConsole(listeners) {
+ return _attachConsole(listeners);
+}
+function attachConsoleToTab(listeners) {
+ return _attachConsole(listeners, true);
+}
+function attachConsoleToWorker(listeners) {
+ return _attachConsole(listeners, true, true);
+}
+
+var _attachConsole = async function (listeners, attachToTab, attachToWorker) {
+ try {
+ function waitForMessage(target) {
+ return new Promise(resolve => {
+ target.addEventListener("message", resolve, { once: true });
+ });
+ }
+
+ // Fetch the console actor out of the expected target
+ // ParentProcessTarget / WorkerTarget / FrameTarget
+ let commands, target, worker;
+ if (!attachToTab) {
+ commands = await CommandsFactory.forMainProcess();
+ target = await commands.descriptorFront.getTarget();
+ } else {
+ commands = await CommandsFactory.forCurrentTabInChromeMochitest();
+ // Descriptor's getTarget will only work if the TargetCommand watches for the first top target
+ await commands.targetCommand.startListening();
+ target = await commands.descriptorFront.getTarget();
+ if (attachToWorker) {
+ const workerName = "console-test-worker.js#" + new Date().getTime();
+ worker = new Worker(workerName);
+ await waitForMessage(worker);
+
+ const { workers } = await target.listWorkers();
+ target = workers.filter(w => w.url == workerName)[0];
+ if (!target) {
+ console.error(
+ "listWorkers failed. Unable to find the worker actor\n"
+ );
+ return null;
+ }
+ // This is still important to attach workers as target is still a descriptor front
+ // which "becomes" a target when calling this method:
+ await target.morphWorkerDescriptorIntoWorkerTarget();
+ }
+ }
+
+ // Attach the Target and the target thread in order to instantiate the console client.
+ await target.attachThread();
+
+ const webConsoleFront = await target.getFront("console");
+
+ // By default the console isn't listening for anything,
+ // request listeners from here
+ const response = await webConsoleFront.startListeners(listeners);
+ return {
+ state: {
+ dbgClient: commands.client,
+ webConsoleFront,
+ actor: webConsoleFront.actor,
+ // Keep a strong reference to the Worker to avoid it being
+ // GCd during the test (bug 1237492).
+ // eslint-disable-next-line camelcase
+ _worker_ref: worker,
+ },
+ response,
+ };
+ } catch (error) {
+ console.error(
+ `attachConsole failed: ${error.error} ${error.message} - ` + error.stack
+ );
+ }
+ return null;
+};
+
+async function createCommandsForTab() {
+ const commands = await CommandsFactory.forMainProcess();
+ await commands.targetCommand.startListening();
+ return commands;
+}
+
+function closeDebugger(state, callback) {
+ const onClose = state.dbgClient.close();
+
+ state.dbgClient = null;
+ state.client = null;
+
+ if (typeof callback === "function") {
+ onClose.then(callback);
+ }
+ return onClose;
+}
+
+function checkConsoleAPICalls(consoleCalls, expectedConsoleCalls) {
+ is(
+ consoleCalls.length,
+ expectedConsoleCalls.length,
+ "received correct number of console calls"
+ );
+ expectedConsoleCalls.forEach(function (message, index) {
+ info("checking received console call #" + index);
+ checkConsoleAPICall(consoleCalls[index], expectedConsoleCalls[index]);
+ });
+}
+
+function checkConsoleAPICall(call, expected) {
+ is(
+ call.arguments?.length || 0,
+ expected.arguments?.length || 0,
+ "number of arguments"
+ );
+
+ checkObject(call, expected);
+}
+
+function checkObject(object, expected) {
+ if (object && object.getGrip) {
+ object = object.getGrip();
+ }
+
+ for (const name of Object.keys(expected)) {
+ const expectedValue = expected[name];
+ const value = object[name];
+ checkValue(name, value, expectedValue);
+ }
+}
+
+function checkValue(name, value, expected) {
+ if (expected === null) {
+ ok(!value, "'" + name + "' is null");
+ } else if (value === undefined) {
+ ok(false, "'" + name + "' is undefined");
+ } else if (value === null) {
+ ok(false, "'" + name + "' is null");
+ } else if (
+ typeof expected == "string" ||
+ typeof expected == "number" ||
+ typeof expected == "boolean"
+ ) {
+ is(value, expected, "property '" + name + "'");
+ } else if (expected instanceof RegExp) {
+ ok(expected.test(value), name + ": " + expected + " matched " + value);
+ } else if (Array.isArray(expected)) {
+ info("checking array for property '" + name + "'");
+ checkObject(value, expected);
+ } else if (typeof expected == "object") {
+ info("checking object for property '" + name + "'");
+ checkObject(value, expected);
+ }
+}
+
+function checkHeadersOrCookies(array, expected) {
+ const foundHeaders = {};
+
+ for (const elem of array) {
+ if (!(elem.name in expected)) {
+ continue;
+ }
+ foundHeaders[elem.name] = true;
+ info("checking value of header " + elem.name);
+ checkValue(elem.name, elem.value, expected[elem.name]);
+ }
+
+ for (const header in expected) {
+ if (!(header in foundHeaders)) {
+ ok(false, header + " was not found");
+ }
+ }
+}
+
+function checkRawHeaders(text, expected) {
+ const headers = text.split(/\r\n|\n|\r/);
+ const arr = [];
+ for (const header of headers) {
+ const index = header.indexOf(": ");
+ if (index < 0) {
+ continue;
+ }
+ arr.push({
+ name: header.substr(0, index),
+ value: header.substr(index + 2),
+ });
+ }
+
+ checkHeadersOrCookies(arr, expected);
+}
+
+var gTestState = {};
+
+function runTests(tests, endCallback) {
+ function* driver() {
+ let lastResult, sendToNext;
+ for (let i = 0; i < tests.length; i++) {
+ gTestState.index = i;
+ const fn = tests[i];
+ info("will run test #" + i + ": " + fn.name);
+ lastResult = fn(sendToNext, lastResult);
+ sendToNext = yield lastResult;
+ }
+ yield endCallback(sendToNext, lastResult);
+ }
+ gTestState.driver = driver();
+ return gTestState.driver.next();
+}
+
+function nextTest(message) {
+ return gTestState.driver.next(message);
+}
+
+function withActiveServiceWorker(win, url, scope) {
+ const opts = {};
+ if (scope) {
+ opts.scope = scope;
+ }
+ return win.navigator.serviceWorker.register(url, opts).then(swr => {
+ if (swr.active) {
+ return swr;
+ }
+
+ // Unfortunately we can't just use navigator.serviceWorker.ready promise
+ // here. If the service worker is for a scope that does not cover the window
+ // then the ready promise will never resolve. Instead monitor the service
+ // workers state change events to determine when its activated.
+ return new Promise(resolve => {
+ const sw = swr.waiting || swr.installing;
+ sw.addEventListener("statechange", function stateHandler(evt) {
+ if (sw.state === "activated") {
+ sw.removeEventListener("statechange", stateHandler);
+ resolve(swr);
+ }
+ });
+ });
+ });
+}
+
+/**
+ *
+ * @param {Front} consoleFront
+ * @param {Function} consoleCall: A function which calls the consoleAPI, e.g. :
+ * `() => top.console.log("test")`.
+ * @returns {Promise} A promise that will be resolved with the packet sent by the server
+ * in response to the consoleAPI call.
+ */
+function consoleAPICall(consoleFront, consoleCall) {
+ const onConsoleAPICall = consoleFront.once("consoleAPICall");
+ consoleCall();
+ return onConsoleAPICall;
+}
diff --git a/devtools/shared/webconsole/test/chrome/console-test-worker.js b/devtools/shared/webconsole/test/chrome/console-test-worker.js
new file mode 100644
index 0000000000..9e92f6af65
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/console-test-worker.js
@@ -0,0 +1,21 @@
+"use strict";
+
+console.log("Log from worker init");
+
+function f() {
+ const a = 1;
+ const b = 2;
+ const c = 3;
+ return { a, b, c };
+}
+
+self.onmessage = function (event) {
+ if (event.data == "ping") {
+ f();
+ postMessage("pong");
+ } else if (event.data?.type == "log") {
+ console.log(event.data.message);
+ }
+};
+
+postMessage("load");
diff --git a/devtools/shared/webconsole/test/chrome/data.json b/devtools/shared/webconsole/test/chrome/data.json
new file mode 100644
index 0000000000..eca9d0e796
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/data.json
@@ -0,0 +1,5 @@
+{
+ "id": "test JSON data",
+ "myArray": ["foo", "bar", "baz", "biff"],
+ "veryLong": "foo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo bar"
+}
diff --git a/devtools/shared/webconsole/test/chrome/data.json^headers^ b/devtools/shared/webconsole/test/chrome/data.json^headers^
new file mode 100644
index 0000000000..bb6b45500f
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/data.json^headers^
@@ -0,0 +1,3 @@
+Content-Type: application/json
+x-very-long: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse a ipsum massa. Phasellus at elit dictum libero laoreet sagittis. Phasellus condimentum ultricies imperdiet. Nam eu ligula justo, ut tincidunt quam. Etiam sollicitudin, tortor sed egestas blandit, sapien sem tincidunt nulla, eu luctus libero odio quis leo. Nam elit massa, mattis quis blandit ac, facilisis vitae arcu. Donec vitae dictum neque. Proin ornare nisl at lectus commodo iaculis eget eget est. Quisque scelerisque vestibulum quam sed interdum.
+x-very-short: hello world
diff --git a/devtools/shared/webconsole/test/chrome/helper_serviceworker.js b/devtools/shared/webconsole/test/chrome/helper_serviceworker.js
new file mode 100644
index 0000000000..81b92a6ddb
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/helper_serviceworker.js
@@ -0,0 +1,22 @@
+"use strict";
+
+console.log("script evaluation");
+console.log("Here is a SAB", new SharedArrayBuffer(1024));
+
+addEventListener("install", function (evt) {
+ console.log("install event");
+});
+
+addEventListener("activate", function (evt) {
+ console.log("activate event");
+});
+
+addEventListener("fetch", function (evt) {
+ console.log("fetch event: " + evt.request.url);
+ evt.respondWith(new Response("Hello world"));
+});
+
+addEventListener("message", function (evt) {
+ console.log("message event: " + evt.data.message);
+ evt.source.postMessage({ type: "PONG" });
+});
diff --git a/devtools/shared/webconsole/test/chrome/network_requests_iframe.html b/devtools/shared/webconsole/test/chrome/network_requests_iframe.html
new file mode 100644
index 0000000000..6bb806b904
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/network_requests_iframe.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Console HTTP test page</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript"><!--
+ "use strict";
+ let setAllowAllCookies = false;
+
+ function makeXhr(method, url, requestBody, callback) {
+ // On the first call, allow all cookies and set cookies, then resume the actual test
+ if (!setAllowAllCookies) {
+ SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]},
+ function() {
+ setAllowAllCookies = true;
+ setCookies();
+ makeXhrCallback(method, url, requestBody, callback);
+ });
+ } else {
+ makeXhrCallback(method, url, requestBody, callback);
+ }
+ }
+
+ function makeXhrCallback(method, url, requestBody, callback) {
+ const xmlhttp = new XMLHttpRequest();
+ xmlhttp.open(method, url, true);
+ if (callback) {
+ xmlhttp.onreadystatechange = function() {
+ if (xmlhttp.readyState == 4) {
+ callback();
+ }
+ };
+ }
+ xmlhttp.send(requestBody);
+ }
+
+ /* exported testXhrGet */
+ function testXhrGet(callback, url = "data.json") {
+ makeXhr("get", url, null, callback);
+ }
+
+ /* exported testXhrPost */
+ function testXhrPost(callback) {
+ const body = "Hello world! " + (new Array(50)).join("foobaz barr");
+ makeXhr("post", "data.json", body, callback);
+ }
+
+ function setCookies() {
+ document.cookie = "foobar=fooval";
+ document.cookie = "omgfoo=bug768096";
+ document.cookie = "badcookie=bug826798=st3fan";
+ }
+ </script>
+ </head>
+ <body>
+ <h1>Web Console HTTP Logging Testpage</h1>
+ <h2>This page is used to test the HTTP logging.</h2>
+
+ <form action="?" method="post">
+ <input name="name" type="text" value="foo bar"><br>
+ <input name="age" type="text" value="144"><br>
+ </form>
+ </body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/sandboxed_iframe.html b/devtools/shared/webconsole/test/chrome/sandboxed_iframe.html
new file mode 100644
index 0000000000..55a6224b50
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/sandboxed_iframe.html
@@ -0,0 +1,8 @@
+<html>
+<head><title>Sandboxed iframe</title></head>
+<body>
+ <iframe id="sandboxed-iframe"
+ sandbox="allow-scripts"
+ srcdoc="<script>var foobarObject = {bug1051224: 'sandboxed'};</script>"></iframe>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_basics.html b/devtools/shared/webconsole/test/chrome/test_basics.html
new file mode 100644
index 0000000000..761a97bc8d
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_basics.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Basic Web Console Actor tests</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Basic Web Console Actor tests</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+async function startTest()
+{
+ removeEventListener("load", startTest);
+
+ let {state, response} = await attachConsoleToTab(["PageError"]);
+ is(response.startedListeners.length, 1, "startedListeners.length");
+ is(response.startedListeners[0], "PageError", "startedListeners: PageError");
+
+ await closeDebugger(state);
+ top.console_ = top.console;
+ top.console = { lolz: "foo" };
+ ({state, response} = await attachConsoleToTab(["PageError", "ConsoleAPI", "foo"]));
+
+ const startedListeners = response.startedListeners;
+ is(startedListeners.length, 2, "startedListeners.length");
+ isnot(startedListeners.indexOf("PageError"), -1, "startedListeners: PageError");
+ isnot(startedListeners.indexOf("ConsoleAPI"), -1,
+ "startedListeners: ConsoleAPI");
+ is(startedListeners.indexOf("foo"), -1, "startedListeners: no foo");
+
+ top.console = top.console_;
+ response = await state.webConsoleFront.stopListeners(["ConsoleAPI", "foo"]);
+
+ is(response.stoppedListeners.length, 1, "stoppedListeners.length");
+ is(response.stoppedListeners[0], "ConsoleAPI", "stoppedListeners: ConsoleAPI");
+ await closeDebugger(state);
+ ({state, response} = await attachConsoleToTab(["ConsoleAPI"]));
+
+ is(response.startedListeners.length, 1, "startedListeners.length");
+ is(response.startedListeners[0], "ConsoleAPI", "startedListeners: ConsoleAPI");
+
+ top.console = top.console_;
+ delete top.console_;
+
+ closeDebugger(state, function() {
+ SimpleTest.finish();
+ });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_cached_messages.html b/devtools/shared/webconsole/test/chrome/test_cached_messages.html
new file mode 100644
index 0000000000..12d5069c7d
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_cached_messages.html
@@ -0,0 +1,217 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for cached messages</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for cached messages</p>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+const { MESSAGE_CATEGORY } = require("devtools/shared/constants");
+
+const previousEnabled = window.docShell.cssErrorReportingEnabled;
+window.docShell.cssErrorReportingEnabled = true;
+
+SimpleTest.registerCleanupFunction(() => {
+ window.docShell.cssErrorReportingEnabled = previousEnabled;
+});
+
+var ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Ci.nsIConsoleAPIStorage);
+let expectedConsoleCalls = [];
+let expectedPageErrors = [];
+
+function doPageErrors() {
+ Services.console.reset();
+
+ expectedPageErrors = [
+ {
+ errorMessage: /fooColor/,
+ sourceName: /.+/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ },
+ {
+ errorMessage: /doTheImpossible/,
+ sourceName: /.+/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ ];
+
+ let container = document.createElement("script");
+ document.body.appendChild(container);
+ container.textContent = "document.body.style.color = 'fooColor';";
+ document.body.removeChild(container);
+
+ SimpleTest.expectUncaughtException();
+
+ container = document.createElement("script");
+ document.body.appendChild(container);
+ container.textContent = "document.doTheImpossible();";
+ document.body.removeChild(container);
+}
+
+function doConsoleCalls() {
+ ConsoleAPIStorage.clearEvents();
+
+ top.console.log("foobarBaz-log", undefined);
+ top.console.info("foobarBaz-info", null);
+ top.console.warn("foobarBaz-warn", document.body);
+
+ expectedConsoleCalls = [
+ {
+ level: "log",
+ filename: /test_cached_messages/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ level: "info",
+ filename: /test_cached_messages/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ level: "warn",
+ filename: /test_cached_messages/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ ];
+}
+</script>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+var {ConsoleServiceListener} =
+ require("devtools/server/actors/webconsole/listeners/console-service");
+var {ConsoleAPIListener} =
+ require("devtools/server/actors/webconsole/listeners/console-api");
+
+let consoleAPIListener, consoleServiceListener;
+let consoleAPICalls = 0;
+let pageErrors = 0;
+
+async function onConsoleAPICall(message) {
+ for (const msg of expectedConsoleCalls) {
+ if (msg.functionName == message.functionName &&
+ msg.filename.test(message.filename)) {
+ consoleAPICalls++;
+ break;
+ }
+ }
+ if (consoleAPICalls == expectedConsoleCalls.length) {
+ await checkConsoleAPICache();
+ }
+}
+
+async function onConsoleServiceMessage(message) {
+ if (!(message instanceof Ci.nsIScriptError)) {
+ return;
+ }
+ for (const msg of expectedPageErrors) {
+ if (msg.category == message.category &&
+ msg.errorMessage.test(message.errorMessage)) {
+ pageErrors++;
+ break;
+ }
+ }
+ if (pageErrors == expectedPageErrors.length) {
+ await testPageErrors();
+ }
+}
+
+function startTest() {
+ removeEventListener("load", startTest);
+
+ consoleAPIListener = new ConsoleAPIListener(top, onConsoleAPICall);
+ consoleAPIListener.init();
+
+ doConsoleCalls();
+}
+
+async function checkConsoleAPICache() {
+ consoleAPIListener.destroy();
+ consoleAPIListener = null;
+ const {state} = await attachConsole(["ConsoleAPI"]);
+ const response = await state.webConsoleFront.getCachedMessages(["ConsoleAPI"]);
+ onCachedConsoleAPI(state, response);
+}
+
+function onCachedConsoleAPI(state, response) {
+ const msgs = response.messages;
+ info("cached console messages: " + msgs.length);
+
+ ok(msgs.length >= expectedConsoleCalls.length,
+ "number of cached console messages");
+
+ for (const {message} of msgs) {
+ for (const expected of expectedConsoleCalls) {
+ if (expected.filename.test(message.filename)) {
+ expectedConsoleCalls.splice(expectedConsoleCalls.indexOf(expected));
+ checkConsoleAPICall(message, expected);
+ break;
+ }
+ }
+ }
+
+ is(expectedConsoleCalls.length, 0, "all expected messages have been found");
+
+ closeDebugger(state, function() {
+ consoleServiceListener = new ConsoleServiceListener(null, onConsoleServiceMessage);
+ consoleServiceListener.init();
+ doPageErrors();
+ });
+}
+
+async function testPageErrors() {
+ consoleServiceListener.destroy();
+ consoleServiceListener = null;
+ const {state} = await attachConsole(["PageError"]);
+ const response = await state.webConsoleFront.getCachedMessages(["PageError"]);
+ onCachedPageErrors(state, response);
+}
+
+function onCachedPageErrors(state, response) {
+ const msgs = response.messages;
+ info("cached page errors: " + msgs.length);
+
+ ok(msgs.length >= expectedPageErrors.length,
+ "number of cached page errors");
+
+ for (const {pageError} of msgs) {
+ for (const expected of expectedPageErrors) {
+ if (expected.category == pageError.category &&
+ expected.errorMessage.test(pageError.errorMessage)) {
+ expectedPageErrors.splice(expectedPageErrors.indexOf(expected));
+ checkObject(pageError, expected);
+ break;
+ }
+ }
+ }
+
+ is(expectedPageErrors.length, 0, "all expected messages have been found");
+
+ closeDebugger(state, function() {
+ SimpleTest.finish();
+ });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_assert.html b/devtools/shared/webconsole/test/chrome/test_console_assert.html
new file mode 100644
index 0000000000..f847d5f5d8
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_assert.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for console.group styling with %c</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+"use strict";
+
+window.onload = async function () {
+ SimpleTest.waitForExplicitFinish();
+ let state;
+ try {
+ state = (await attachConsole(["ConsoleAPI"])).state;
+ const {webConsoleFront} = state;
+
+ await testFalseAssert(webConsoleFront);
+ await testFalsyAssert(webConsoleFront);
+ await testUndefinedAssert(webConsoleFront);
+ await testNullAssert(webConsoleFront);
+ await testTrueAssert(webConsoleFront);
+
+ } catch (e) {
+ ok(false, `Error thrown: ${e.message}`);
+ }
+
+ await closeDebugger(state);
+ SimpleTest.finish();
+};
+
+async function testFalseAssert(consoleFront) {
+ info(`Testing console.assert(false)`);
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.assert(false, "assertion is false")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["assertion is false"]
+ });
+}
+
+async function testFalsyAssert(consoleFront) {
+ info(`Testing console.assert(0")`);
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.assert(0, "assertion is false")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["assertion is false"]
+ });
+}
+
+async function testUndefinedAssert(consoleFront) {
+ info(`Testing console.assert(undefined)`);
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.assert(undefined, "assertion is false")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["assertion is false"]
+ });
+}
+
+async function testNullAssert(consoleFront) {
+ info(`Testing console.assert(null)`);
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.assert(null, "assertion is false")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["assertion is false"]
+ });
+}
+
+async function testTrueAssert(consoleFront) {
+ info(`Testing console.assert(true)`);
+ const onConsoleApiCall = consoleAPICall(
+ consoleFront,
+ () => top.console.assert(true, "assertion is false")
+ );
+
+ const TIMEOUT = Symbol();
+ const onTimeout = new Promise(resolve => setTimeout(() => resolve(TIMEOUT), 1000));
+
+ const res = await Promise.race([onConsoleApiCall, onTimeout]);
+ is(res, TIMEOUT,
+ "There was no consoleAPICall event in response to a truthy console.assert");
+}
+
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_group_styling.html b/devtools/shared/webconsole/test/chrome/test_console_group_styling.html
new file mode 100644
index 0000000000..23a93f8b8d
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_group_styling.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for console.group styling with %c</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+"use strict";
+
+window.onload = async function () {
+ SimpleTest.waitForExplicitFinish();
+ let state;
+ try {
+ state = (await attachConsole(["ConsoleAPI"])).state
+ const consoleFront = state.webConsoleFront;
+
+ await testSingleCustomStyleGroup(consoleFront);
+ await testSingleCustomStyleGroupCollapsed(consoleFront);
+ await testMultipleCustomStyleGroup(consoleFront);
+ await testMultipleCustomStyleGroupCollapsed(consoleFront);
+ await testFormatterWithNoStyleGroup(consoleFront);
+ await testFormatterWithNoStyleGroupCollapsed(consoleFront);
+ } catch (e) {
+ ok(false, `Error thrown: ${e.message}`);
+ }
+
+ await closeDebugger(state);
+ SimpleTest.finish();
+};
+
+async function testSingleCustomStyleGroup(consoleFront) {
+ info("Testing console.group with a custom style");
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.group("%cfoobar", "color:red")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["foobar"],
+ styles: ["color:red"]
+ });
+}
+
+async function testSingleCustomStyleGroupCollapsed(consoleFront) {
+ info("Testing console.groupCollapsed with a custom style");
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.groupCollapsed("%cfoobaz", "color:blue")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["foobaz"],
+ styles: ["color:blue"]
+ });
+}
+
+async function testMultipleCustomStyleGroup(consoleFront) {
+ info("Testing console.group with multiple custom styles");
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.group("%cfoo%cbar", "color:red", "color:blue")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["foo", "bar"],
+ styles: ["color:red", "color:blue"]
+ });
+}
+
+async function testMultipleCustomStyleGroupCollapsed(consoleFront) {
+ info("Testing console.groupCollapsed with multiple custom styles");
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.group("%cfoo%cbaz", "color:red", "color:green")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["foo", "baz"],
+ styles: ["color:red", "color:green"]
+ });
+}
+
+async function testFormatterWithNoStyleGroup(consoleFront) {
+ info("Testing console.group with one formatter but no style");
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.group("%cfoobar")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["%cfoobar"],
+ });
+ is(packet.message.styles, undefined, "No 'styles' property");
+}
+
+async function testFormatterWithNoStyleGroupCollapsed(consoleFront) {
+ info("Testing console.groupCollapsed with one formatter but no style");
+ const packet = await consoleAPICall(
+ consoleFront,
+ () => top.console.groupCollapsed("%cfoobaz")
+ );
+
+ checkConsoleAPICall(packet.message, {
+ arguments: ["%cfoobaz"],
+ });
+ is(packet.message.styles, undefined, "No 'styles' property");
+}
+
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_serviceworker.html b/devtools/shared/webconsole/test/chrome/test_console_serviceworker.html
new file mode 100644
index 0000000000..4cb6332abd
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_serviceworker.html
@@ -0,0 +1,212 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the Console API and Service Workers</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the Console API and Service Workers</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+// Utils functions
+function withFrame(url) {
+ return new Promise(resolve => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = function () {
+ resolve(iframe);
+ };
+ iframe.src = url;
+ document.body.appendChild(iframe);
+ });
+}
+
+function navigateFrame(iframe, url) {
+ return new Promise(resolve => {
+ iframe.onload = function () {
+ resolve(iframe);
+ };
+ iframe.src = url;
+ });
+}
+
+function forceReloadFrame(iframe) {
+ return new Promise(resolve => {
+ iframe.onload = function () {
+ resolve(iframe);
+ };
+ iframe.contentWindow.location.reload(true);
+ });
+}
+
+function messageServiceWorker(win, scope, message) {
+ return win.navigator.serviceWorker.getRegistration(scope).then(swr => {
+ return new Promise(resolve => {
+ win.navigator.serviceWorker.onmessage = evt => {
+ resolve();
+ };
+ const sw = swr.active || swr.waiting || swr.installing;
+ sw.postMessage({ type: "PING", message });
+ });
+ });
+}
+
+function unregisterServiceWorker(win) {
+ return win.navigator.serviceWorker.ready.then(swr => {
+ return swr.unregister();
+ });
+}
+
+// Test
+const BASE_URL = "https://example.com/chrome/devtools/shared/webconsole/test/chrome/";
+const SERVICE_WORKER_URL = BASE_URL + "helper_serviceworker.js";
+const SCOPE = BASE_URL + "foo/";
+const NONSCOPE_FRAME_URL = BASE_URL + "sandboxed_iframe.html";
+const SCOPE_FRAME_URL = SCOPE + "fake.html";
+const SCOPE_FRAME_URL2 = SCOPE + "whatsit.html";
+const MESSAGE = 'Tic Tock';
+
+const expectedConsoleCalls = [
+ {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['script evaluation'],
+ },
+ {
+ level: "log",
+ filename: /helper_serviceworker/,
+ // Note that the second argument isn't a SharedArrayBuffer, but a DevTools "Object Front" instance.
+ arguments: ['Here is a SAB', { class : "SharedArrayBuffer" }],
+ },
+ {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['install event'],
+ },
+ {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['activate event'],
+ },
+ {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['fetch event: ' + SCOPE_FRAME_URL],
+ },
+ {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['fetch event: ' + SCOPE_FRAME_URL2],
+ },
+];
+let consoleCalls = [];
+
+const startTest = async function () {
+ removeEventListener("load", startTest);
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["devtools.webconsole.filter.serviceworkers", true],
+ [
+ "dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled",
+ true
+ ],
+ ]}, resolve);
+ });
+
+ const {state, response} = await attachConsoleToTab(["ConsoleAPI"]);
+ onAttach(state, response);
+};
+addEventListener("load", startTest);
+
+const onAttach = async function (state, response) {
+ onConsoleAPICall = onConsoleAPICall.bind(null, state);
+ state.webConsoleFront.on("consoleAPICall", onConsoleAPICall);
+
+ let currentFrame;
+ try {
+ // First, we need a frame from which to register our script. This
+ // will not trigger any console calls.
+ info("Loading a non-scope frame from which to register a service worker.");
+ currentFrame = await withFrame(NONSCOPE_FRAME_URL);
+
+ // Now register the service worker and wait for it to become
+ // activate. This should trigger 3 console calls; 1 for script
+ // evaluation, 1 for the install event, and 1 for the activate
+ // event. These console calls are received because we called
+ // register(), not because we are in scope for the worker.
+ info("Registering the service worker");
+ await withActiveServiceWorker(currentFrame.contentWindow,
+ SERVICE_WORKER_URL, SCOPE);
+ ok(!currentFrame.contentWindow.navigator.serviceWorker.controller,
+ 'current frame should not be controlled');
+
+ // Now that the service worker is activate, lets navigate our frame.
+ // This will trigger 1 more console call for the fetch event.
+ info("Service worker registered. Navigating frame.");
+ await navigateFrame(currentFrame, SCOPE_FRAME_URL);
+ ok(currentFrame.contentWindow.navigator.serviceWorker.controller,
+ 'navigated frame should be controlled');
+
+ // We now have a controlled frame. Lets perform a non-navigation fetch.
+ // This should produce another console call for the fetch event.
+ info("Frame navigated. Calling fetch().");
+ await currentFrame.contentWindow.fetch(SCOPE_FRAME_URL2);
+
+ // Now force refresh our controlled frame. This will cause the frame
+ // to bypass the service worker and become an uncontrolled frame. It
+ // also happens to make the frame display a 404 message because the URL
+ // does not resolve to a real resource. This is ok, as we really only
+ // care about the frame being non-controlled, but still having a location
+ // that matches our service worker scope so we can provide its not
+ // incorrectly getting console calls.
+ info("Completed fetch(). Force refreshing to get uncontrolled frame.");
+ await forceReloadFrame(currentFrame);
+ ok(!currentFrame.contentWindow.navigator.serviceWorker.controller,
+ 'current frame should not be controlled after force refresh');
+ is(currentFrame.contentWindow.location.toString(), SCOPE_FRAME_URL,
+ 'current frame should still have in-scope location URL even though it got 404');
+
+ // Now postMessage() the service worker to trigger its message event
+ // handler. This will generate 1 or 2 to console.log() statements
+ // depending on if the worker thread needs to spin up again. In either
+ // case, though, we should not get any console calls because we don't
+ // have a controlled or registering document.
+ info("Completed force refresh. Messaging service worker.");
+ await messageServiceWorker(currentFrame.contentWindow, SCOPE, MESSAGE);
+
+ info("Done messaging service worker. Unregistering service worker.");
+ await unregisterServiceWorker(currentFrame.contentWindow);
+
+ info('Service worker unregistered. Checking console calls.');
+ state.webConsoleFront.off("consoleAPICall", onConsoleAPICall);
+ checkConsoleAPICalls(consoleCalls, expectedConsoleCalls);
+ } catch(error) {
+ ok(false, 'unexpected error: ' + error);
+ } finally {
+ if (currentFrame) {
+ currentFrame.remove();
+ currentFrame = null;
+ }
+ consoleCalls = [];
+ closeDebugger(state, function() {
+ SimpleTest.finish();
+ });
+ }
+};
+
+function onConsoleAPICall(state, packet) {
+ info("received message level: " + packet.message.level);
+ consoleCalls.push(packet.message);
+}
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_serviceworker_cached.html b/devtools/shared/webconsole/test/chrome/test_console_serviceworker_cached.html
new file mode 100644
index 0000000000..af9696844b
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_serviceworker_cached.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for getCachedMessages and Service Workers</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for getCachedMessages and Service Workers</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const BASE_URL = "https://example.com/chrome/devtools/shared/webconsole/test/chrome/";
+const SERVICE_WORKER_URL = BASE_URL + "helper_serviceworker.js";
+const FRAME_URL = BASE_URL + "sandboxed_iframe.html";
+
+const firstTabExpectedCalls = [
+ {
+ message: {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['script evaluation'],
+ }
+ }, {
+ message: {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['Here is a SAB'],
+ }
+ }, {
+ message: {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['install event'],
+ }
+ }, {
+ message: {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['activate event'],
+ }
+ },
+];
+
+const secondTabExpectedCalls = [
+ {
+ message: {
+ level: "log",
+ filename: /helper_serviceworker/,
+ arguments: ['fetch event: ' + FRAME_URL],
+ }
+ }
+];
+
+const startTest = async function () {
+ removeEventListener("load", startTest);
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["devtools.webconsole.filter.serviceworkers", true],
+ [
+ "dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled",
+ true
+ ],
+ ]}, resolve);
+ });
+
+ info("Adding a tab and attaching a service worker");
+ const tab1 = await addTab(FRAME_URL);
+ const swr = await withActiveServiceWorker(tab1.linkedBrowser.contentWindow,
+ SERVICE_WORKER_URL);
+
+ info("Attaching console to tab 1");
+ let {state} = await attachConsoleToTab(["ConsoleAPI"]);
+ let calls = await state.webConsoleFront.getCachedMessages(["ConsoleAPI"]);
+ checkConsoleAPICalls(calls.messages, firstTabExpectedCalls);
+ await closeDebugger(state);
+
+ // Because this tab is being added after the original messages happened,
+ // they shouldn't show up in a call to getCachedMessages.
+ // However, there is a fetch event which is logged due to loading the tab.
+ info("Adding a new tab at the same URL");
+
+ await addTab(FRAME_URL);
+ info("Attaching console to tab 2");
+ state = (await attachConsoleToTab(["ConsoleAPI"])).state;
+ calls = await state.webConsoleFront.getCachedMessages(["ConsoleAPI"]);
+ checkConsoleAPICalls(calls.messages, secondTabExpectedCalls);
+ await closeDebugger(state);
+
+ await swr.unregister();
+
+ SimpleTest.finish();
+};
+addEventListener("load", startTest);
+
+// This test needs to add tabs that are controlled by a service worker
+// so use some special powers to dig around and find gBrowser
+const {gBrowser} = SpecialPowers._getTopChromeWindow(SpecialPowers.window);
+
+SimpleTest.registerCleanupFunction(() => {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function addTab(url) {
+ info("Adding a new tab with URL: '" + url + "'");
+ return new Promise(resolve => {
+ const tab = gBrowser.selectedTab = gBrowser.addTab(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ gBrowser.selectedBrowser.addEventListener("load", function () {
+ info("URL '" + url + "' loading complete");
+ resolve(tab);
+ }, {capture: true, once: true});
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_styling.html b/devtools/shared/webconsole/test/chrome/test_console_styling.html
new file mode 100644
index 0000000000..841e19076f
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_styling.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for console.log styling with %c</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for console.log styling with %c</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+let expectedConsoleCalls = [];
+
+function doConsoleCalls(aState)
+{
+ top.console.log("%cOne formatter with no styles");
+ top.console.log("%cOne formatter", "color: red");
+ top.console.log("%cTwo formatters%cEach with an arg",
+ "color: red", "background: red");
+ top.console.log("%c%cTwo formatters next to each other",
+ "color: red", "background: red");
+ top.console.log("%c%c%cThree formatters next to each other",
+ "color: red", "background: red", "font-size: 150%");
+ top.console.log("%c%cTwo formatters%cWith a third separated",
+ "color: red", "background: red", "font-size: 150%");
+ top.console.log("%cOne formatter", "color: red",
+ "Second arg with no styles");
+ top.console.log("%cOne formatter", "color: red",
+ "%cSecond formatter is ignored", "background: blue")
+
+ expectedConsoleCalls = [
+ {
+ level: "log",
+ styles: undefined,
+ arguments: ["%cOne formatter with no styles"],
+ },
+ {
+ level: "log",
+ styles: /^color: red$/,
+ arguments: ["One formatter"],
+ },
+ {
+ level: "log",
+ styles: /^color: red,background: red$/,
+ arguments: ["Two formatters", "Each with an arg"],
+ },
+ {
+ level: "log",
+ styles: /^background: red$/,
+ arguments: ["Two formatters next to each other"],
+ },
+ {
+ level: "log",
+ styles: /^font-size: 150%$/,
+ arguments: ["Three formatters next to each other"],
+ },
+ {
+ level: "log",
+ styles: /^background: red,font-size: 150%$/,
+ arguments: ["Two formatters", "With a third separated"],
+ },
+ {
+ level: "log",
+ styles: /^color: red$/,
+ arguments: ["One formatter", "Second arg with no styles"],
+ },
+ {
+ level: "log",
+ styles: /^color: red$/,
+ arguments: ["One formatter",
+ "%cSecond formatter is ignored",
+ "background: blue"],
+ },
+ ];
+}
+
+async function startTest()
+{
+ removeEventListener("load", startTest);
+
+ const {state, response} = await attachConsoleToTab(["ConsoleAPI"]);
+ onAttach(state, response);
+}
+
+function onAttach(aState, aResponse)
+{
+ onConsoleAPICall = onConsoleAPICall.bind(null, aState);
+ aState.webConsoleFront.on("consoleAPICall", onConsoleAPICall);
+ doConsoleCalls(aState.actor);
+}
+
+let consoleCalls = [];
+
+function onConsoleAPICall(aState, aPacket)
+{
+ info("received message level: " + aPacket.message.level);
+
+ consoleCalls.push(aPacket.message);
+ if (consoleCalls.length != expectedConsoleCalls.length) {
+ return;
+ }
+
+ aState.webConsoleFront.off("consoleAPICall", onConsoleAPICall);
+
+ expectedConsoleCalls.forEach(function(aMessage, aIndex) {
+ info("checking received console call #" + aIndex);
+ const expected = expectedConsoleCalls[aIndex];
+ const consoleCall = consoleCalls[aIndex];
+ if (expected.styles == undefined) {
+ delete expected.styles;
+ is(consoleCall.styles, undefined, "No 'styles' property")
+ }
+ checkConsoleAPICall(consoleCall, expected);
+ });
+
+
+ consoleCalls = [];
+
+ closeDebugger(aState, function() {
+ SimpleTest.finish();
+ });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_timestamp.html b/devtools/shared/webconsole/test/chrome/test_console_timestamp.html
new file mode 100644
index 0000000000..a64de0d7b2
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_timestamp.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for console.group styling with %c</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+"use strict";
+
+window.onload = async function () {
+ SimpleTest.waitForExplicitFinish();
+ let state;
+ try {
+ state = (await attachConsole(["ConsoleAPI"])).state;
+ const consoleFront = state.webConsoleFront;
+
+ info("Testing console.log packet timestamp correctness");
+ const clientLogTimestamp = Date.now();
+ const packet = await consoleAPICall(consoleFront,
+ () => top.console.log("test"));
+ const packetReceivedTimestamp = Date.now();
+
+ const {timeStamp} = packet.message;
+ ok(clientLogTimestamp <= timeStamp && timeStamp <= packetReceivedTimestamp,
+ "console.log message timestamp is between the expected time range " +
+ `(${clientLogTimestamp} <= ${timeStamp} <= ${packetReceivedTimestamp})`
+ );
+ } catch (e) {
+ ok(false, `Error thrown: ${e.message}`);
+ }
+
+ await closeDebugger(state);
+ SimpleTest.finish();
+};
+
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_console_worker.html b/devtools/shared/webconsole/test/chrome/test_console_worker.html
new file mode 100644
index 0000000000..330c083f6b
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_console_worker.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the Console API and Workers</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the Console API and Workers</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const expectedCachedConsoleCalls = [{
+ message:{
+ level: "log",
+ filename: /console-test-worker/,
+ arguments: ['Log from worker init'],
+ }
+}];
+
+const expectedConsoleAPICalls = [{
+ message: {
+ level: "log",
+ arguments: ['Log was requested from worker'],
+ }
+}];
+
+window.onload = async function () {
+ const {state} = await attachConsoleToWorker(["ConsoleAPI"]);
+
+ await testCachedMessages(state);
+ await testConsoleAPI(state);
+
+ closeDebugger(state, function() {
+ SimpleTest.finish();
+ });
+};
+
+const testCachedMessages = async function (state) {
+ info("testCachedMessages entered");
+ return new Promise(resolve => {
+ const onCachedConsoleAPI = (response) => {
+ const consoleCalls = response.messages;
+
+ info('Received cached response. Checking console calls.');
+ checkConsoleAPICalls(consoleCalls, expectedCachedConsoleCalls);
+ resolve();
+ };
+ state.webConsoleFront.getCachedMessages(["ConsoleAPI"]).then(onCachedConsoleAPI);
+ })
+};
+
+const testConsoleAPI = async function (state) {
+ info("testConsoleAPI: adding listener for consoleAPICall");
+ const onConsoleApiMessage = state.webConsoleFront.once("consoleAPICall");
+ state._worker_ref.postMessage({
+ type: "log",
+ message: "Log was requested from worker"
+ });
+ const packet = await onConsoleApiMessage;
+ info("received message level: " + packet.message.level);
+ checkConsoleAPICalls([packet], expectedConsoleAPICalls);
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_consoleapi.html b/devtools/shared/webconsole/test/chrome/test_consoleapi.html
new file mode 100644
index 0000000000..b5d8edf23e
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_consoleapi.html
@@ -0,0 +1,225 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the Console API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the Console API</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+let expectedConsoleCalls = [];
+
+function doConsoleCalls(aState)
+{
+ const longString = (new Array(DevToolsServer.LONG_STRING_LENGTH + 2)).join("a");
+
+ top.console.log("foobarBaz-log", undefined);
+
+ top.console.log("Float from not a number: %f", "foo");
+ top.console.log("Float from string: %f", "1.2");
+ top.console.log("Float from number: %f", 1.3);
+
+ top.console.info("foobarBaz-info", null);
+ top.console.warn("foobarBaz-warn", top.document.documentElement);
+ top.console.debug(null);
+ top.console.trace();
+ top.console.dir(top.document, top.location);
+ top.console.log("foo", longString);
+
+ const sandbox = new Cu.Sandbox(null, { invisibleToDebugger: true });
+ const sandboxObj = sandbox.eval("new Object");
+ top.console.log(sandboxObj);
+
+ function fromAsmJS() {
+ top.console.error("foobarBaz-asmjs-error", undefined);
+ }
+
+ (function(global, foreign) {
+ "use asm";
+ function inAsmJS2() { foreign.fromAsmJS() }
+ function inAsmJS1() { inAsmJS2() }
+ return inAsmJS1
+ })(null, { fromAsmJS })();
+
+ expectedConsoleCalls = [
+ {
+ level: "log",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ level: "log",
+ arguments: ["Float from not a number: NaN"],
+ },
+ {
+ level: "log",
+ arguments: ["Float from string: 1.200000"],
+ },
+ {
+ level: "log",
+ arguments: ["Float from number: 1.300000"],
+ },
+ {
+ level: "info",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ level: "warn",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ {
+ level: "debug",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: [{ type: "null" }],
+ },
+ {
+ level: "trace",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ stacktrace: [
+ {
+ filename: /test_consoleapi/,
+ functionName: "doConsoleCalls",
+ },
+ {
+ filename: /test_consoleapi/,
+ functionName: "onAttach",
+ },
+ ],
+ },
+ {
+ level: "dir",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "HTMLDocument",
+ },
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "Location",
+ }
+ ],
+ },
+ {
+ level: "log",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: [
+ "foo",
+ {
+ type: "longString",
+ initial: longString.substring(0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH),
+ length: longString.length,
+ actor: /[a-z]/,
+ },
+ ],
+ },
+ {
+ level: "log",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "InvisibleToDebugger: Object",
+ },
+ ],
+ },
+ {
+ level: "error",
+ filename: /test_consoleapi/,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ arguments: ["foobarBaz-asmjs-error", { type: "undefined" }],
+
+ stacktrace: [
+ {
+ filename: /test_consoleapi/,
+ functionName: "fromAsmJS",
+ },
+ {
+ filename: /test_consoleapi/,
+ functionName: "inAsmJS2",
+ },
+ {
+ filename: /test_consoleapi/,
+ functionName: "inAsmJS1",
+ },
+ {
+ filename: /test_consoleapi/,
+ functionName: "doConsoleCalls",
+ },
+ {
+ filename: /test_consoleapi/,
+ functionName: "onAttach",
+ },
+ ],
+ },
+ ];
+}
+
+async function startTest()
+{
+ removeEventListener("load", startTest);
+
+ const {state, response} = await attachConsoleToTab(["ConsoleAPI"]);
+ onAttach(state, response);
+}
+
+function onAttach(aState, aResponse)
+{
+ onConsoleAPICall = onConsoleAPICall.bind(null, aState);
+ aState.webConsoleFront.on("consoleAPICall", onConsoleAPICall);
+ doConsoleCalls(aState.actor);
+}
+
+let consoleCalls = [];
+
+function onConsoleAPICall(aState, aPacket)
+{
+ info("received message level: " + aPacket.message.level);
+
+ consoleCalls.push(aPacket.message);
+ if (consoleCalls.length != expectedConsoleCalls.length) {
+ return;
+ }
+
+ aState.webConsoleFront.off("consoleAPICall", onConsoleAPICall);
+
+ expectedConsoleCalls.forEach(function(aMessage, aIndex) {
+ info("checking received console call #" + aIndex);
+ checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]);
+ });
+
+
+ consoleCalls = [];
+
+ closeDebugger(aState, function() {
+ SimpleTest.finish();
+ });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_consoleapi_innerID.html b/devtools/shared/webconsole/test/chrome/test_consoleapi_innerID.html
new file mode 100644
index 0000000000..a39b29289d
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_consoleapi_innerID.html
@@ -0,0 +1,159 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the innerID property of the Console API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the Console API</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+let expectedConsoleCalls = [];
+
+function doConsoleCalls(aState)
+{
+ const { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ const console = new ConsoleAPI({
+ innerID: window.top.windowGlobalChild.innerWindowId
+ });
+
+ const longString = (new Array(DevToolsServer.LONG_STRING_LENGTH + 2)).join("a");
+
+ console.log("foobarBaz-log", undefined);
+ console.info("foobarBaz-info", null);
+ console.warn("foobarBaz-warn", top.document.documentElement);
+ console.debug(null);
+ console.trace();
+ console.dir(top.document, top.location);
+ console.log("foo", longString);
+
+ expectedConsoleCalls = [
+ {
+ level: "log",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ level: "info",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ level: "warn",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ {
+ level: "debug",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ arguments: [{ type: "null" }],
+ },
+ {
+ level: "trace",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ stacktrace: [
+ {
+ filename: /test_consoleapi/,
+ functionName: "doConsoleCalls",
+ },
+ {
+ filename: /test_consoleapi/,
+ functionName: "onAttach",
+ },
+ ],
+ },
+ {
+ level: "dir",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "HTMLDocument",
+ },
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "Location",
+ }
+ ],
+ },
+ {
+ level: "log",
+ filename: /test_consoleapi/,
+ timeStamp: /^\d+$/,
+ arguments: [
+ "foo",
+ {
+ type: "longString",
+ initial: longString.substring(0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH),
+ length: longString.length,
+ actor: /[a-z]/,
+ },
+ ],
+ },
+ ];
+}
+
+async function startTest()
+{
+ removeEventListener("load", startTest);
+
+ const {state} = await attachConsoleToTab(["ConsoleAPI"]);
+ onAttach(state);
+}
+
+function onAttach(aState)
+{
+ onConsoleAPICall = onConsoleAPICall.bind(null, aState);
+ aState.webConsoleFront.on("consoleAPICall", onConsoleAPICall);
+ doConsoleCalls(aState.actor);
+}
+
+let consoleCalls = [];
+
+function onConsoleAPICall(aState, aPacket)
+{
+ info("received message level: " + aPacket.message.level);
+
+ consoleCalls.push(aPacket.message);
+ if (consoleCalls.length != expectedConsoleCalls.length) {
+ return;
+ }
+
+ aState.webConsoleFront.off("consoleAPICall", onConsoleAPICall);
+
+ expectedConsoleCalls.forEach(function(aMessage, aIndex) {
+ info("checking received console call #" + aIndex);
+ checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]);
+ });
+
+
+ consoleCalls = [];
+
+ closeDebugger(aState, function() {
+ SimpleTest.finish();
+ });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_file_uri.html b/devtools/shared/webconsole/test/chrome/test_file_uri.html
new file mode 100644
index 0000000000..8582a23091
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_file_uri.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for file activity tracking</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for file activity tracking</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const {NetUtil} = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+const {FileUtils} = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+let gState;
+let gTmpFile;
+
+function doFileActivity()
+{
+ info("doFileActivity");
+ const fileContent = "<p>hello world from bug 798764";
+
+ gTmpFile = new FileUtils.File(PathUtils.join(PathUtils.tempDir, "bug798764.html"));
+ gTmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ const fout = FileUtils.openSafeFileOutputStream(gTmpFile,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
+
+ const stream = Cc[
+ "@mozilla.org/io/arraybuffer-input-stream;1"
+ ].createInstance(Ci.nsIArrayBufferInputStream);
+ const buffer = new TextEncoder().encode(fileContent).buffer;
+ stream.setData(buffer, 0, buffer.byteLength);
+ NetUtil.asyncCopy(stream, fout, addIframe);
+}
+
+function addIframe(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was saved successfully");
+
+ const iframe = document.createElement("iframe");
+ iframe.src = NetUtil.newURI(gTmpFile).spec;
+ document.body.appendChild(iframe);
+}
+
+async function startTest()
+{
+ removeEventListener("load", startTest);
+
+ const {state} = await attachConsole(["FileActivity"]);
+ onAttach(state);
+}
+
+function onAttach(aState)
+{
+ gState = aState;
+ gState.webConsoleFront.on("fileActivity", onFileActivity);
+ doFileActivity();
+}
+
+function onFileActivity(aPacket)
+{
+ gState.webConsoleFront.off("fileActivity", onFileActivity);
+
+ info("aPacket.uri: " + aPacket.uri);
+ ok(/\bbug798764\b.*\.html$/.test(aPacket.uri), "file URI match");
+
+ testEnd();
+}
+
+function testEnd()
+{
+ if (gTmpFile) {
+ SimpleTest.executeSoon(function() {
+ try {
+ gTmpFile.remove(false);
+ }
+ catch (ex) {
+ if (ex.name != "NS_ERROR_FILE_IS_LOCKED") {
+ throw ex;
+ }
+ // Sometimes remove() throws because the file is not unlocked soon
+ // enough.
+ }
+ gTmpFile = null;
+ });
+ }
+
+ if (gState) {
+ closeDebugger(gState, function() {
+ gState = null;
+ SimpleTest.finish();
+ });
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_jsterm_autocomplete.html b/devtools/shared/webconsole/test/chrome/test_jsterm_autocomplete.html
new file mode 100644
index 0000000000..bbc9aeb6fc
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_jsterm_autocomplete.html
@@ -0,0 +1,635 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for JavaScript terminal functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for JavaScript terminal autocomplete functionality</p>
+
+<script class="testbody" type="text/javascript">
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+ const {
+ MAX_AUTOCOMPLETE_ATTEMPTS,
+ MAX_AUTOCOMPLETIONS
+ } = require("devtools/shared/webconsole/js-property-provider");
+ const RESERVED_JS_KEYWORDS = require("devtools/shared/webconsole/reserved-js-words");
+
+
+ addEventListener("load", startTest);
+
+ async function startTest() {
+ // First run the tests with a tab as a target.
+ let {state} = await attachConsoleToTab(["PageError"]);
+ await performTests({state, isWorker: false});
+
+ // Then run the tests with a worker as a target.
+ state = (await attachConsoleToWorker(["PageError"])).state;
+ await performTests({state, isWorker: true});
+
+ SimpleTest.finish();
+ }
+
+ async function performTests({state, isWorker}) {
+ // Set up the global variables needed to test autocompletion in the target.
+ const script = `
+ // This is for workers so autocomplete acts the same
+ if (!this.window) {
+ window = this;
+ }
+
+ window.foobarObject = Object.create(null);
+ window.foobarObject.foo = 1;
+ window.foobarObject.foobar = 2;
+ window.foobarObject.foobaz = 3;
+ window.foobarObject.omg = 4;
+ window.foobarObject.omgfoo = 5;
+ window.foobarObject.strfoo = "foobarz";
+ window.foobarObject.omgstr = "foobarz" +
+ (new Array(${DevToolsServer.LONG_STRING_LENGTH})).join("abb");
+ window.largeObject1 = Object.create(null);
+ for (let i = 0; i < ${MAX_AUTOCOMPLETE_ATTEMPTS + 1}; i++) {
+ window.largeObject1['a' + i] = i;
+ }
+
+ window.largeObject2 = Object.create(null);
+ for (let i = 0; i < ${MAX_AUTOCOMPLETIONS * 2}; i++) {
+ window.largeObject2['a' + i] = i;
+ }
+
+ window.proxy1 = new Proxy({foo: 1}, {
+ getPrototypeOf() { throw new Error() }
+ });
+ window.proxy2 = new Proxy(Object.create(Object.create(null, {foo:{}})), {
+ ownKeys() { throw new Error() }
+ });
+ window.emojiObject = Object.create(null);
+ window.emojiObject["😎"] = "😎";
+
+ window.insensitiveTestCase = Object.create(null, Object.getOwnPropertyDescriptors({
+ PROP: "",
+ Prop: "",
+ prop: "",
+ PRÖP: "",
+ pröp: "",
+ }));
+
+ window.elementAccessTestCase = Object.create(null, Object.getOwnPropertyDescriptors({
+ bar: "",
+ BAR: "",
+ dataTest: "",
+ "data-test": "",
+ 'da"ta"test': "",
+ 'da\`ta\`test': "",
+ "da'ta'test": "",
+ }));
+
+ window.varify = true;
+
+ var Cu_Sandbox = Cu ? Cu.Sandbox : null;
+ `;
+ await evaluateExpression(state.webConsoleFront, script);
+
+ const tests = [
+ doAutocomplete1,
+ doAutocomplete2,
+ doAutocomplete3,
+ doAutocomplete4,
+ doAutocompleteLarge1,
+ doAutocompleteLarge2,
+ doAutocompleteProxyThrowsPrototype,
+ doAutocompleteProxyThrowsOwnKeys,
+ doAutocompleteDotSurroundedBySpaces,
+ doAutocompleteAfterOr,
+ doInsensitiveAutocomplete,
+ doElementAccessAutocomplete,
+ doAutocompleteAfterOperator,
+ dontAutocompleteAfterDeclaration,
+ doKeywordsAutocomplete,
+ dontAutocomplete,
+ ];
+
+ if (!isWorker) {
+ // `Cu` is not defined in workers, then we can't test `Cu.Sandbox`
+ tests.push(doAutocompleteSandbox);
+ // Some cases are handled in worker context because we can't use parser.js.
+ // See Bug 1507181.
+ tests.push(
+ doAutocompleteArray,
+ doAutocompleteString,
+ doAutocompleteCommands,
+ doAutocompleteBracketSurroundedBySpaces,
+ );
+ }
+
+ for (const test of tests) {
+ await test(state.webConsoleFront);
+ }
+
+ // Null out proxy1 and proxy2: the proxy handlers use scripted functions
+ // that can keep the debugger sandbox alive longer than necessary via their
+ // environment chain (due to the webconsole helper functions defined there).
+ await evaluateExpression(state.webConsoleFront, `this.proxy1 = null; this.proxy2 = null;`);
+
+ await closeDebugger(state);
+ }
+
+ async function doAutocomplete1(webConsoleFront) {
+ info("test autocomplete for 'window.foo'");
+ const response = await webConsoleFront.autocomplete("window.foo");
+ const matches = response.matches;
+
+ is(response.matchProp, "foo", "matchProp");
+ is(matches.length, 1, "matches.length");
+ is(matches[0], "foobarObject", "matches[0]");
+ }
+
+ async function doAutocomplete2(webConsoleFront) {
+ info("test autocomplete for 'window.foobarObject.'");
+ const response = await webConsoleFront.autocomplete("window.foobarObject.");
+ const matches = response.matches;
+
+ ok(!response.matchProp, "matchProp");
+ is(matches.length, 7, "matches.length");
+ checkObject(matches,
+ ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
+ }
+
+ async function doAutocomplete3(webConsoleFront) {
+ // Check that completion suggestions are offered inside the string.
+ info("test autocomplete for 'dump(window.foobarObject.)'");
+ const response = await webConsoleFront.autocomplete("dump(window.foobarObject.)", 25);
+ const matches = response.matches;
+
+ ok(!response.matchProp, "matchProp");
+ is(matches.length, 7, "matches.length");
+ checkObject(matches,
+ ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
+ }
+
+ async function doAutocomplete4(webConsoleFront) {
+ // Check that completion requests can have no suggestions.
+ info("test autocomplete for 'dump(window.foobarObject.)'");
+ const response = await webConsoleFront.autocomplete("dump(window.foobarObject.)");
+ ok(!response.matchProp, "matchProp");
+ is(response.matches, null, "matches is null");
+ }
+
+ async function doAutocompleteLarge1(webConsoleFront) {
+ // Check that completion requests with too large objects will
+ // have no suggestions.
+ info("test autocomplete for 'window.largeObject1.'");
+ const response = await webConsoleFront.autocomplete("window.largeObject1.");
+ ok(!response.matchProp, "matchProp");
+ info (response.matches.join("|"));
+ is(response.matches.length, 0, "Bailed out with too many properties");
+ }
+
+ async function doAutocompleteLarge2(webConsoleFront) {
+ // Check that completion requests with pretty large objects will
+ // have MAX_AUTOCOMPLETIONS suggestions
+ info("test autocomplete for 'window.largeObject2.'");
+ const response = await webConsoleFront.autocomplete("window.largeObject2.");
+ ok(!response.matchProp, "matchProp");
+ is(response.matches.length, MAX_AUTOCOMPLETIONS, "matches.length is MAX_AUTOCOMPLETIONS");
+ }
+
+ async function doAutocompleteProxyThrowsPrototype(webConsoleFront) {
+ // Check that completion provides own properties even if [[GetPrototypeOf]] throws.
+ info("test autocomplete for 'window.proxy1.'");
+ const response = await webConsoleFront.autocomplete("window.proxy1.");
+ ok(!response.matchProp, "matchProp");
+ is(response.matches.length, 14, "matches.length");
+ ok(response.matches.includes("foo"), "matches has own property for proxy with throwing getPrototypeOf trap");
+ }
+
+ async function doAutocompleteProxyThrowsOwnKeys(webConsoleFront) {
+ // Check that completion provides inherited properties even if [[OwnPropertyKeys]] throws.
+ info("test autocomplete for 'window.proxy2.'");
+ const response = await webConsoleFront.autocomplete("window.proxy2.");
+ ok(!response.matchProp, "matchProp");
+ is(response.matches.length, 1, "matches.length");
+ checkObject(response.matches, ["foo"]);
+ }
+
+ async function doAutocompleteSandbox(webConsoleFront) {
+ // Check that completion provides inherited properties even if [[OwnPropertyKeys]] throws.
+ info("test autocomplete for 'Cu_Sandbox.'");
+ const response = await webConsoleFront.autocomplete("Cu_Sandbox.");
+ ok(!response.matchProp, "matchProp");
+ const keys = Object.getOwnPropertyNames(Object.prototype).sort();
+ is(response.matches.length, keys.length, "matches.length");
+ // checkObject(response.matches, keys);
+ is(response.matches.join(" - "), keys.join(" - "));
+ }
+
+ async function doAutocompleteArray(webConsoleFront) {
+ info("test autocomplete for [1,2,3]");
+ const response = await webConsoleFront.autocomplete("[1,2,3].");
+ let {matches} = response;
+
+ ok(!!matches.length, "There are completion results for the array");
+ ok(matches.includes("length") && matches.includes("filter"),
+ "Array autocomplete contains expected results");
+
+ info("test autocomplete for '[] . '");
+ matches = (await webConsoleFront.autocomplete("[] . ")).matches;
+ ok(matches.length > 1);
+ ok(matches.includes("length") && matches.includes("filter"),
+ "Array autocomplete contains expected results");
+ ok(!matches.includes("copy"), "Array autocomplete does not contain helpers");
+
+ info("test autocomplete for '[1,2,3]['");
+ matches = (await webConsoleFront.autocomplete("[1,2,3][")).matches;
+ ok(matches.length > 1);
+ ok(matches.includes('"length"') && matches.includes('"filter"'),
+ "Array autocomplete contains expected results, surrounded by quotes");
+
+ info("test autocomplete for '[1,2,3]['");
+ matches = (await webConsoleFront.autocomplete("[1,2,3]['")).matches;
+ ok(matches.length > 1);
+ ok(matches.includes("'length'") && matches.includes("'filter'"),
+ "Array autocomplete contains expected results, surrounded by quotes");
+
+ info("test autocomplete for '[1,2,3][l");
+ matches = (await webConsoleFront.autocomplete("[1,2,3][l")).matches;
+ ok(matches.length >= 1);
+ ok(matches.includes('"length"'),
+ "Array autocomplete contains expected results, surrounded by quotes");
+
+ info("test autocomplete for '[1,2,3]['l");
+ matches = (await webConsoleFront.autocomplete("[1,2,3]['l")).matches;
+ ok(matches.length >= 1);
+ ok(matches.includes("'length'"),
+ "Array autocomplete contains expected results, surrounded by quotes");
+ }
+
+ async function doAutocompleteString(webConsoleFront) {
+ info(`test autocomplete for "foo".`);
+ const response = await webConsoleFront.autocomplete(`"foo".`);
+ let {matches} = response;
+
+ ok(!!matches.length, "There are completion results for the string");
+ ok(matches.includes("substr") && matches.includes("trim"),
+ "String autocomplete contains expected results");
+
+ info("test autocomplete for `foo`[");
+ matches = (await webConsoleFront.autocomplete("`foo`[")).matches;
+ ok(matches.length > 1, "autocomplete string with bracket works");
+ ok(matches.includes('"substr"') && matches.includes('"trim"'),
+ "String autocomplete contains expected results, surrounded by quotes");
+ }
+
+ async function doAutocompleteDotSurroundedBySpaces(webConsoleFront) {
+ info("test autocomplete for 'window.foobarObject\n .'");
+ let {matches} = await webConsoleFront.autocomplete("window.foobarObject\n .");
+ is(matches.length, 7);
+ checkObject(matches,
+ ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
+
+ info("test autocomplete for 'window.foobarObject\n .o'");
+ matches = (await webConsoleFront.autocomplete("window.foobarObject\n .o")).matches;
+ is(matches.length, 3);
+ checkObject(matches, ["omg", "omgfoo", "omgstr"]);
+
+ info("test autocomplete for 'window.foobarObject\n .\n s'");
+ matches = (await webConsoleFront.autocomplete("window.foobarObject\n .\n s")).matches;
+ is(matches.length, 1);
+ checkObject(matches, ["strfoo"]);
+
+ info("test autocomplete for 'window.foobarObject\n . '");
+ matches = (await webConsoleFront.autocomplete("window.foobarObject\n . ")).matches;
+ is(matches.length, 7);
+ checkObject(matches,
+ ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
+
+ matches =
+ (await webConsoleFront.autocomplete("window.foobarObject. foo ; window.foo")).matches;
+ is(matches.length, 1);
+ checkObject(matches, ["foobarObject"]);
+ }
+
+ async function doAutocompleteBracketSurroundedBySpaces(webConsoleFront) {
+ const wrap = (arr, quote = `"`) => arr.map(x => `${quote}${x}${quote}`);
+ let matches = await getAutocompleteMatches(webConsoleFront, "window.foobarObject\n [")
+ is(matches.length, 7);
+ checkObject(matches,
+ wrap(["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]));
+
+ matches = await getAutocompleteMatches(webConsoleFront, "window.foobarObject\n ['o")
+ is(matches.length, 3);
+ checkObject(matches, wrap(["omg", "omgfoo", "omgstr"], "'"));
+
+ matches = await getAutocompleteMatches(webConsoleFront, "window.foobarObject\n [\n s");
+ is(matches.length, 1);
+ checkObject(matches, [`"strfoo"`]);
+
+ matches = await getAutocompleteMatches(webConsoleFront, "window.foobarObject\n [ ");
+ is(matches.length, 7);
+ checkObject(matches,
+ wrap(["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]));
+
+ matches = await getAutocompleteMatches(webConsoleFront, "window.emojiObject [ '");
+ is(matches.length, 1);
+ checkObject(matches, [`'😎'`]);
+ }
+
+ async function doAutocompleteAfterOr(webConsoleFront) {
+ info("test autocomplete for 'true || foo'");
+ const {matches} = await webConsoleFront.autocomplete("true || foobar");
+ is(matches.length, 1, "autocomplete returns expected results");
+ is(matches.join("-"), "foobarObject");
+ }
+
+ async function doInsensitiveAutocomplete(webConsoleFront) {
+ info("test autocomplete for 'window.insensitiveTestCase.'");
+ let {matches} = await webConsoleFront.autocomplete("window.insensitiveTestCase.");
+ is(matches.join("-"), "prop-pröp-Prop-PROP-PRÖP",
+ "autocomplete returns the expected items, in the expected order");
+
+ info("test autocomplete for 'window.insensitiveTestCase.p'");
+ matches = (await webConsoleFront.autocomplete("window.insensitiveTestCase.p")).matches;
+ is(matches.join("-"), "prop-pröp-Prop-PROP-PRÖP",
+ "autocomplete is case-insensitive when first letter is lowercased");
+
+ info("test autocomplete for 'window.insensitiveTestCase.pRoP'");
+ matches = (await webConsoleFront.autocomplete("window.insensitiveTestCase.pRoP")).matches;
+ is(matches.join("-"), "prop-Prop-PROP",
+ "autocomplete is case-insensitive when first letter is lowercased");
+
+ info("test autocomplete for 'window.insensitiveTestCase.P'");
+ matches = (await webConsoleFront.autocomplete("window.insensitiveTestCase.P")).matches;
+ is(matches.join("-"), "Prop-PROP-PRÖP",
+ "autocomplete is case-sensitive when first letter is uppercased");
+
+ info("test autocomplete for 'window.insensitiveTestCase.PROP'");
+ matches = (await webConsoleFront.autocomplete("window.insensitiveTestCase.PROP")).matches;
+ is(matches.join("-"), "PROP",
+ "autocomplete is case-sensitive when first letter is uppercased");
+
+ info("test autocomplete for 'window.insensitiveTestCase.prö'");
+ matches = (await webConsoleFront.autocomplete("window.insensitiveTestCase.prö")).matches;
+ is(matches.join("-"), "pröp-PRÖP", "expected result with lowercase diacritic");
+
+ info("test autocomplete for 'window.insensitiveTestCase.PRÖ'");
+ matches = (await webConsoleFront.autocomplete("window.insensitiveTestCase.PRÖ")).matches;
+ is(matches.join("-"), "PRÖP", "expected result with uppercase diacritic");
+ }
+
+ async function doElementAccessAutocomplete(webConsoleFront) {
+ info("test autocomplete for 'window.elementAccessTestCase['");
+ let res = (await webConsoleFront.autocomplete("window.elementAccessTestCase["));
+ is(
+ res.matches.join("|"),
+ `"bar"|"da'ta'test"|"da\\"ta\\"test"|"da\`ta\`test"|"data-test"|"dataTest"|"BAR"`,
+ "autocomplete returns the expected items, wrapped in quotes");
+ is(res.isElementAccess, true);
+
+ info("test autocomplete for 'window.elementAccessTestCase[d'");
+ res = await webConsoleFront.autocomplete("window.elementAccessTestCase[d");
+ is(
+ res.matches.join("|"),
+ `"da'ta'test"|"da\\"ta\\"test"|"da\`ta\`test"|"data-test"|"dataTest"`,
+ "autocomplete returns the expected filtered items");
+ is(res.isElementAccess, true);
+
+ info(`test autocomplete for 'window.elementAccessTestCase["d'`);
+ res = await webConsoleFront.autocomplete(`window.elementAccessTestCase["d`);
+ is(
+ res.matches.join("|"),
+ `"da'ta'test"|"da\\"ta\\"test"|"da\`ta\`test"|"data-test"|"dataTest"`,
+ "autocomplete returns the expected items, wrapped in quotes");
+ is(res.isElementAccess, true);
+
+ info(`test autocomplete for 'window.elementAccessTestCase["data-`);
+ res = await webConsoleFront.autocomplete(`window.elementAccessTestCase["data-`);
+ is(res.matches.join("|"), `"data-test"`,
+ "autocomplete returns the expected items, wrapped in quotes");
+ is(res.isElementAccess, true);
+
+ info(`test autocomplete for 'window.elementAccessTestCase['d'`);
+ res = await webConsoleFront.autocomplete(`window.elementAccessTestCase['d`);
+ is(
+ res.matches.join("|"),
+ `'da"ta"test'|'da\\'ta\\'test'|'da\`ta\`test'|'data-test'|'dataTest'`,
+ "autocomplete returns the expected items, wrapped in the same quotes the user entered");
+ is(res.isElementAccess, true);
+
+ info("test autocomplete for 'window.elementAccessTestCase[`d'");
+ res = await webConsoleFront.autocomplete("window.elementAccessTestCase[`d");
+ is(
+ res.matches.join("|"),
+ "`da'ta'test`|`da\"ta\"test`|`da\\`ta\\`test`|`data-test`|`dataTest`",
+ "autocomplete returns the expected items, wrapped in the same quotes the user entered");
+ is(res.isElementAccess, true);
+
+ info(`test autocomplete for '['`);
+ res = await webConsoleFront.autocomplete(`[`);
+ is(res.matches, null, "it does not return anything");
+
+ info(`test autocomplete for '[1,2,3'`);
+ res = await webConsoleFront.autocomplete(`[1,2,3`);
+ is(res.matches, null, "it does not return anything");
+
+ info(`test autocomplete for '["'`);
+ res = await webConsoleFront.autocomplete(`["`);
+ is(res.matches, null, "it does not return anything");
+
+ info(`test autocomplete for '[;'`);
+ res = await webConsoleFront.autocomplete(`[;`);
+ is(res.matches, null, "it does not return anything");
+ }
+
+ async function doAutocompleteCommands(webConsoleFront) {
+ info("test autocomplete for 'c'");
+ let matches = (await webConsoleFront.autocomplete("c")).matches;
+ ok(matches.includes("clear"), "commands are returned");
+
+ info("test autocomplete for 's'");
+ matches = (await webConsoleFront.autocomplete("s")).matches;
+ is(matches.includes("screenshot"), false, "screenshot is not returned");
+
+ info("test autocomplete for ':s'");
+ matches = (await webConsoleFront.autocomplete(":s")).matches;
+ is(matches.includes(":screenshot"), true, "screenshot is returned");
+
+ info("test autocomplete for 'window.c'");
+ matches = (await webConsoleFront.autocomplete("window.c")).matches;
+ ok(!matches.includes("clear"), "commands are not returned");
+
+ info("test autocomplete for 'window[c'");
+ matches = (await webConsoleFront.autocomplete("window[c")).matches;
+ ok(!matches.includes("clear"), "commands are not returned");
+
+ info(`test autocomplete for 'window["c'`);
+ matches = (await webConsoleFront.autocomplete(`window["c`)).matches;
+ ok(!matches.includes("clear"), "commands are not returned");
+
+ info(`test autocomplete for 'window["c'`);
+ matches = (await webConsoleFront.autocomplete(`window["c`)).matches;
+ ok(!matches.includes("clear"), "commands are not returned");
+
+ info(`test autocomplete for 'window[";c'`);
+ matches = (await webConsoleFront.autocomplete(`window[";c`)).matches;
+ ok(!matches.includes("clear"), "commands are not returned");
+
+ info(`test autocomplete for 'window[;c'`);
+ matches = (await webConsoleFront.autocomplete(`window[;c`)).matches;
+ ok(!matches.includes("clear"), "commands are not returned");
+ }
+
+ async function doAutocompleteAfterOperator(webConsoleFront) {
+ const inputs = [
+ "true;foob",
+ "true,foob",
+ "({key:foob",
+ "a=foob",
+ "if(a<foob",
+ "if(a>foob",
+ "1+foob",
+ "1-foob",
+ "++foob",
+ "--foob",
+ "1*foob",
+ "2**foob",
+ "1/foob",
+ "1%foob",
+ "1|foob",
+ "1&foob",
+ "1^foob",
+ "~foob",
+ "1<<foob",
+ "1>>foob",
+ "1>>>foob",
+ "false||foob",
+ "false&&foob",
+ "x=true?foob",
+ "x=false?1:foob",
+ "!foob",
+ "false??foob",
+ ];
+
+ for (const input of inputs) {
+ info(`test autocomplete for "${input}"`);
+ const matches = (await webConsoleFront.autocomplete(input)).matches;
+ ok(matches.includes("foobarObject"), `Expected autocomplete result for ${input}"`);
+ }
+ }
+
+ async function dontAutocompleteAfterDeclaration(webConsoleFront) {
+ info("test autocomplete for 'var win'");
+ let matches = (await webConsoleFront.autocomplete("var win")).matches;
+ is(matches, null, "no autocompletion on a var declaration");
+
+ info("test autocomplete for 'const win'");
+ matches = (await webConsoleFront.autocomplete("const win")).matches;
+ is(matches, null, "no autocompletion on a const declaration");
+
+ info("test autocomplete for 'let win'");
+ matches = (await webConsoleFront.autocomplete("let win")).matches;
+ is(matches, null, "no autocompletion on a let declaration");
+
+ info("test autocomplete for 'function win'");
+ matches = (await webConsoleFront.autocomplete("function win")).matches;
+ is(matches, null, "no autocompletion on a function declaration");
+
+ info("test autocomplete for 'class win'");
+ matches = (await webConsoleFront.autocomplete("class win")).matches;
+ is(matches, null, "no autocompletion on a class declaration");
+
+ info("test autocomplete for 'const win = win'");
+ matches = (await webConsoleFront.autocomplete("const win = win")).matches;
+ ok(matches.includes("window"), "autocompletion still happens after the `=` sign");
+
+ info("test autocomplete for 'in var'");
+ matches = (await webConsoleFront.autocomplete("in var")).matches;
+ ok(matches.includes("varify"),
+ "autocompletion still happens with a property name starting with 'var'");
+}
+
+async function doKeywordsAutocomplete(webConsoleFront) {
+ info("test autocomplete for 'func'");
+ let matches = (await webConsoleFront.autocomplete("func")).matches;
+ ok(matches.includes("function"), "keywords are returned");
+
+ info("test autocomplete for ':func'");
+ matches = (await webConsoleFront.autocomplete(":func")).matches;
+ is(!matches.includes("function"), true,
+ "'function' is not returned when prefixed with ':'");
+
+ info("test autocomplete for 'window.func'");
+ matches = (await webConsoleFront.autocomplete("window.func")).matches;
+ ok(!matches.includes("function"),
+ "'function' is not returned when doing a property access");
+
+ info("test autocomplete for 'window[func'");
+ matches = (await webConsoleFront.autocomplete("window[func")).matches;
+ ok(!matches.includes("function"),
+ "'function' is not returned when doing an element access");
+ }
+
+ async function dontAutocomplete(webConsoleFront) {
+ const inputs = [
+ "",
+ " ",
+ "\n",
+ "\n ",
+ " \n ",
+ " \n",
+ "true;",
+ "true,",
+ "({key:",
+ "a=",
+ "if(a<",
+ "if(a>",
+ "1+",
+ "1-",
+ "++",
+ "--",
+ "1*",
+ "2**",
+ "1/",
+ "1%",
+ "1|",
+ "1&",
+ "1^",
+ "~",
+ "1<<",
+ "1>>",
+ "1>>>",
+ "false||",
+ "false&&",
+ "x=true?",
+ "x=false?1:",
+ "!",
+ ...RESERVED_JS_KEYWORDS.map(keyword => `${keyword} `),
+ ...RESERVED_JS_KEYWORDS.map(keyword => `${keyword} `),
+ ];
+ for (const input of inputs) {
+ info(`test autocomplete for "${input}"`);
+ const matches = (await webConsoleFront.autocomplete(input)).matches;
+ is(matches, null, `No autocomplete result for ${input}"`);
+ }
+ }
+
+ async function getAutocompleteMatches(webConsoleFront, input) {
+ info(`test autocomplete for "${input}"`);
+ const res = (await webConsoleFront.autocomplete(input));
+ return res.matches;
+ }
+
+ async function evaluateExpression(consoleFront, expression) {
+ const onEvaluationResult = consoleFront.once("evaluationResult");
+ await consoleFront.evaluateJSAsync({ text: expression });
+ return onEvaluationResult;
+ }
+
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_network_get.html b/devtools/shared/webconsole/test/chrome/test_network_get.html
new file mode 100644
index 0000000000..1394941ec3
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_network_get.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the network actor (GET request)</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (GET request)</p>
+
+<iframe src="http://example.com/chrome/devtools/shared/webconsole/test/chrome/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+async function startTest()
+{
+ await SpecialPowers.pushPrefEnv({
+ 'set': [
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ['network.cookie.sameSite.laxByDefault', false],
+ ]
+ });
+
+ const commands = await createCommandsForTab();
+ const resourceCommand = commands.resourceCommand;
+
+ info("test network GET request");
+ const resource = await new Promise(resolve => {
+ resourceCommand
+ .watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable: () => {},
+ onUpdated: resourceUpdate => {
+ resolve(resourceUpdate[0].resource);
+ },
+ })
+ .then(() => {
+ // Spawn the network request after we started watching
+ const iframe = document.querySelector("iframe").contentWindow;
+ iframe.wrappedJSObject.testXhrGet(null, "data.json?" + Date.now());
+ });
+ });
+
+ const { client } = commands;
+ const netActor = resource.actor;
+
+ info("checking request headers");
+ const requestHeadersPacket = await client.request({ to: netActor, type: "getRequestHeaders" });
+ ok(!!requestHeadersPacket.headers.length, `request headers > 0 (${requestHeadersPacket.headers.length})`);
+ ok(requestHeadersPacket.headersSize > 0, `request headersSize > 0 (${requestHeadersPacket.headersSize})`);
+ ok(!!requestHeadersPacket.rawHeaders, "request rawHeaders available");
+
+ checkHeadersOrCookies(requestHeadersPacket.headers, {
+ Referer: /network_requests_iframe\.html/,
+ Cookie: /bug768096/,
+ });
+
+ checkRawHeaders(requestHeadersPacket.rawHeaders, {
+ Referer: /network_requests_iframe\.html/,
+ Cookie: /bug768096/,
+ });
+
+ info("checking request cookies");
+
+ const requestCookiesPacket = await client.request({ to: netActor, type: "getRequestCookies" });
+ is(requestCookiesPacket.cookies.length, 3, "request cookies length");
+
+ checkHeadersOrCookies(requestCookiesPacket.cookies, {
+ foobar: "fooval",
+ omgfoo: "bug768096",
+ badcookie: "bug826798=st3fan",
+ });
+
+ info("checking request POST data");
+ const postDataPacket = await client.request({ to: netActor, type: "getRequestPostData" });
+ ok(!postDataPacket.postData.text, "no request POST data");
+ ok(!postDataPacket.postDataDiscarded, "request POST data was not discarded");
+
+ info("checking response headers");
+ const responseHeaderPacket = await client.request({ to: netActor, type: "getResponseHeaders" });
+
+ ok(!!responseHeaderPacket.headers.length, "response headers > 0");
+ ok(responseHeaderPacket.headersSize > 0, "response headersSize > 0");
+ ok(!!responseHeaderPacket.rawHeaders, "response rawHeaders available");
+
+ checkHeadersOrCookies(responseHeaderPacket.headers, {
+ "content-type": /^application\/(json|octet-stream)$/,
+ "content-length": /^\d+$/,
+ });
+
+ checkRawHeaders(responseHeaderPacket.rawHeaders, {
+ "content-type": /^application\/(json|octet-stream)$/,
+ "content-length": /^\d+$/,
+ });
+
+ info("checking response cookies");
+ const responseCookiesPacket = await client.request({ to: netActor, type: "getResponseCookies" });
+ is(responseCookiesPacket.cookies.length, 0, "response cookies length");
+
+ info("checking response content");
+ const responseContentPacket = await client.request({ to: netActor, type: "getResponseContent" });
+ ok(responseContentPacket.content.text, "response content text");
+ ok(!responseContentPacket.contentDiscarded, "response content was not discarded");
+
+ info("checking event timings");
+ const eventTimingPacket = await client.request({ to: netActor, type: "getEventTimings" });
+ checkObject(eventTimingPacket, {
+ timings: {
+ blocked: /^-1|\d+$/,
+ dns: /^-1|\d+$/,
+ connect: /^-1|\d+$/,
+ send: /^-1|\d+$/,
+ wait: /^-1|\d+$/,
+ receive: /^-1|\d+$/,
+ },
+ totalTime: /^\d+$/,
+ });
+
+ await commands.destroy();
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+}
+
+addEventListener("load", startTest, { once: true});
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_network_post.html b/devtools/shared/webconsole/test/chrome/test_network_post.html
new file mode 100644
index 0000000000..dc17432125
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_network_post.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the network actor (POST request)</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (POST request)</p>
+
+<iframe src="http://example.com/chrome/devtools/shared/webconsole/test/chrome/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+async function startTest()
+{
+ await SpecialPowers.pushPrefEnv({
+ 'set': [
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ['network.cookie.sameSite.laxByDefault', false],
+ ]
+ });
+
+ const commands = await createCommandsForTab();
+ const resourceCommand = commands.resourceCommand;
+
+ info("test network POST request");
+ const resource = await new Promise(resolve => {
+ resourceCommand
+ .watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable: () => {},
+ onUpdated: resourceUpdate => {
+ resolve(resourceUpdate[0].resource);
+ },
+ })
+ .then(() => {
+ // Spawn the network request after we started watching
+ const iframe = document.querySelector("iframe").contentWindow;
+ iframe.wrappedJSObject.testXhrPost();
+ });
+ });
+
+ const { client } = commands;
+ const netActor = resource.actor;
+
+ info("checking request headers");
+ const requestHeadersPacket = await client.request({ to: netActor, type: "getRequestHeaders" });
+
+ ok(!!requestHeadersPacket.headers.length, "request headers > 0");
+ ok(requestHeadersPacket.headersSize > 0, "request headersSize > 0");
+ ok(!!requestHeadersPacket.rawHeaders.length, "request rawHeaders available");
+
+ checkHeadersOrCookies(requestHeadersPacket.headers, {
+ Referer: /network_requests_iframe\.html/,
+ Cookie: /bug768096/,
+ });
+
+ checkRawHeaders(requestHeadersPacket.rawHeaders, {
+ Referer: /network_requests_iframe\.html/,
+ Cookie: /bug768096/,
+ });
+
+ info("checking request cookies");
+ const requestCookiesPacket = await client.request({ to: netActor, type: "getRequestCookies" });
+ is(requestCookiesPacket.cookies.length, 3, "request cookies length");
+
+ checkHeadersOrCookies(requestCookiesPacket.cookies, {
+ foobar: "fooval",
+ omgfoo: "bug768096",
+ badcookie: "bug826798=st3fan",
+ });
+
+ info("checking request POST data");
+ const requestPostDataPacket = await client.request({ to: netActor, type: "getRequestPostData" });
+
+ checkObject(requestPostDataPacket, {
+ postData: {
+ text: /^Hello world! foobaz barr.+foobaz barr$/,
+ },
+ postDataDiscarded: false,
+ });
+
+ is(requestPostDataPacket.postData.text.length, 552, "postData text length");
+
+ info("checking response headers");
+ const responseHeadersPacket = await client.request({ to: netActor, type: "getResponseHeaders" });
+ ok(!!responseHeadersPacket.headers.length, "response headers > 0");
+ ok(responseHeadersPacket.headersSize > 0, "response headersSize > 0");
+ ok(!!responseHeadersPacket.rawHeaders, "response rawHeaders available");
+
+ checkHeadersOrCookies(responseHeadersPacket.headers, {
+ "content-type": /^application\/(json|octet-stream)$/,
+ "content-length": /^\d+$/,
+ });
+
+ checkRawHeaders(responseHeadersPacket.rawHeaders, {
+ "content-type": /^application\/(json|octet-stream)$/,
+ "content-length": /^\d+$/,
+ });
+
+ info("checking response cookies");
+ const responseCookiesPacket = await client.request({ to: netActor, type: "getResponseCookies" });
+ is(responseCookiesPacket.cookies.length, 0, "response cookies length");
+
+ info("checking response content");
+ const responseContentPacket = await client.request({ to: netActor, type: "getResponseContent" });
+ checkObject(responseContentPacket, {
+ content: {
+ text: /"test JSON data"/,
+ },
+ contentDiscarded: false,
+ });
+
+ info("checking event timings");
+ const eventTimingsPacket = await client.request({ to: netActor, type: "getEventTimings" });
+ checkObject(eventTimingsPacket, {
+ timings: {
+ blocked: /^-1|\d+$/,
+ dns: /^-1|\d+$/,
+ connect: /^-1|\d+$/,
+ send: /^-1|\d+$/,
+ wait: /^-1|\d+$/,
+ receive: /^-1|\d+$/,
+ },
+ totalTime: /^\d+$/,
+ });
+
+ await commands.destroy();
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+}
+
+addEventListener("load", startTest, { once: true });
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_network_security-hsts.html b/devtools/shared/webconsole/test/chrome/test_network_security-hsts.html
new file mode 100644
index 0000000000..32582e5909
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_network_security-hsts.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the network actor (HSTS detection)</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (HSTS detection)</p>
+
+<iframe src="https://example.com/chrome/devtools/shared/webconsole/test/chrome/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const TEST_CASES = [
+ {
+ desc: "no HSTS",
+ url: "https://example.com",
+ usesHSTS: false,
+ },
+ {
+ desc: "HSTS from this response",
+ url: "https://example.com/"+
+ "browser/browser/base/content/test/general/browser_star_hsts.sjs",
+ usesHSTS: true,
+ },
+ {
+ desc: "stored HSTS from previous response",
+ url: "https://example.com/",
+ usesHSTS: true,
+ }
+];
+
+async function startTest()
+{
+ info("Test detection of HTTP Strict Transport Security.");
+ for (const testCase of TEST_CASES) {
+ await checkHSTS(testCase)
+ }
+
+ // Reset HSTS state.
+ const gSSService = Cc["@mozilla.org/ssservice;1"].getService(Ci.nsISiteSecurityService);
+ const uri = Services.io.newURI(TEST_CASES[0].url);
+ gSSService.resetState(uri);
+
+ SimpleTest.finish();
+}
+
+async function checkHSTS({desc, url, usesHSTS}) {
+ info("Testing HSTS for " + url);
+ const commands = await createCommandsForTab();
+ const resourceCommand = commands.resourceCommand;
+
+ const resource = await new Promise(resolve => {
+ resourceCommand
+ .watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
+ onAvailable: () => {},
+ onUpdated: resourceUpdate => {
+ resolve(resourceUpdate[0].resource);
+ },
+ })
+ .then(() => {
+ // Spawn the network requests after we started watching
+ const iframe = document.querySelector("iframe").contentWindow;
+ iframe.wrappedJSObject.makeXhrCallback("GET", url);
+ });
+ });
+
+ const packet = await commands.client.request({ to: resource.actor, type: "getSecurityInfo" });
+ is(
+ packet.securityInfo.hsts,
+ usesHSTS,
+ "Strict Transport Security detected correctly for " + url
+ );
+ await commands.destroy();
+}
+
+addEventListener("load", startTest, { once: true });
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_nsiconsolemessage.html b/devtools/shared/webconsole/test/chrome/test_nsiconsolemessage.html
new file mode 100644
index 0000000000..4e460225df
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_nsiconsolemessage.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for nsIConsoleMessages</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Make sure that nsIConsoleMessages are logged. See bug 859756.</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+SimpleTest.waitForExplicitFinish();
+
+let expectedMessages = [];
+
+async function startTest()
+{
+ removeEventListener("load", startTest);
+ const {state} = await attachConsole(["PageError"]);
+ onAttach(state);
+}
+
+function onAttach(aState)
+{
+ onLogMessage = onLogMessage.bind(null, aState);
+ aState.webConsoleFront.on("logMessage", onLogMessage);
+
+ expectedMessages = [{
+ message: "hello world! bug859756",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ }];
+
+ Services.console.logStringMessage("hello world! bug859756");
+
+ info("waiting for messages");
+}
+
+const receivedMessages = [];
+
+function onLogMessage(aState, aPacket)
+{
+ info("received message: " + aPacket.message);
+
+ let found = false;
+ for (const expected of expectedMessages) {
+ if (expected.message == aPacket.message) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return;
+ }
+
+ receivedMessages.push(aPacket);
+ if (receivedMessages.length != expectedMessages.length) {
+ return;
+ }
+
+ aState.webConsoleFront.off("logMessage", onLogMessage);
+
+ checkObject(receivedMessages, expectedMessages);
+
+ closeDebugger(aState, () => SimpleTest.finish());
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_object_actor.html b/devtools/shared/webconsole/test/chrome/test_object_actor.html
new file mode 100644
index 0000000000..f42035e2ce
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_object_actor.html
@@ -0,0 +1,158 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the object actor</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the object actor</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({
+ "set": [["security.allow_eval_with_system_principal", true]]
+});
+
+async function startTest() {
+ removeEventListener("load", startTest);
+
+ const longString = (new Array(DevToolsServer.LONG_STRING_LENGTH + 3)).join("\u0629");
+ createTestGlobalVariable(longString);
+
+ const {state} = await attachConsoleToTab(["ConsoleAPI"]);
+ const onConsoleApiCall = state.webConsoleFront.once("consoleAPICall");
+ top.console.log("hello", top.wrappedJSObject.foobarObject);
+ const {message} = await onConsoleApiCall;
+
+ info("checking the console API call packet");
+ checkConsoleAPICall(message, {
+ level: "log",
+ filename: /test_object_actor/,
+ arguments: ["hello", {
+ type: "object",
+ actor: /[a-z]/,
+ }],
+ });
+
+ info("inspecting object properties");
+ const {ownProperties} = await message.arguments[1].getPrototypeAndProperties();
+
+ const expectedProps = {
+ "abArray": {
+ value: {
+ type: "object",
+ class: "Array",
+ actor: /[a-z]/,
+ },
+ },
+ "foo": {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: 1,
+ },
+ "foobar": {
+ value: "hello",
+ },
+ "foobaz": {
+ value: {
+ type: "object",
+ class: "HTMLDocument",
+ actor: /[a-z]/,
+ },
+ },
+ "getterAndSetter": {
+ get: {
+ type: "object",
+ class: "Function",
+ actor: /[a-z]/,
+ },
+ set: {
+ type: "object",
+ class: "Function",
+ actor: /[a-z]/,
+ },
+ },
+ "longStringObj": {
+ value: {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ },
+ },
+ "notInspectable": {
+ value: {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ },
+ },
+ "omg": {
+ value: { type: "null" },
+ },
+ "omgfn": {
+ value: {
+ type: "object",
+ class: "Function",
+ actor: /[a-z]/,
+ },
+ },
+ "tamarbuta": {
+ value: {
+ type: "longString",
+ initial: longString.substring(0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH),
+ length: longString.length,
+ },
+ },
+ "testfoo": {
+ value: false,
+ },
+ };
+ is(Object.keys(ownProperties).length, Object.keys(expectedProps).length,
+ "number of enumerable properties");
+ checkObject(ownProperties, expectedProps);
+
+ await closeDebugger(state);
+ SimpleTest.finish();
+}
+
+
+function createTestGlobalVariable(longString) {
+ // Here we put the objects in the correct window, to avoid having them all
+ // wrapped by proxies for cross-compartment access.
+ const foobarObject = top.Object.create(null);
+ foobarObject.tamarbuta = longString;
+ foobarObject.foo = 1;
+ foobarObject.foobar = "hello";
+ foobarObject.omg = null;
+ foobarObject.testfoo = false;
+ foobarObject.notInspectable = top.Object.create(null);
+ foobarObject.omgfn = new top.Function("return 'myResult'");
+ foobarObject.abArray = new top.Array("a", "b");
+ foobarObject.foobaz = top.document;
+
+ top.Object.defineProperty(foobarObject, "getterAndSetter", {
+ enumerable: true,
+ get: new top.Function("return 'foo';"),
+ set: new top.Function("1+2"),
+ });
+
+ foobarObject.longStringObj = top.Object.create(null);
+ foobarObject.longStringObj.toSource = new top.Function("'" + longString + "'");
+ foobarObject.longStringObj.toString = new top.Function("'" + longString + "'");
+ foobarObject.longStringObj.boom = "explode";
+ top.wrappedJSObject.foobarObject = foobarObject;
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_object_actor_native_getters.html b/devtools/shared/webconsole/test/chrome/test_object_actor_native_getters.html
new file mode 100644
index 0000000000..6ac292fb9a
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_object_actor_native_getters.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for the native getters in object actors</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the native getters in object actors</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+async function startTest() {
+ removeEventListener("load", startTest);
+ const {state} = await attachConsoleToTab(["ConsoleAPI"]);
+
+ const onConsoleAPICall = state.webConsoleFront.once("consoleAPICall");
+ top.console.log("hello", document);
+ const {message} = await onConsoleAPICall;
+
+ info("checking the console API call packet");
+ checkConsoleAPICall(message, {
+ level: "log",
+ filename: /test_object_actor/,
+ arguments: ["hello", {
+ type: "object",
+ actor: /[a-z]/,
+ }],
+ });
+
+ info("inspecting object properties");
+ const args = message.arguments;
+ const {ownProperties, safeGetterValues} = await args[1].getPrototypeAndProperties();
+
+ const expectedProps = {
+ "location": {
+ get: {
+ type: "object",
+ class: "Function",
+ actor: /[a-z]/,
+ },
+ },
+ };
+ ok(Object.keys(ownProperties).length >= Object.keys(expectedProps).length,
+ "number of properties");
+
+ info("check ownProperties");
+ checkObject(ownProperties, expectedProps);
+
+ info("check safeGetterValues");
+ checkObject(safeGetterValues, {
+ "title": {
+ getterValue: /native getters in object actors/,
+ getterPrototypeLevel: 2,
+ },
+ "styleSheets": {
+ getterValue: /Front for obj\//,
+ getterPrototypeLevel: 2,
+ },
+ });
+
+ await closeDebugger(state);
+ SimpleTest.finish();
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_object_actor_native_getters_lenient_this.html b/devtools/shared/webconsole/test/chrome/test_object_actor_native_getters_lenient_this.html
new file mode 100644
index 0000000000..473dfb3b03
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_object_actor_native_getters_lenient_this.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test that WebIDL attributes with the LenientThis extended attribute
+ do not appear in the wrong objects</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the native getters in object actors</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+async function startTest() {
+ removeEventListener("load", startTest);
+ const {state} = await attachConsoleToTab(["ConsoleAPI"]);
+
+ const onConsoleApiCall = state.webConsoleFront.once("consoleAPICall");
+ const docAsProto = Object.create(document);
+ top.console.log("hello", docAsProto);
+ const {message} = await onConsoleApiCall;
+
+ info("checking the console API call packet");
+ checkConsoleAPICall(message, {
+ level: "log",
+ filename: /test_object_actor/,
+ arguments: ["hello", {
+ type: "object",
+ actor: /[a-z]/,
+ }],
+ });
+
+ info("inspecting object properties");
+ const args = message.arguments;
+
+ const {ownProperties, safeGetterValues} = await args[1].getPrototypeAndProperties();
+
+ is(Object.keys(ownProperties).length, 0, "number of properties");
+ is(Object.keys(safeGetterValues).length, 0, "number of safe getters");
+
+ await closeDebugger(state);
+ SimpleTest.finish();
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/chrome/test_page_errors.html b/devtools/shared/webconsole/test/chrome/test_page_errors.html
new file mode 100644
index 0000000000..0976eed92d
--- /dev/null
+++ b/devtools/shared/webconsole/test/chrome/test_page_errors.html
@@ -0,0 +1,224 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for page errors</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for page errors</p>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+const { MESSAGE_CATEGORY } = require("devtools/shared/constants");
+SimpleTest.waitForExplicitFinish();
+
+const previousEnabled = window.docShell.cssErrorReportingEnabled;
+window.docShell.cssErrorReportingEnabled = true;
+
+SimpleTest.registerCleanupFunction(() => {
+ window.docShell.cssErrorReportingEnabled = previousEnabled;
+});
+
+let expectedPageErrors = [];
+
+const NO_UNCAUGHT_EXCEPTION = Symbol();
+
+function doPageErrors() {
+ expectedPageErrors = {
+ "document.body.style.color = 'fooColor';": {
+ errorMessage: /fooColor/,
+ sourceName: /test_page_errors/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ },
+ "document.doTheImpossible();": {
+ errorMessage: /doTheImpossible/,
+ errorMessageName: undefined,
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "(42).toString(0);": {
+ errorMessage: /radix/,
+ errorMessageName: "JSMSG_BAD_RADIX",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;": {
+ errorMessage: /read.only/,
+ errorMessageName: "JSMSG_READ_ONLY",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "([]).length = -1": {
+ errorMessage: /array length/,
+ errorMessageName: "JSMSG_BAD_ARRAY_LENGTH",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "'abc'.repeat(-1);": {
+ errorMessage: /repeat count.*non-negative/,
+ errorMessageName: "JSMSG_NEGATIVE_REPETITION_COUNT",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "'a'.repeat(2e28);": {
+ errorMessage: /repeat count.*less than infinity/,
+ errorMessageName: "JSMSG_RESULTING_STRING_TOO_LARGE",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "77.1234.toExponential(-1);": {
+ errorMessage: /out of range/,
+ errorMessageName: "JSMSG_PRECISION_RANGE",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "function a() { return; 1 + 1; }": {
+ errorMessage: /unreachable code/,
+ errorMessageName: "JSMSG_STMT_AFTER_RETURN",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: false,
+ warning: true,
+ },
+ "let a, a;": {
+ errorMessage: /redeclaration of/,
+ errorMessageName: "JSMSG_REDECLARED_VAR",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ notes: [
+ {
+ messageBody: /Previously declared at line/,
+ frame: {
+ source: /test_page_errors/,
+ }
+ }
+ ]
+ },
+ [`let error = new TypeError("abc");
+ error.name = "MyError";
+ error.message = "here";
+ throw error`]: {
+ errorMessage: /MyError: here/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ "DOMTokenList.prototype.contains.call([])": {
+ errorMessage: /does not implement interface/,
+ errorMessageName: "MSG_METHOD_THIS_DOES_NOT_IMPLEMENT_INTERFACE",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ },
+ [`let error2 = new TypeError("abc");
+ error2.name = "MyPromiseError";
+ error2.message = "here2";
+ Promise.reject(error2)`]: {
+ errorMessage: /MyPromiseError: here2/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "chrome javascript",
+ timeStamp: FRACTIONAL_NUMBER_REGEX,
+ error: true,
+ warning: false,
+ // Promise.reject doesn't produce an uncaught exception
+ // even though |exception: true|.
+ [NO_UNCAUGHT_EXCEPTION]: true
+ }
+ };
+
+ let container = document.createElement("script");
+ for (const stmt of Object.keys(expectedPageErrors)) {
+ if (expectedPageErrors[stmt].error &&
+ !expectedPageErrors[stmt][NO_UNCAUGHT_EXCEPTION]) {
+ SimpleTest.expectUncaughtException();
+ }
+ info("starting stmt: " + stmt);
+ container = document.createElement("script");
+ document.body.appendChild(container);
+ container.textContent = stmt;
+ document.body.removeChild(container);
+ info("ending stmt: " + stmt);
+ }
+}
+
+async function startTest() {
+ removeEventListener("load", startTest);
+
+ const {state} = await attachConsole(["PageError"]);
+ onAttach(state);
+}
+
+function onAttach(state) {
+ onPageError = onPageError.bind(null, state);
+ state.webConsoleFront.on("pageError", onPageError);
+ doPageErrors();
+}
+
+const pageErrors = [];
+
+function onPageError(state, packet) {
+ if (!packet.pageError.sourceName.includes("test_page_errors")) {
+ info("Ignoring error from unknown source: " + packet.pageError.sourceName);
+ return;
+ }
+
+ pageErrors.push(packet.pageError);
+ if (pageErrors.length != Object.keys(expectedPageErrors).length) {
+ return;
+ }
+
+ state.webConsoleFront.off("pageError", onPageError);
+
+ Object.values(expectedPageErrors).forEach(function(message, index) {
+ info("checking received page error #" + index);
+ checkObject(pageErrors[index], Object.values(expectedPageErrors)[index]);
+ });
+
+ closeDebugger(state, function() {
+ SimpleTest.finish();
+ });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/webconsole/test/xpcshell/.eslintrc.js b/devtools/shared/webconsole/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/webconsole/test/xpcshell/head.js b/devtools/shared/webconsole/test/xpcshell/head.js
new file mode 100644
index 0000000000..e65552771e
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported require */
+
+"use strict";
+
+var { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js b/devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js
new file mode 100644
index 0000000000..3df015056f
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js
@@ -0,0 +1,225 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+const {
+ analyzeInputString,
+} = require("resource://devtools/shared/webconsole/analyze-input-string.js");
+
+add_task(() => {
+ const tests = [
+ {
+ desc: "simple property access",
+ input: `var a = {b: 1};a.b`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `var a = {b: 1};a`,
+ lastStatement: "a.b",
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "deep property access",
+ input: `a.b.c`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a.b`,
+ lastStatement: "a.b.c",
+ mainExpression: `a.b`,
+ matchProp: `c`,
+ },
+ },
+ {
+ desc: "element access",
+ input: `a["b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a["b`,
+ mainExpression: `a`,
+ matchProp: `"b`,
+ },
+ },
+ {
+ desc: "element access without quotes",
+ input: `a[b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a[b`,
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "simple optional chaining access",
+ input: `a?.b`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a?.b`,
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "deep optional chaining access",
+ input: `a?.b?.c`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a?.b`,
+ lastStatement: `a?.b?.c`,
+ mainExpression: `a?.b`,
+ matchProp: `c`,
+ },
+ },
+ {
+ desc: "optional chaining element access",
+ input: `a?.["b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a?.["b`,
+ mainExpression: `a`,
+ matchProp: `"b`,
+ },
+ },
+ {
+ desc: "optional chaining element access without quotes",
+ input: `a?.[b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a?.[b`,
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "deep optional chaining element access with quotes",
+ input: `var a = {b: 1, c: ["."]}; a?.["b"]?.c?.["d[.`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `var a = {b: 1, c: ["."]}; a?.["b"]?.c`,
+ lastStatement: `a?.["b"]?.c?.["d[.`,
+ mainExpression: `a?.["b"]?.c`,
+ matchProp: `"d[.`,
+ },
+ },
+ {
+ desc: "literal arrays with newline",
+ input: `[1,2,3,\n4\n].`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `[1,2,3,\n4\n]`,
+ lastStatement: `[1,2,3,4].`,
+ mainExpression: `[1,2,3,4]`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "number literal with newline",
+ input: `1\n.`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `1\n`,
+ lastStatement: `1\n.`,
+ mainExpression: `1`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "string literal",
+ input: `"abc".`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `"abc"`,
+ lastStatement: `"abc".`,
+ mainExpression: `"abc"`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "string literal containing backslash",
+ input: `"\\n".`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `"\\n"`,
+ lastStatement: `"\\n".`,
+ mainExpression: `"\\n"`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "single quote string literal containing backslash",
+ input: `'\\r'.`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `'\\r'`,
+ lastStatement: `'\\r'.`,
+ mainExpression: `'\\r'`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "template string literal containing backslash",
+ input: "`\\\\`.",
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: "`\\\\`",
+ lastStatement: "`\\\\`.",
+ mainExpression: "`\\\\`",
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "unterminated double quote string literal",
+ input: `"\n`,
+ expected: {
+ err: "unterminated string literal",
+ },
+ },
+ {
+ desc: "unterminated single quote string literal",
+ input: `'\n`,
+ expected: {
+ err: "unterminated string literal",
+ },
+ },
+ {
+ desc: "optional chaining operator with spaces",
+ input: `test ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `test ?. ["propA"] ?. [0] ?. ["propB"] `,
+ lastStatement: `test ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`,
+ mainExpression: `test ?. ["propA"] ?. [0] ?. ["propB"]`,
+ matchProp: `'to`,
+ },
+ },
+ ];
+
+ for (const { input, desc, expected } of tests) {
+ const result = analyzeInputString(input);
+ for (const [key, value] of Object.entries(expected)) {
+ Assert.equal(value, result[key], `${desc} | ${key} has expected value`);
+ }
+ }
+});
diff --git a/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js b/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js
new file mode 100644
index 0000000000..891eadb342
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js
@@ -0,0 +1,746 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+const {
+ fallibleJsPropertyProvider: jsPropertyProvider,
+} = require("resource://devtools/shared/webconsole/js-property-provider.js");
+
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+addDebuggerToGlobal(globalThis);
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+
+ const testArray = `var testArray = [
+ {propA: "A"},
+ {
+ propB: "B",
+ propC: [
+ "D"
+ ]
+ },
+ [
+ {propE: "E"}
+ ]
+ ]`;
+
+ const testObject = 'var testObject = {"propA": [{"propB": "B"}]}';
+ const testHyphenated = 'var testHyphenated = {"prop-A": "res-A"}';
+ const testLet = "let foobar = {a: ''}; const blargh = {a: 1};";
+
+ const testGenerators = `
+ // Test with generator using a named function.
+ function* genFunc() {
+ for (let i = 0; i < 10; i++) {
+ yield i;
+ }
+ }
+ let gen1 = genFunc();
+ gen1.next();
+
+ // Test with generator using an anonymous function.
+ let gen2 = (function* () {
+ for (let i = 0; i < 10; i++) {
+ yield i;
+ }
+ })();`;
+
+ const testGetters = `
+ var testGetters = {
+ get x() {
+ return Object.create(null, Object.getOwnPropertyDescriptors({
+ hello: "",
+ world: "",
+ }));
+ },
+ get y() {
+ return Object.create(null, Object.getOwnPropertyDescriptors({
+ get y() {
+ return "plop";
+ },
+ }));
+ }
+ };
+ `;
+
+ const testProxies = `
+ var testSelfPrototypeProxy = new Proxy({
+ hello: 1
+ }, {
+ getPrototypeOf: () => testProxy
+ });
+ var testArrayPrototypeProxy = new Proxy({
+ world: 2
+ }, {
+ getPrototypeOf: () => Array.prototype
+ })
+ `;
+
+ const sandbox = Cu.Sandbox("http://example.com");
+ const dbg = new Debugger();
+ const dbgObject = dbg.addDebuggee(sandbox);
+ const dbgEnv = dbgObject.asEnvironment();
+ Cu.evalInSandbox(
+ `
+ const hello = Object.create(null, Object.getOwnPropertyDescriptors({world: 1}));
+ String.prototype.hello = hello;
+ Number.prototype.hello = hello;
+ Array.prototype.hello = hello;
+ `,
+ sandbox
+ );
+ Cu.evalInSandbox(testArray, sandbox);
+ Cu.evalInSandbox(testObject, sandbox);
+ Cu.evalInSandbox(testHyphenated, sandbox);
+ Cu.evalInSandbox(testLet, sandbox);
+ Cu.evalInSandbox(testGenerators, sandbox);
+ Cu.evalInSandbox(testGetters, sandbox);
+ Cu.evalInSandbox(testProxies, sandbox);
+
+ info("Running tests with dbgObject");
+ runChecks(dbgObject, null, sandbox);
+
+ info("Running tests with dbgEnv");
+ runChecks(null, dbgEnv, sandbox);
+}
+
+function runChecks(dbgObject, environment, sandbox) {
+ const propertyProvider = (inputValue, options) =>
+ jsPropertyProvider({
+ dbgObject,
+ environment,
+ inputValue,
+ ...options,
+ });
+
+ info("Test that suggestions are given for 'this'");
+ let results = propertyProvider("t");
+ test_has_result(results, "this");
+
+ if (dbgObject != null) {
+ info("Test that suggestions are given for 'this.'");
+ results = propertyProvider("this.");
+ test_has_result(results, "testObject");
+
+ info("Test that suggestions are given for '(this).'");
+ results = propertyProvider("(this).");
+ test_has_result(results, "testObject");
+
+ info("Test that suggestions are given for deep 'this' properties access");
+ results = propertyProvider("(this).testObject.propA.");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("(this).testObject.propA[");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(this)['testObject']['propA'][");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(this).testObject['propA'].");
+ test_has_result(results, "shift");
+
+ info("Test that no suggestions are given for 'this.this'");
+ results = propertyProvider("this.this");
+ test_has_no_results(results);
+ }
+
+ info("Test that suggestions are given for 'globalThis'");
+ results = propertyProvider("g");
+ test_has_result(results, "globalThis");
+
+ info("Test that suggestions are given for 'globalThis.'");
+ results = propertyProvider("globalThis.");
+ test_has_result(results, "testObject");
+
+ info("Test that suggestions are given for '(globalThis).'");
+ results = propertyProvider("(globalThis).");
+ test_has_result(results, "testObject");
+
+ info(
+ "Test that suggestions are given for deep 'globalThis' properties access"
+ );
+ results = propertyProvider("(globalThis).testObject.propA.");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("(globalThis).testObject.propA[");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(globalThis)['testObject']['propA'][");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(globalThis).testObject['propA'].");
+ test_has_result(results, "shift");
+
+ info("Testing lexical scope issues (Bug 1207868)");
+ results = propertyProvider("foobar");
+ test_has_result(results, "foobar");
+
+ results = propertyProvider("foobar.");
+ test_has_result(results, "a");
+
+ results = propertyProvider("blargh");
+ test_has_result(results, "blargh");
+
+ results = propertyProvider("blargh.");
+ test_has_result(results, "a");
+
+ info("Test that suggestions are given for 'foo[n]' where n is an integer.");
+ results = propertyProvider("testArray[0].");
+ test_has_result(results, "propA");
+
+ info("Test that suggestions are given for multidimensional arrays.");
+ results = propertyProvider("testArray[2][0].");
+ test_has_result(results, "propE");
+
+ info("Test that suggestions are given for nested arrays.");
+ results = propertyProvider("testArray[1].propC[0].");
+ test_has_result(results, "indexOf");
+
+ info("Test that suggestions are given for literal arrays.");
+ results = propertyProvider("[1,2,3].");
+ test_has_result(results, "indexOf");
+
+ results = propertyProvider("[1,2,3].h");
+ test_has_result(results, "hello");
+
+ results = propertyProvider("[1,2,3].hello.w");
+ test_has_result(results, "world");
+
+ info("Test that suggestions are given for literal arrays with newlines.");
+ results = propertyProvider("[1,2,3,\n4\n].");
+ test_has_result(results, "indexOf");
+
+ info("Test that suggestions are given for literal strings.");
+ results = propertyProvider("'foo'.");
+ test_has_result(results, "charAt");
+ results = propertyProvider('"foo".');
+ test_has_result(results, "charAt");
+ results = propertyProvider("`foo`.");
+ test_has_result(results, "charAt");
+ results = propertyProvider("`foo doc`.");
+ test_has_result(results, "charAt");
+ results = propertyProvider('`foo " doc`.');
+ test_has_result(results, "charAt");
+ results = propertyProvider("`foo ' doc`.");
+ test_has_result(results, "charAt");
+ results = propertyProvider("'[1,2,3]'.");
+ test_has_result(results, "charAt");
+ results = propertyProvider("'foo'.h");
+ test_has_result(results, "hello");
+ results = propertyProvider("'foo'.hello.w");
+ test_has_result(results, "world");
+ results = propertyProvider(`"\\n".`);
+ test_has_result(results, "charAt");
+ results = propertyProvider(`'\\r'.`);
+ test_has_result(results, "charAt");
+ results = propertyProvider("`\\\\`.");
+ test_has_result(results, "charAt");
+
+ info("Test that suggestions are not given for syntax errors.");
+ results = propertyProvider("'foo\"");
+ Assert.equal(null, results);
+ results = propertyProvider("'foo d");
+ Assert.equal(null, results);
+ results = propertyProvider(`"foo d`);
+ Assert.equal(null, results);
+ results = propertyProvider("`foo d");
+ Assert.equal(null, results);
+ results = propertyProvider("[1,',2]");
+ Assert.equal(null, results);
+ results = propertyProvider("'[1,2].");
+ Assert.equal(null, results);
+ results = propertyProvider("'foo'..");
+ Assert.equal(null, results);
+
+ info("Test that suggestions are not given without a dot.");
+ results = propertyProvider("'foo'");
+ test_has_no_results(results);
+ results = propertyProvider("`foo`");
+ test_has_no_results(results);
+ results = propertyProvider("[1,2,3]");
+ test_has_no_results(results);
+ results = propertyProvider("[1,2,3].\n'foo'");
+ test_has_no_results(results);
+
+ info("Test that suggestions are not given for index that's out of bounds.");
+ results = propertyProvider("testArray[10].");
+ Assert.equal(null, results);
+
+ info("Test that invalid element access syntax does not return anything");
+ results = propertyProvider("testArray[][1].");
+ Assert.equal(null, results);
+
+ info("Test that deep element access works.");
+ results = propertyProvider("testObject['propA'][0].");
+ test_has_result(results, "propB");
+
+ results = propertyProvider("testArray[1]['propC'].");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("testArray[1].propC[0][");
+ test_has_result(results, `"trim"`);
+
+ results = propertyProvider("testArray[1].propC[0].");
+ test_has_result(results, "trim");
+
+ info(
+ "Test that suggestions are displayed when variable is wrapped in parens"
+ );
+ results = propertyProvider("(testObject)['propA'][0].");
+ test_has_result(results, "propB");
+
+ results = propertyProvider("(testArray)[1]['propC'].");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("(testArray)[1].propC[0][");
+ test_has_result(results, `"trim"`);
+
+ results = propertyProvider("(testArray)[1].propC[0].");
+ test_has_result(results, "trim");
+
+ info("Test that suggestions are given if there is an hyphen in the chain.");
+ results = propertyProvider("testHyphenated['prop-A'].");
+ test_has_result(results, "trim");
+
+ info("Test that we have suggestions for generators.");
+ const gen1Result = Cu.evalInSandbox("gen1.next().value", sandbox);
+ results = propertyProvider("gen1.");
+ test_has_result(results, "next");
+ info("Test that the generator next() was not executed");
+ const gen1NextResult = Cu.evalInSandbox("gen1.next().value", sandbox);
+ Assert.equal(gen1Result + 1, gen1NextResult);
+
+ info("Test with an anonymous generator.");
+ const gen2Result = Cu.evalInSandbox("gen2.next().value", sandbox);
+ results = propertyProvider("gen2.");
+ test_has_result(results, "next");
+ const gen2NextResult = Cu.evalInSandbox("gen2.next().value", sandbox);
+ Assert.equal(gen2Result + 1, gen2NextResult);
+
+ info(
+ "Test that getters are not executed if authorizedEvaluations is undefined"
+ );
+ results = propertyProvider("testGetters.x.");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x[");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.hell");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x['hell");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ info(
+ "Test that getters are not executed if authorizedEvaluations does not match"
+ );
+ results = propertyProvider("testGetters.x.", { authorizedEvaluations: [] });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGetters"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGtrs", "x"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["x"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ info("Test that deep getter property access returns intermediate getters");
+ results = propertyProvider("testGetters.y.y.");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ results = propertyProvider("testGetters['y'].y.");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ results = propertyProvider("testGetters['y']['y'].");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ results = propertyProvider("testGetters.y['y'].");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ info("Test that deep getter property access invoke intermediate getters");
+ results = propertyProvider("testGetters.y.y.", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ results = propertyProvider("testGetters['y'].y.", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ results = propertyProvider("testGetters['y']['y'].", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ results = propertyProvider("testGetters.y['y'].", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ info(
+ "Test that getters are executed if matching an authorizedEvaluation element"
+ );
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGetters", "x"]],
+ });
+ test_has_exact_results(results, ["hello", "world"]);
+ Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false);
+ Assert.ok(Object.keys(results).includes("getterPath") === false);
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGetters", "x"], ["y"]],
+ });
+ test_has_exact_results(results, ["hello", "world"]);
+ Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false);
+ Assert.ok(Object.keys(results).includes("getterPath") === false);
+
+ info("Test that executing getters filters with provided string");
+ results = propertyProvider("testGetters.x.hell", {
+ authorizedEvaluations: [["testGetters", "x"]],
+ });
+ test_has_exact_results(results, ["hello"]);
+
+ results = propertyProvider("testGetters.x['hell", {
+ authorizedEvaluations: [["testGetters", "x"]],
+ });
+ test_has_exact_results(results, ["'hello'"]);
+
+ info(
+ "Test children getters are not executed if not included in authorizedEvaluation"
+ );
+ results = propertyProvider("testGetters.y.y.", {
+ authorizedEvaluations: [["testGetters", "y", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ info(
+ "Test children getters are executed if matching an authorizedEvaluation element"
+ );
+ results = propertyProvider("testGetters.y.y.", {
+ authorizedEvaluations: [
+ ["testGetters", "y"],
+ ["testGetters", "y", "y"],
+ ],
+ });
+ test_has_result(results, "trim");
+
+ info("Test with number literals");
+ results = propertyProvider("1.");
+ Assert.ok(results === null, "Does not complete on possible floating number");
+
+ results = propertyProvider("(1)..");
+ Assert.ok(results === null, "Does not complete on invalid syntax");
+
+ results = propertyProvider("(1.1.).");
+ Assert.ok(results === null, "Does not complete on invalid syntax");
+
+ results = propertyProvider("1..");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("1 .");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("1\n.");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider(".1.");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("1[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("1[toFixed");
+ test_has_exact_results(results, [`"toFixed"`]);
+
+ results = propertyProvider("1['toFixed");
+ test_has_exact_results(results, ["'toFixed'"]);
+
+ results = propertyProvider("1.1[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("(1).");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("(.1).");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("(1.1).");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("(1).toFixed");
+ test_has_exact_results(results, ["toFixed"]);
+
+ results = propertyProvider("(1)[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("(1.1)[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("(1)[toFixed");
+ test_has_exact_results(results, [`"toFixed"`]);
+
+ results = propertyProvider("(1)['toFixed");
+ test_has_exact_results(results, ["'toFixed'"]);
+
+ results = propertyProvider("(1).h");
+ test_has_result(results, "hello");
+
+ results = propertyProvider("(1).hello.w");
+ test_has_result(results, "world");
+
+ info("Test access on dot-notation invalid property name");
+ results = propertyProvider("testHyphenated.prop");
+ Assert.ok(
+ !results.matches.has("prop-A"),
+ "Does not return invalid property name on dot access"
+ );
+
+ results = propertyProvider("testHyphenated['prop");
+ test_has_result(results, `'prop-A'`);
+
+ results = propertyProvider(`//t`);
+ Assert.ok(results === null, "Does not complete in inline comment");
+
+ results = propertyProvider(`// t`);
+ Assert.ok(
+ results === null,
+ "Does not complete in inline comment after space"
+ );
+
+ results = propertyProvider(`//I'm a comment\nt`);
+ test_has_result(results, "testObject");
+
+ results = propertyProvider(`1/t`);
+ test_has_result(results, "testObject");
+
+ results = propertyProvider(`/* t`);
+ Assert.ok(results === null, "Does not complete in multiline comment");
+
+ results = propertyProvider(`/*I'm\nt`);
+ Assert.ok(
+ results === null,
+ "Does not complete in multiline comment after line break"
+ );
+
+ results = propertyProvider(`/*I'm a comment\n \t * /t`);
+ Assert.ok(
+ results === null,
+ "Does not complete in multiline comment after line break and invalid comment end"
+ );
+
+ results = propertyProvider(`/*I'm a comment\n \t */t`);
+ test_has_result(results, "testObject");
+
+ results = propertyProvider(`/*I'm a comment\n \t */\n\nt`);
+ test_has_result(results, "testObject");
+
+ info("Test local expression variables");
+ results = propertyProvider("b", { expressionVars: ["a", "b", "c"] });
+ test_has_result(results, "b");
+ Assert.equal(results.matches.has("a"), false);
+ Assert.equal(results.matches.has("c"), false);
+
+ info(
+ "Test that local expression variables are not included when accessing an object properties"
+ );
+ results = propertyProvider("testObject.prop", {
+ expressionVars: ["propLocal"],
+ });
+ Assert.equal(results.matches.has("propLocal"), false);
+ test_has_result(results, "propA");
+
+ results = propertyProvider("testObject['prop", {
+ expressionVars: ["propLocal"],
+ });
+ test_has_result(results, "'propA'");
+ Assert.equal(results.matches.has("propLocal"), false);
+
+ info("Test that expression with optional chaining operator are completed");
+ results = propertyProvider("testObject?.prop");
+ test_has_result(results, "propA");
+
+ results = propertyProvider("testObject?.propA[0]?.propB?.to");
+ test_has_result(results, "toString");
+
+ results = propertyProvider("testObject?.propA?.[0]?.propB?.to");
+ test_has_result(results, "toString");
+
+ results = propertyProvider(
+ "testObject ?. propA[0] ?. propB ?. to"
+ );
+ test_has_result(results, "toString");
+
+ results = propertyProvider("testObject?.[prop");
+ test_has_result(results, '"propA"');
+
+ results = propertyProvider(`testObject?.["prop`);
+ test_has_result(results, '"propA"');
+
+ results = propertyProvider(`testObject?.['prop`);
+ test_has_result(results, `'propA'`);
+
+ results = propertyProvider(`testObject?.["propA"]?.[0]?.["propB"]?.["to`);
+ test_has_result(results, `"toString"`);
+
+ results = propertyProvider(
+ `testObject ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`
+ );
+ test_has_result(results, "'toString'");
+
+ results = propertyProvider("[1,2,3]?.");
+ test_has_result(results, "indexOf");
+
+ results = propertyProvider("'foo'?.");
+ test_has_result(results, "charAt");
+
+ results = propertyProvider("1?.");
+ test_has_result(results, "toFixed");
+
+ // check this doesn't throw since `propC` is not defined.
+ results = propertyProvider("testObject?.propC?.this?.does?.not?.exist?.d");
+
+ // check that ternary operator isn't mistaken for optional chaining
+ results = propertyProvider(`true?.3.to`);
+ test_has_result(results, `toExponential`);
+
+ results = propertyProvider(`true?.3?.to`);
+ test_has_result(results, `toExponential`);
+
+ // Test more ternary
+ results = propertyProvider(`true?t`);
+ test_has_result(results, `testObject`);
+
+ results = propertyProvider(`true??t`);
+ test_has_result(results, `testObject`);
+
+ results = propertyProvider(`true?/* comment */t`);
+ test_has_result(results, `testObject`);
+
+ results = propertyProvider(`true?<t`);
+ test_has_no_results(results);
+
+ // Test autocompletion on debugger statement does not throw
+ results = propertyProvider(`debugger.`);
+ Assert.ok(results === null, "Does not complete a debugger keyword");
+
+ // Test autocompletion on Proxies
+ // proxy does not get autocompletion result from prototype defined in `getPrototypeOf`
+ test_has_no_results(propertyProvider(`testArrayPrototypeProxy.filte`));
+ results = propertyProvider(`testArrayPrototypeProxy.`);
+ // it does get the own property
+ test_has_result(results, `world`);
+ // as well as method from the actual proxy target prototype
+ test_has_result(results, `hasOwnProperty`);
+
+ results = propertyProvider(`testSelfPrototypeProxy.`);
+ test_has_result(results, `hello`);
+ test_has_result(results, `hasOwnProperty`);
+}
+
+/**
+ * A helper that ensures an empty array of results were found.
+ * @param Object results
+ * The results returned by jsPropertyProvider.
+ */
+function test_has_no_results(results) {
+ Assert.notEqual(results, null);
+ Assert.equal(results.matches.size, 0);
+}
+/**
+ * A helper that ensures (required) results were found.
+ * @param Object results
+ * The results returned by jsPropertyProvider.
+ * @param String requiredSuggestion
+ * A suggestion that must be found from the results.
+ */
+function test_has_result(results, requiredSuggestion) {
+ Assert.notEqual(results, null);
+ Assert.ok(results.matches.size > 0);
+ Assert.ok(
+ results.matches.has(requiredSuggestion),
+ `<${requiredSuggestion}> found in ${[...results.matches.values()].join(
+ " - "
+ )}`
+ );
+}
+
+/**
+ * A helper that ensures results are the expected ones.
+ * @param Object results
+ * The results returned by jsPropertyProvider.
+ * @param Array expectedMatches
+ * An array of the properties that should be returned by jsPropertyProvider.
+ */
+function test_has_exact_results(results, expectedMatches) {
+ Assert.deepEqual([...results.matches], expectedMatches);
+}
diff --git a/devtools/shared/webconsole/test/xpcshell/xpcshell.toml b/devtools/shared/webconsole/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..992064a108
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/xpcshell.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = "devtools"
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = ""
+
+["test_analyze_input_string.js"]
+
+["test_js_property_provider.js"]
diff --git a/devtools/shared/webextension-fallback.html b/devtools/shared/webextension-fallback.html
new file mode 100644
index 0000000000..1ac7005851
--- /dev/null
+++ b/devtools/shared/webextension-fallback.html
@@ -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 https://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<h1>Your addon does not have any document opened yet.</h1>
diff --git a/devtools/shared/worker/helper.js b/devtools/shared/worker/helper.js
new file mode 100644
index 0000000000..20ccd3de5e
--- /dev/null
+++ b/devtools/shared/worker/helper.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/. */
+
+/* eslint-env amd */
+
+"use strict";
+
+/* global workerHelper */
+/* exported workerHelper */
+(function (root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ } else if (typeof exports === "object") {
+ module.exports = factory();
+ } else {
+ root.workerHelper = factory();
+ }
+})(this, function () {
+ /**
+ * This file is to only be included by ChromeWorkers. This exposes
+ * a `createTask` function to workers to register tasks for communication
+ * back to `devtools/shared/worker`.
+ *
+ * Tasks can be send their responses via a return value, either a primitive
+ * or a promise.
+ *
+ * createTask(self, "average", function (data) {
+ * return data.reduce((sum, val) => sum + val, 0) / data.length;
+ * });
+ *
+ * createTask(self, "average", function (data) {
+ * return new Promise((resolve, reject) => {
+ * resolve(data.reduce((sum, val) => sum + val, 0) / data.length);
+ * });
+ * });
+ *
+ *
+ * Errors:
+ *
+ * Returning an Error value, or if the returned promise is rejected, this
+ * propagates to the DevToolsWorker as a rejected promise. If an error is
+ * thrown in a synchronous function, that error is also propagated.
+ */
+
+ /**
+ * Takes a worker's `self` object, a task name, and a function to
+ * be called when that task is called. The task is called with the
+ * passed in data as the first argument
+ *
+ * @param {object} self
+ * @param {string} name
+ * @param {function} fn
+ */
+ function createTask(self, name, fn) {
+ // Store a hash of task name to function on the Worker
+ if (!self._tasks) {
+ self._tasks = {};
+ }
+
+ // Create the onmessage handler if not yet created.
+ if (!self.onmessage) {
+ self.onmessage = createHandler(self);
+ }
+
+ // Store the task on the worker.
+ self._tasks[name] = fn;
+ }
+
+ /**
+ * Creates the `self.onmessage` handler for a Worker.
+ *
+ * @param {object} self
+ * @return {function}
+ */
+ function createHandler(self) {
+ return function (e) {
+ const { id, task, data } = e.data;
+ const taskFn = self._tasks[task];
+
+ if (!taskFn) {
+ self.postMessage({ id, error: `Task "${task}" not found in worker.` });
+ return;
+ }
+
+ try {
+ handleResponse(taskFn(data));
+ } catch (ex) {
+ handleError(ex);
+ }
+
+ function handleResponse(response) {
+ // If a promise
+ if (response && typeof response.then === "function") {
+ response.then(
+ val => self.postMessage({ id, response: val }),
+ handleError
+ );
+ } else if (response instanceof Error) {
+ // If an error object
+ handleError(response);
+ } else {
+ // If anything else
+ self.postMessage({ id, response });
+ }
+ }
+
+ function handleError(error = "Error") {
+ try {
+ // First, try and structured clone the error across directly.
+ self.postMessage({ id, error });
+ } catch (x) {
+ // We could not clone whatever error value was given. Do our best to
+ // stringify it.
+ let errorString = `Error while performing task "${task}": `;
+
+ try {
+ errorString += error.toString();
+ } catch (ex) {
+ errorString += "<could not stringify error>";
+ }
+
+ if ("stack" in error) {
+ try {
+ errorString += "\n" + error.stack;
+ } catch (err) {
+ // Do nothing
+ }
+ }
+
+ self.postMessage({ id, error: errorString });
+ }
+ }
+ };
+ }
+
+ return { createTask };
+});
diff --git a/devtools/shared/worker/moz.build b/devtools/shared/worker/moz.build
new file mode 100644
index 0000000000..79a71ca5ce
--- /dev/null
+++ b/devtools/shared/worker/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/.
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+DevToolsModules(
+ "helper.js",
+ "worker.js",
+)
diff --git a/devtools/shared/worker/tests/browser/browser.toml b/devtools/shared/worker/tests/browser/browser.toml
new file mode 100644
index 0000000000..4bab73b019
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = ["../../../../server/tests/browser/head.js"]
+
+["browser_worker-01.js"]
+
+["browser_worker-02.js"]
+
+["browser_worker-03.js"]
diff --git a/devtools/shared/worker/tests/browser/browser_worker-01.js b/devtools/shared/worker/tests/browser/browser_worker-01.js
new file mode 100644
index 0000000000..a8dafcf4cb
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser_worker-01.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the devtools/shared/worker communicates properly
+// as both CommonJS module and as a JSM.
+
+const BUFFER_SIZE = 8;
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+});
+
+add_task(async function () {
+ // Test both CJS and JSM versions
+
+ await testWorker("JSM", () =>
+ ChromeUtils.import("resource://devtools/shared/worker/worker.js")
+ );
+ await testWorker("CommonJS", () =>
+ require("resource://devtools/shared/worker/worker.js")
+ );
+ await testTransfer();
+});
+
+async function testWorker(context, workerFactory) {
+ // Needed for blob:null
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ const { DevToolsWorker, workerify } = workerFactory();
+
+ const blob = new Blob(
+ [
+ `
+importScripts("resource://gre/modules/workers/require.js");
+const { createTask } = require("resource://devtools/shared/worker/helper.js");
+
+createTask(self, "groupByField", function({
+ items,
+ groupField
+}) {
+ const groups = {};
+ for (const item of items) {
+ if (!groups[item[groupField]]) {
+ groups[item[groupField]] = [];
+ }
+ groups[item[groupField]].push(item);
+ }
+ return { groups };
+});
+ `,
+ ],
+ { type: "application/javascript" }
+ );
+
+ const WORKER_URL = URL.createObjectURL(blob);
+ const worker = new DevToolsWorker(WORKER_URL);
+
+ const results = await worker.performTask("groupByField", {
+ items: [
+ { name: "Paris", country: "France" },
+ { name: "Lagos", country: "Nigeria" },
+ { name: "Lyon", country: "France" },
+ ],
+ groupField: "country",
+ });
+
+ is(
+ Object.keys(results.groups).join(","),
+ "France,Nigeria",
+ `worker should have returned the expected result in ${context}`
+ );
+
+ URL.revokeObjectURL(WORKER_URL);
+
+ const fn = workerify(x => x * x);
+ is(await fn(5), 25, `workerify works in ${context}`);
+ fn.destroy();
+
+ worker.destroy();
+}
+
+async function testTransfer() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ const { workerify } = ChromeUtils.import(
+ "resource://devtools/shared/worker/worker.js"
+ );
+ const workerFn = workerify(({ buf }) => buf.byteLength);
+ const buf = new ArrayBuffer(BUFFER_SIZE);
+
+ is(
+ buf.byteLength,
+ BUFFER_SIZE,
+ "Size of the buffer before transfer is correct."
+ );
+
+ is(await workerFn({ buf }), 8, "Sent array buffer to worker");
+ is(buf.byteLength, 8, "Array buffer was copied, not transferred.");
+
+ is(await workerFn({ buf }, [buf]), 8, "Sent array buffer to worker");
+ is(buf.byteLength, 0, "Array buffer was transferred, not copied.");
+
+ workerFn.destroy();
+}
diff --git a/devtools/shared/worker/tests/browser/browser_worker-02.js b/devtools/shared/worker/tests/browser/browser_worker-02.js
new file mode 100644
index 0000000000..80c50cf887
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser_worker-02.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests errors are handled properly by the DevToolsWorker.
+
+const {
+ DevToolsWorker,
+} = require("resource://devtools/shared/worker/worker.js");
+
+const blob = new Blob(
+ [
+ `
+importScripts("resource://gre/modules/workers/require.js");
+const { createTask } = require("resource://devtools/shared/worker/helper.js");
+
+createTask(self, "myTask", function({
+ shouldThrow,
+} = {}) {
+ if (shouldThrow) {
+ throw new Error("err");
+ }
+
+ return "OK";
+});
+ `,
+ ],
+ { type: "application/javascript" }
+);
+
+add_task(async function () {
+ try {
+ new DevToolsWorker("resource://i/dont/exist.js");
+ ok(false, "Creating a DevToolsWorker with an invalid URL throws");
+ } catch (e) {
+ ok(true, "Creating a DevToolsWorker with an invalid URL throws");
+ }
+
+ const WORKER_URL = URL.createObjectURL(blob);
+ const worker = new DevToolsWorker(WORKER_URL);
+ try {
+ await worker.performTask("myTask", { shouldThrow: true });
+ ok(
+ false,
+ "DevToolsWorker returns a rejected promise when an error occurs in the worker"
+ );
+ } catch (e) {
+ ok(
+ true,
+ "DevToolsWorker returns a rejected promise when an error occurs in the worker"
+ );
+ }
+
+ try {
+ await worker.performTask("not a real task");
+ ok(
+ false,
+ "DevToolsWorker returns a rejected promise when task does not exist"
+ );
+ } catch (e) {
+ ok(
+ true,
+ "DevToolsWorker returns a rejected promise when task does not exist"
+ );
+ }
+
+ worker.destroy();
+ try {
+ await worker.performTask("myTask");
+ ok(
+ false,
+ "DevToolsWorker rejects when performing a task on a destroyed worker"
+ );
+ } catch (e) {
+ ok(
+ true,
+ "DevToolsWorker rejects when performing a task on a destroyed worker"
+ );
+ }
+
+ URL.revokeObjectURL(WORKER_URL);
+});
diff --git a/devtools/shared/worker/tests/browser/browser_worker-03.js b/devtools/shared/worker/tests/browser/browser_worker-03.js
new file mode 100644
index 0000000000..185ba92d5e
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser_worker-03.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the devtools/shared/worker can handle:
+// returned primitives (or promise or Error)
+//
+// And tests `workerify` by doing so.
+
+const { workerify } = require("resource://devtools/shared/worker/worker.js");
+function square(x) {
+ return x * x;
+}
+
+function squarePromise(x) {
+ return new Promise(resolve => resolve(x * x));
+}
+
+function squareError(x) {
+ return new Error("Nope");
+}
+
+function squarePromiseReject(x) {
+ return new Promise((_, reject) => reject("Nope"));
+}
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+});
+
+add_task(async function () {
+ // Needed for blob:null
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ let fn = workerify(square);
+ is(await fn(5), 25, "return primitives successful");
+ fn.destroy();
+
+ fn = workerify(squarePromise);
+ is(await fn(5), 25, "promise primitives successful");
+ fn.destroy();
+
+ fn = workerify(squareError);
+ try {
+ await fn(5);
+ ok(false, "return error should reject");
+ } catch (e) {
+ ok(true, "return error should reject");
+ }
+ fn.destroy();
+
+ fn = workerify(squarePromiseReject);
+ try {
+ await fn(5);
+ ok(false, "returned rejected promise rejects");
+ } catch (e) {
+ ok(true, "returned rejected promise rejects");
+ }
+ fn.destroy();
+});
diff --git a/devtools/shared/worker/worker.js b/devtools/shared/worker/worker.js
new file mode 100644
index 0000000000..4d753a928e
--- /dev/null
+++ b/devtools/shared/worker/worker.js
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ChromeWorker */
+
+(function (factory) {
+ if (this.module && module.id.includes("worker")) {
+ // require
+ const dumpn = require("devtools/shared/DevToolsUtils").dumpn;
+ factory.call(
+ this,
+ require,
+ exports,
+ module,
+ { Cc, Ci, Cu },
+ ChromeWorker,
+ dumpn
+ );
+ } else {
+ // Cu.import
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ this.isWorker = false;
+ this.console = console;
+ factory.call(
+ this,
+ require,
+ this,
+ { exports: this },
+ { Cc, Ci, Cu },
+ ChromeWorker,
+ null
+ );
+ this.EXPORTED_SYMBOLS = ["DevToolsWorker", "workerify"];
+ }
+}).call(
+ this,
+ function (require, exports, module, { Ci, Cc }, ChromeWorker, dumpn) {
+ let MESSAGE_COUNTER = 0;
+
+ /**
+ * Creates a wrapper around a ChromeWorker, providing easy
+ * communication to offload demanding tasks. The corresponding URL
+ * must implement the interface provided by `devtools/shared/worker/helper`.
+ *
+ * @param {string} url
+ * The URL of the worker.
+ * @param Object opts
+ * An option with the following optional fields:
+ * - name: a name that will be printed with logs
+ * - verbose: log incoming and outgoing messages
+ */
+ function DevToolsWorker(url, opts) {
+ opts = opts || {};
+ this._worker = new ChromeWorker(url);
+ this._verbose = opts.verbose;
+ this._name = opts.name;
+
+ this._worker.addEventListener("error", this.onError);
+ }
+ exports.DevToolsWorker = DevToolsWorker;
+
+ /**
+ * Performs the given task in a chrome worker, passing in data.
+ * Returns a promise that resolves when the task is completed, resulting in
+ * the return value of the task.
+ *
+ * @param {string} task
+ * The name of the task to execute in the worker.
+ * @param {any} data
+ * Data to be passed into the task implemented by the worker.
+ * @param {undefined|Array} transfer
+ * Optional array of transferable objects to transfer ownership of.
+ * @return {Promise}
+ */
+ DevToolsWorker.prototype.performTask = function (task, data, transfer) {
+ if (this._destroyed) {
+ return Promise.reject(
+ "Cannot call performTask on a destroyed DevToolsWorker"
+ );
+ }
+ const worker = this._worker;
+ const id = ++MESSAGE_COUNTER;
+ const payload = { task, id, data };
+
+ if (this._verbose && dumpn) {
+ dumpn(
+ "Sending message to worker" +
+ (this._name ? " (" + this._name + ")" : "") +
+ ": " +
+ JSON.stringify(payload, null, 2)
+ );
+ }
+ worker.postMessage(payload, transfer);
+
+ return new Promise((resolve, reject) => {
+ const listener = ({ data: result }) => {
+ if (this._verbose && dumpn) {
+ dumpn(
+ "Received message from worker" +
+ (this._name ? " (" + this._name + ")" : "") +
+ ": " +
+ JSON.stringify(result, null, 2)
+ );
+ }
+
+ if (result.id !== id) {
+ return;
+ }
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+
+ /**
+ * Terminates the underlying worker. Use when no longer needing the worker.
+ */
+ DevToolsWorker.prototype.destroy = function () {
+ this._worker.terminate();
+ this._worker = null;
+ this._destroyed = true;
+ };
+
+ DevToolsWorker.prototype.onError = function ({
+ message,
+ filename,
+ lineno,
+ }) {
+ dump(new Error(message + " @ " + filename + ":" + lineno) + "\n");
+ };
+
+ /**
+ * Takes a function and returns a Worker-wrapped version of the same function.
+ * Returns a promise upon resolution.
+ * @see `./devtools/shared/shared/tests/browser/browser_devtools-worker-03.js
+ *
+ * ⚠ This should only be used for tests or A/B testing performance ⚠
+ *
+ * The original function must:
+ *
+ * Be a pure function, that is, not use any variables not declared within the
+ * function, or its arguments.
+ *
+ * Return a value or a promise.
+ *
+ * Note any state change in the worker will not affect the callee's context.
+ *
+ * @param {function} fn
+ * @return {function}
+ */
+ function workerify(fn) {
+ console.warn(
+ "`workerify` should only be used in tests or measuring performance. " +
+ "This creates an object URL on the browser window, and should not be " +
+ "used in production."
+ );
+ // Fetch modules here as we don't want to include it normally.
+ const { URL, Blob } =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ const stringifiedFn = createWorkerString(fn);
+ const blob = new Blob([stringifiedFn]);
+ const url = URL.createObjectURL(blob);
+ const worker = new DevToolsWorker(url);
+
+ const wrapperFn = (data, transfer) =>
+ worker.performTask("workerifiedTask", data, transfer);
+
+ wrapperFn.destroy = function () {
+ URL.revokeObjectURL(url);
+ worker.destroy();
+ };
+
+ return wrapperFn;
+ }
+ exports.workerify = workerify;
+
+ /**
+ * Takes a function, and stringifies it, attaching the worker-helper.js
+ * boilerplate hooks.
+ */
+ function createWorkerString(fn) {
+ return `importScripts("resource://gre/modules/workers/require.js");
+ const { createTask } = require("resource://devtools/shared/worker/helper.js");
+ createTask(self, "workerifiedTask", ${fn.toString()});`;
+ }
+ }
+);